Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Evoluindo arquiteturas reativas

Evoluindo arquiteturas reativas

Slides de apoio para minha apresentação da sobre aprendizados na evolução do design arquitetural de aplicações, adquiridos ao longo de diversas interações durante o dia-a-dia da construção de apps (Android / mobile) powered by Reactive Programming / RxJava

Apresentação oferecida nos seguintes eventos

- QConSP 2017 (abril/2017)
- GDG-SP Android Meetup #49 (julho/2017)
- The Developers Conference / Trilha Android (julho/2017)

Ubiratan Soares
PRO

April 26, 2017
Tweet

More Decks by Ubiratan Soares

Other Decks in Programming

Transcript

  1. EVOLUINDO
    ARQUITETURAS
    REATIVAS
    Ubiratan Soares
    Julho / 2017

    View Slide

  2. O QUE É UMA
    ARQUITETURA EM
    MOBILE ?

    View Slide

  3. MVP
    MVVM
    VIPER
    FLUX REDUX
    CLEAN
    MVC

    MVI

    View Slide

  4. View Slide

  5. PRINCÍPIOS DE ARQUITETURA
    Organização
    Facilidade em se encontrar o que se precisa
    Menor impedância para se resolver bugs
    Menos dor ao escalar em tamanho (codebase e devs)
    Estilo de projeto unificado, definido e defendido pelo time

    View Slide

  6. UMA QUEIXA COMUM
    NA COMUNIDADE
    MOBILE ?

    View Slide

  7. TEM PELO MENOS UM UNIT TEST NO APP?

    View Slide

  8. EM MOBILE,
    ARQUITETURA É CRÍTICA
    PARA TESTABILIDADE

    View Slide

  9. QUAL ARQUITETURA
    ESCOLHER ENTÃO ???

    View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. NÃO HÁ
    SILVER
    BULLETS!

    View Slide

  14. ESTUDO DE CASO
    MVP / CLEAN

    View Slide

  15. PRESENTATION
    LAYER
    DATA
    LAYER
    DB
    REST
    ETC
    UI
    . . .

    View Slide

  16. public interface ViewDelegate {
    void displayResults(DataModel model);
    void networkingError();
    void displayEmptyState();
    void displayErrorState();
    // More delegation
    }

    View Slide

  17. public class MainActivity extends AppCompatActivity implements ViewDelegate {
    Presenter presenter; // How to resolve this instance ???
    @Override protected void onStart() {
    super.onStart();
    presenter.bindView(this);
    presenter.fetchData();
    }
    @Override public void displayResults(DataModel model) {
    // Put data into view
    }
    @Override public void networkingError() {
    // Up to you
    }
    @Override public void displayEmptyState() {
    // And this too!
    }
    @Override public void displayErrorState() {
    // Please, do not mess with your user
    }
    }

    View Slide

  18. public class Presenter {
    public void bindView(ViewDelegate delegate) {
    this.delegate = delegate;
    }
    public void fetchData() {
    source.fetchData(new DataSource.Callback() {
    @Override public void onDataLoaded(DataModel model) {
    delegate.displayResults(model);
    }
    @Override public void onError(Throwable t) {
    if (t instanceof NetworkingError) {
    delegate.networkingError();
    } else if (t instanceof NoDataAvailable) {

    }
    }
    });
    }
    }

    View Slide

  19. DATASOURCE
    REST GATEWAY
    PRESENTER
    VIEW DELEGATION CALLBACKS
    PLATAFORM CONTROLLER
    CALLBACK
    UNIT TESTS
    (Mocked Contract)
    FUNCTIONAL UI TESTS
    INTEGRATION TESTS
    INTEGRATION TESTS
    (DOUBLES)
    UNIT TESTS
    (Mocked Source
    +
    Mocked View)
    DATAMODEL

    View Slide

  20. String description = “Blah”
    String date = “2010-02-26T19:35:24Z”
    int step = 2
    String description = “Blah”
    LocalDateTime dateTime = (JSR310)
    TrackingStep currentStep = (enum)
    String description = “Blah”
    String formattedDate = “26/02/2010”
    String currentStep = “Concluído”
    Response
    Model
    Domain
    Model
    View
    Model
    DATA
    MODEL

    View Slide

  21. PROBLEMAS EM POTENCIAL
    Qual representação de dados utilizar? Unificada ou separada?
    Onde aplicar parsing? E formatação para a UI?
    Callbacks aninhados
    Memory leaks no nível do mecanismo de entrega
    Etc

    View Slide

  22. BRACE
    YOURSELVES
    RX
    IS COMING

    View Slide

  23. COMO
    ADICIONAR RX
    NESSA
    ARQUITETURA ??

    View Slide

  24. PRESENTATION
    LAYER
    DATA
    LAYER
    DB
    REST
    ETC
    UI
    . . .
    Callback(T)
    Callback(T) Callback(T)

    View Slide

  25. SUBSTITUIR
    CALLBACKS POR
    SEQUÊNCIAS
    OBSERVÁVEIS

    View Slide

  26. PRIMEIRA INTERAÇÃO
    CAMADA DE DADOS
    REATIVA

    View Slide

  27. REST GATEWAY
    VIEW DELEGATION
    VIEW
    DATA SOURCE
    Flowable
    PRESENTER
    Callback(T)
    FlowableSubscriber Disposable

    View Slide

  28. public interface EventsSource {
    Flowable fetchWith(MessageToFetchParameters params);
    Flowable sendMessage(MessageToSendParameters params);
    }
    ADEUS
    CALLBACKS !!!

    View Slide

  29. public class MessagesInfrastructure implements EventsSource {
    @Override public Flowable fetchWith(MessageToFetch params) {
    return restAPI.getMessages(params)
    .subscribeOn(Schedulers.io())
    .map(PayloadMapper::map)
    .flatMap(Flowable::fromIterable);
    }
    @Override public Flowable sendMessage(MessageToSend params) {
    SendMessageToBody body = SendMessageToBody.convert(params);
    return restAPI.sendMessage(body)
    .subscribeOn(Schedulers.io())
    .flatMap(emptyBody -> fetchWith(sameFrom(params)));
    }
    }
    Chained request, easy !!!!

    View Slide

  30. VANTAGENS OBSERVADAS
    Facilidades via frameworks utilitários para REST / DB
    Validação de dados de entrada e tradução de modelos
    como etapas do pipeline
    Tratamento de erros, auto retry, exponential backoff no
    “baixo nível”

    View Slide

  31. PROBLEMAS OBSERVADOS
    Consumir os dados no nível da apresentação nos força a
    rodar comportamentos na thread principal do app
    (orquestração dos callbacks)
    Indireção forçada para prover Scheduler via DI, para
    propósitos de testes
    Muitas responsabilidades no Presenter

    View Slide

  32. SEGUNDA INTERAÇÃO
    CAMADA DE
    APRESENTAÇÃO REATIVA

    View Slide

  33. View Slide

  34. REST GATEWAY
    VIEW DELEGATION
    VIEW
    DATA SOURCE
    Flowable
    PRESENTER
    FlowableSubscriber
    Flowable
    Disposable

    View Slide

  35. public interface SomeView {
    Function, Disposable> results();
    Function, Disposable> showEmptyState();
    Function, Disposable> hideEmptyState();
    Function, Disposable> showLoading();
    Function, Disposable> hideLoading();
    // More delegation
    }

    View Slide

  36. public static Disposable bind(Flowable flow,
    Function, Disposable> uiFunc) {
    return uiFunc.call(flow);
    }
    public static Function, Disposable> uiFunction(Consumer uiAction) {
    return uiFunction(uiAction, () -> {});
    }
    public static Function, Disposable> uiFunction(Consumer uiAction,
    Action done) {
    try {
    return flowable ->
    flowable
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    uiAction,
    throwable -> Logger.e(throwable.getMessage()),
    done
    );
    } catch (Exception e) { throw new RuntimeException();}
    }

    View Slide

  37. public static Disposable bind(Flowable flow,
    Function, Disposable> uiFunc) {
    return uiFunc.call(flow);
    }
    public static Function, Disposable> uiFunction(Consumer uiAction) {
    return uiFunction(uiAction, () -> {});
    }
    public static Function, Disposable> uiFunction(Consumer uiAction,
    Action done) {
    try {
    return flowable ->
    flowable
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    uiAction,
    throwable -> Logger.e(throwable.getMessage()),
    done
    );
    } catch (Exception e) { throw new RuntimeException();}
    }

    View Slide

  38. public static Disposable bind(Flowable flow,
    Function, Disposable> uiFunc) {
    return uiFunc.call(flow);
    }
    public static Function, Disposable> uiFunction(Consumer uiAction) {
    return uiFunction(uiAction, () -> {});
    }
    public static Function, Disposable> uiFunction(ConsumeruiAction,
    Action done) {
    try {
    return flowable ->
    flowable
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    uiAction,
    throwable -> Logger.e(throwable.getMessage()),
    done
    );
    } catch (Exception e) { throw new RuntimeException();}
    }

    View Slide

  39. public class MessagingActivity extends BaseActivity
    implements MessagesStreamView {
    @Override public Function, Disposable> restoreNotSentMessage() {
    return uiFunction(message -> {
    Toast.makeText(this, "Erro ao enviar mensagem", LENGTH_SHORT).show();
    messageInput.setText(message);
    });
    }
    @Override public Function, Disposable> enableSomeOption() {
    return uiFunction(action -> optionView.setVisibility(VISIBLE));
    }
    @Override public Function, Disposable> disableSomeOption() {
    return uiFunction(action -> optionView.setVisibility(GONE));
    }
    @Override public Function, Disposable> showEmptyState() {
    return uiFunction(action -> emptyStateContainer.setVisibility(VISIBLE));
    }
    // More delegate methods

    View Slide

  40. public class MessagingActivity extends BaseActivity
    implements MessagesStreamView {
    @Override public Function, Disposable> restoreNotSentMessage() {
    return uiFunction(message -> {
    Toast.makeText(this, "Erro ao enviar mensagem", LENGTH_SHORT).show();
    messageInput.setText(message);
    });
    }
    @Override public Function, Disposable> enableComplaintOption() {
    return uiFunction(action -> optionView.setVisibility(VISIBLE));
    }
    @Override public Function, Disposable> disableComplaintOption() {
    return uiFunction(action -> optionView.setVisibility(GONE));
    }
    @Override public Function, Disposable> showEmptyState() {
    return uiFunction(action -> emptyStateContainer.setVisibility(VISIBLE));
    }
    // More delegate methods

    View Slide

  41. public class ReactivePresenter {
    private final CompositeDisposable disposable = new CompositeDisposable();
    private V view;
    public void bind(V view) { this.view = view; }
    public void unbind() {
    disposable.clear();
    this.view = null;
    }
    public CompositeDisposable subscriptions() {
    return disposable;
    }
    protected V view() { return view; }
    protected boolean isBinded() { return view != null; }
    }

    View Slide

  42. public class ReactivePresenter {
    private final CompositeDisposable disposable = new CompositeDisposable();
    private V view;
    public void bind(V view) { this.view = view; }
    public void unbind() {
    disposable.dispose();
    this.view = null;
    }
    public CompositeDisposable subscriptions() {
    return disposable;
    }
    protected V view() { return view; }
    protected boolean isBinded() { return view != null; }
    }

    View Slide

  43. public void userRequiredMediation(String userId, String messageText) {
    MessageToSendParameters parameters = new MessageToSendParameters.Builder()
    .userId(userId)
    // …
    .messageText(messageText)
    .build();
    executionPipeline(parameters);
    }
    private void executionPipeline(MessageToSend parameters) {
    Flowable execution =
    source.sendMessage(parameters)
    .doOnSubscribe(this::prepareToLoad)
    .map(ViewModelMappers::map)
    .flatMap(Flowable::fromIterable)
    .doOnCompleted(this::finishLoadingMessages);
    subscriptions().add(bind(execution, view().onMessagesLoaded()));
    }

    View Slide

  44. public void userRequiredMediation(String userId, String messageText) {
    MessageToSendParameters parameters = new MessageToSendParameters.Builder()
    .userId(userId)
    // …
    .messageText(messageText)
    .build();
    executionPipeline(parameters);
    }
    private void executionPipeline(MessageToSend parameters) {
    Flowable execution =
    source.sendMessage(parameters)
    .doOnSubscribe(this::prepareToSend)
    .map(ViewModelMappers::map)
    .flatMap(Flowable::fromIterable)
    .doOnCompleted(this::finishSendMessage);
    subscriptions().add(bind(execution, view().onMessagesLoaded()));
    }

    View Slide

  45. VANTAGENS OBSERVADAS
    Presenter não precisa mais da noção de threading
    Presenter passar a orquestrar a UI através de um pipeline de
    execução bem definido
    Condições relacionadas aos dados no ciclo de vida do fluxo
    podem ser disparada a partir do pipeline
    Tradução de ViewModels é uma etapa do pipeline

    View Slide

  46. PROBLEMAS OBSERVADOS
    1) Protocolo View ainda gordo
    2) “Repetição” de código entre Presenters, normalmente
    relacionada a comportamentos de UI similares que
    acompanhando o ciclo de vida da sequências
    - Mostrar empty state se não houver dados
    - Mostrar loading ao iniciar operação; esconder ao terminar
    - Etc
    3) Testes ruins de serem lidos

    View Slide

  47. @Test public void shouldNotDisplayResults_WhenEmptyData() {
    presenter.bind(view);
    // When source has no data to return
    when(source.getResults()).thenReturn(Flowable.empty());
    // and presenter requires data
    presenter.fetchResults();
    // we should not display any data into View
    verify(view.resultsDeliveredAction, never()).call(Flowable.just(any());
    }

    View Slide

  48. public class MockView implements SomeView {
    @Mock public Action resultsDeliveredAction;
    public MockFAQView() {
    MockitoAnnotations.initMocks(this);
    }
    @Override public Function, Disposable> onResults() {
    return flowable -> flowable.subscribe(resultsDeliveredAction);
    }
    ...

    View Slide

  49. public class MockView implements SomeView {
    @Mock public Action resultsDeliveredAction;
    public MockFAQView() {
    MockitoAnnotations.initMocks(this);
    }
    @Override public Function, Disposable> onResults() {
    return flowable -> flowable.subscribe(resultsDeliveredAction);
    }
    ...

    View Slide

  50. TERCEIRA INTERAÇÃO
    REACTIVE VIEW
    SEGREGATION

    View Slide

  51. public interface SomeView {
    Func1, Disposable> results();
    Function, Disposable> showEmptyState();
    Function, Disposable> hideEmptyState();
    Function, Disposable> showLoading();
    Function, Disposable> hideLoading();
    Function, Disposable> networkError();
    Function, Disposable> networkUnavailable();
    Function, Disposable> networkSlow();
    }

    View Slide

  52. UI BEHAVIOR
    VIEW PROTOCOL
    UI BEHAVIOR UI BEHAVIOR
    UI BEHAVIOR . . .

    View Slide

  53. public interface EmptyStateView {
    Function, Disposable> showEmptyState();
    Function, Disposable> hideEmptyState();
    }
    public interface LoadingView {
    Function, Disposable> showLoading();
    Function, Disposable> hideLoading();
    }

    View Slide

  54. public interface SomeView
    extends LoadingView, EmptyStateView, NetworkingReporterView {
    Function, Disposable> displayResults();
    }
    public interface NetworkingReporterView {
    Function, Disposable> networkError();
    Function, Disposable> networkUnavailable();
    Function, Disposable> networkSlow();
    }

    View Slide

  55. - Cada comportamento poderia ter
    o seu “mini-presenter” associado, e
    o Presenter “grande” faria a
    orquestração dos colaboradores
    - Melhor estratégia : fazer a
    composição ser uma etapa do
    pipeline !!!

    View Slide

  56. f(g(x))

    View Slide

  57. public class LoadingWhenProcessing implements FlowableTransformer {
    private PublishSubject show, hide = PublishSubject.create();
    public Dispsoable bind(LoadingView view) {
    CompositeDisposable composite = new CompositeDisposable();
    composite.add(bind(show, view.showLoading()));
    composite.add(bind(hide, view.hideLoading()));
    return composite;
    }
    @Override public Flowable call(Flowable upstream) {
    return upstream
    .doOnSubscribe(this::showLoading)
    .doOnTerminate(this::hideLoading);
    }
    private void hideLoading() {
    hide.onNext(Unit.instance());
    }
    private void showLoading() {
    show.onNext(Unit.instance());
    }
    }

    View Slide

  58. public class SomePresenter
    extends ReactivePresenter {
    // Hook all behaviors for view
    [ ... ]
    public void executeOperation() {
    bind(executionPipeline(), view().results());
    }
    private Flowable executionPipeline() {
    return source.search()
    .compose(networkErrorFeedback)
    .compose(loadingWhenProcessing)
    .compose(coordinateRefresh)
    .compose(emptyStateWhenMissingData)
    .compose(errorWhenProblems)
    .map(DataViewModelMapper::map);
    }
    }

    View Slide

  59. VANTAGENS
    Cada evento delegado para a UI agora é unit-testable de uma
    mais fácil !!!
    Presenters apenas orquestram a UI (como prega MVP)
    Transformers são facilmente reutilizáveis

    View Slide

  60. PROBLEMAS ENCONTRADOS (I)
    1) Boilerplate para o binding de comportamentos
    @Override public void bind(SomeView view) {
    super.bind(view);
    subscription().add(loadingWhileProcessing.bind(view));
    subscription().add(networkErrorFeedback.bind(view));
    subscription().add(coordinateRefresh.bind(view));
    subscription().add(emptyStateWhenMissingData.bind(view));
    subscription().add(errorStateWhenProblem.bind(view));
    }

    View Slide

  61. PROBLEMAS ENCONTRADOS (II)
    2) Comportamentos injetados via DI no Presenter; possível confusão ao
    fazer pull das dependências
    3) Cooperação entre comportamentos, como fazer?
    4) Comando para ação na View sinalizado via emissão de item
    5) Testes de transformers são mais isolados, mas não necessariamente
    mais legíveis!

    View Slide

  62. @Test public void shouldTransformView_WhenErrorAtStream() {
    loadingWhileFetching.bindLoadingContent(view);
    // When stream will propagate an error
    Flowable stream = Flowable.error(new RuntimeCryptoException());
    // and we add this transformation to pipeline
    stream.compose(loadingWhileFetching)
    .subscribe(
    s -> {},
    throwable -> {},
    () -> {}
    );
    // we still should interact with loading actions
    verify(view.showLoadingAction).call(Flowable.just(any());
    verify(view.hideLoadingAction).call(Flowable.just(any());
    }

    View Slide

  63. View Slide

  64. QUARTA INTERAÇÃO
    SIMPLIFICAR PARA
    ESCALAR

    View Slide

  65. PRINCÍPIO EM SOFTWARE
    “Camadas mais internas
    escondem complexidade das
    camadas mais externas”

    View Slide

  66. REMODELANDO AS APIs
    Queremos manter comportamentos segregados como etapas do
    pipeline via transformadores
    Queremos adicionar mais comportamentos que estão associados à
    condições dos dados de forma transparente
    Queremos diminuir a fricção para implementação dessa abordagem em
    novos fluxos
    Queremos facilitar escrita e entendimento de testes
    Queremos fornecer 100% dos objetos via DI (incluindo a própria View)

    View Slide

  67. REPENSANDO VIEW PASSIVA
    public interface EmptyStateView {
    Function, Disposable> showEmptyState();
    Function, Disposable> hideEmptyState();
    }

    View Slide

  68. REPENSANDO VIEW PASSIVA
    public interface EmptyStateView {
    Function, Disposable> showEmptyState();
    Function, Disposable> hideEmptyState();
    }
    public interface EmptyStateView {
    Action showEmptyState();
    Action hideEmptyState();
    }

    View Slide

  69. IMPL. VIEW PASSIVA (ANTES)
    @Override public Func1, Subscription> showEmptyState() {
    return RxUi.uiFunction(unit -> emptyState.setVisibility(View.VISIBLE));
    }
    @Override public Func1, Subscription> hideEmptyState() {
    return RxUi.uiFunction(unit -> emptyState.setVisibility(View.GONE));
    }
    @Override public Func1, Subscription> showLoading() {
    return RxUi.uiFunction(unit -> progress.setVisibility(View.VISIBLE));
    }
    @Override public Func1, Subscription> hideLoading() {
    return RxUi.uiFunction(unit -> progress.setVisibility(View.GONE));
    }
    // More delegation

    View Slide

  70. IMPL. VIEW PASSIVA (DEPOIS)
    @Override public Action showLoading() {
    return () -> loading.setVisibility(View.VISIBLE);
    }
    @Override public Action hideLoading() {
    return () -> loading.setVisibility(View.GONE);
    }
    @Override public Action showEmptyState() {
    return () -> emptyState.setVisibility(View.VISIBLE);
    }
    @Override public Action hideEmptyState() {
    return () -> emptyState.setVisibility(View.GONE);
    }
    // More Delegation

    View Slide

  71. ENTREGA DE DADOS (ANTES)
    public interface SomeView
    extends EmptyStateView, LoadingView, NetworkErrorReporterView {
    Function, Disposable> onResults();
    }
    // At view implementation
    @Override public Function, Disposable> onResults() {
    return RxUi.uiFunction(
    model -> adapter.add(model),
    this::displayResults
    );
    }

    View Slide

  72. ENTREGA DE DADOS (DEPOIS)
    public interface DisplayFactsView
    extends LoadingView, ErrorStateView, EmptyStateView {
    Disposable subscribeInto(Flowable flow);
    }
    @Override public Disposable subscribeInto(Flowable flow) {
    return flow
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    model -> addToAdapter(model),
    throwable -> Logger.e(throwable.getMessage()),
    () -> displayResults()
    );
    }

    View Slide

  73. ENTREGA DE DADOS (DEPOIS)
    public interface DisplayFactsView
    extends LoadingView, ErrorStateView, EmptyStateView {
    Disposable subscribeInto(Flowable flow);
    }
    @Override public Disposable subscribeInto(Flowable flow) {
    return flow
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    model -> addToAdapter(model),
    throwable -> Logger.e(throwable.getMessage()),
    () -> displayResults()
    );
    }

    View Slide

  74. public ComplexPresenter(
    DataSource source,
    NetworkErrorFeedback networkErrorFeedback,
    LoadingWhileFetching loadingWhileFetching,
    CoordinateRefreshWhenLoadingContent coordinateRefresh,
    ShowEmptyStateWhenMissingData emptyStateWhenMissingData,
    ShowErrorState errorState) {
    this.source = source;
    this.networkErrorFeedback = networkErrorFeedback;
    this.loadingWhileFetching = loadingWhileFetching;
    this.coordinateRefresh = coordinateRefresh;
    this.emptyStateWhenMissingData = emptyStateWhenMissingData;
    this.errorState = errorState;
    }
    @Override public void bind(ComplexView view) {
    super.bind(view);
    subscriptions().add(loadingWhileFetching.bindLoadingContent(view));
    subscriptions().add(networkErrorFeedback.bindNetworkingReporter(view));
    subscriptions().add(coordinateRefresh.bindRefreshableView(view));
    subscriptions().add(emptyStateWhenMissingData.bindEmptyStateView(view));
    subscriptions().add(errorState.bindErrorStateView(view));
    }
    ANTES …

    View Slide

  75. public class FactsPresenter {
    private GetRandomFacts usecase;
    private DisplayFactsView view;
    private BehavioursCoordinator coordinator;
    private ViewModelMapper mapper;
    public FactsPresenter(GetRandomFacts usecase,
    DisplayFactsView view,
    BehavioursCoordinator coordinator,
    ViewModelMapper mapper) {
    this.usecase = usecase;
    this.view = view;
    this.coordinator = coordinator;
    this.mapper = mapper;
    }
    DEPOIS!!!

    View Slide

  76. public class FactsPresenter {
    private GetRandomFacts usecase;
    private DisplayFactsView view;
    private BehavioursCoordinator coordinator;
    private ViewModelMapper mapper;
    public FactsPresenter(GetRandomFacts usecase,
    DisplayFactsView view,
    BehavioursCoordinator coordinator,
    ViewModelMapper mapper) {
    this.usecase = usecase;
    this.view = view;
    this.coordinator = coordinator;
    this.mapper = mapper;
    }

    View Slide

  77. public class BehavioursCoordinator implements FlowableTransformer {
    private AssignEmptyState dealWithEmptyState;
    private AssignErrorState assignErrorState;
    private LoadingCoordination loadingCoordinator;
    // More transfomers
    public BehavioursCoordinator(AssignEmptyState dealWithEmptyState,
    AssignErrorState assignErrorState,
    // More transfomers
    LoadingCoordination loadingCoordinator) {
    this.dealWithEmptyState = dealWithEmptyState;
    this.assignErrorState = assignErrorState;
    this.loadingCoordinator = loadingCoordinator;
    // More transfomers
    }
    @Override public Flowable apply(Flowable upstream) {
    return upstream
    .compose(dealWithEmptyState)
    .compose(assignErrorState)
    // compose all transformers
    .compose(loadingCoordinator);
    }
    }

    View Slide

  78. public class BehavioursCoordinator implements FlowableTransformer {
    private AssignEmptyState dealWithEmptyState;
    private AssignErrorState assignErrorState;
    private LoadingCoordination loadingCoordinator;
    // More transfomers
    public BehavioursCoordinator(AssignEmptyState dealWithEmptyState,
    AssignErrorState assignErrorState,
    // More transfomers
    LoadingCoordination loadingCoordinator) {
    this.dealWithEmptyState = dealWithEmptyState;
    this.assignErrorState = assignErrorState;
    this.loadingCoordinator = loadingCoordinator;
    // More transfomers
    }
    @Override public Flowable apply(Flowable upstream) {
    return upstream
    .compose(dealWithEmptyState)
    .compose(assignErrorState)
    // compose all transformers
    .compose(loadingCoordinator);
    }
    }

    View Slide

  79. public class HideAtStartShowAtError implements FlowableTransformer {
    private Action whenStart;
    private Action atError;
    private ErrorPredicate errorPredicate;
    private Scheduler targetScheduler;
    // Constructor
    @Override public Publisher apply(Flowable upstream) {
    return upstream
    .doOnSubscribe(subscription -> hide())
    .doOnError(this::evaluateAndShowIfApplicable);
    }
    private void evaluateAndShowIfApplicable(Throwable throwable) {
    if (errorPredicate.evaluate(throwable)) subscribeAndFireAction(atError);
    }
    private void hide() { subscribeAndFireAction(whenStart);}
    private void subscribeAndFireAction(Action toPerform) {
    Completable.fromAction(toPerform).subscribeOn(targetScheduler).subscribe();
    }
    }

    View Slide

  80. public class HideAtStartShowAtError implements FlowableTransformer {
    private Action whenStart;
    private Action atError;
    private ErrorPredicate errorPredicate;
    private Scheduler targetScheduler;
    // Constructor
    @Override public Publisher apply(Flowable upstream) {
    return upstream
    .doOnSubscribe(subscription -> hide())
    .doOnError(this::evaluateAndShowIfApplicable);
    }
    private void evaluateAndShowIfApplicable(Throwable throwable) {
    if (errorPredicate.evaluate(throwable)) subscribeAndFireAction(atError);
    }
    private void hide() { subscribeAndFireAction(whenStart);}
    private void subscribeAndFireAction(Action toPerform) {
    Completable.fromAction(toPerform).subscribeOn(targetScheduler).subscribe();
    }
    }

    View Slide

  81. public class AssignEmptyState implements FlowableTransformer {
    EmptyStateView view;
    Scheduler uiScheduler;
    public AssignEmptyState(EmptyStateView view, Scheduler uiScheduler) {
    this.view = view;
    this.uiScheduler = uiScheduler;
    }
    @Override public Publisher apply(Flowable upstream) {
    HideAtStartShowAtError delegate = new HideAtStartShowAtError<>(
    view.hideEmptyState(),
    view.showEmptyState(),
    error -> error instanceof ContentNotFoundError,
    uiScheduler
    );
    return upstream.compose(delegate);
    }
    }

    View Slide

  82. public class AssignEmptyState implements FlowableTransformer {
    EmptyStateView view;
    Scheduler uiScheduler;
    public AssignEmptyState(EmptyStateView view, Scheduler uiScheduler) {
    this.view = view;
    this.uiScheduler = uiScheduler;
    }
    @Override public Publisher apply(Flowable upstream) {
    HideAtStartShowAtError delegate = new HideAtStartShowAtError<>(
    view.hideEmptyState(),
    view.showEmptyState(),
    error -> error instanceof ContentNotFoundError,
    uiScheduler
    );
    return upstream.compose(delegate);
    }
    }

    View Slide

  83. public void fetchData() {
    if (isBinded()) {
    RxUi.bind(executionPipeline(), view().results());
    }
    }
    private Flowable executionPipeline() {
    return source.fetch()
    .compose(networkErrorFeedback)
    .compose(loadingWhileFetching)
    .compose(coordinateRefresh)
    .compose(emptyStateWhenMissingData)
    .compose(showErroState)
    .map(ViewModelMapper::map)
    .flatMap(Flowable::fromIterable);
    }
    ANTES …

    View Slide

  84. NOVO PIPELINE
    public void fetchRandomFacts() {
    Flowable dataFlow =
    usecase.fetchTrivia()
    .compose(coordinator)
    .map(fact -> mapper.translate(fact));
    Disposable toDispose = view.subscribeInto(dataFlow);
    // TODO : find a better way to handle this
    Disposable disposable = view.subscribeInto(dataFlow);
    }

    View Slide

  85. TESTES LIMPOS (I)
    @Test public void shouldNotAssignError_WhenFlowEmmits() throws Exception {
    Flowable.just("A", "B")
    .compose(assignErrorState)
    .subscribe();
    verify(hide, oneTimeOnly()).run();
    verify(show, never()).run();
    }
    @Test public void shouldNotAssignError_WithEmptyFlow() throws Exception {
    Flowable empty = Flowable.empty();
    empty.compose(assignErrorState).subscribe();
    verify(hide, oneTimeOnly()).run();
    verify(show, never()).run();
    }

    View Slide

  86. TESTES LIMPOS (II)
    @Test public void shouldPresent_NoContentError_IntoView()
    throws Exception {
    Flowable noContent =
    Flowable.error(new ContentNotFoundError());
    when(usecase.fetchTrivia()).thenReturn(noContent);
    presenter.fetchRandomFacts();
    BehavioursVerifier.with(view)
    .showLoadingFirstHideLoadingAfter()
    .shouldShowEmptyState()
    .shouldNotShowErrorState();
    }

    View Slide

  87. TESTES LIMPOS (III)
    @Test public void shouldPresent_NoContentError_IntoView()
    throws Exception {
    Flowable noContent =
    Flowable.error(new ContentNotFoundError());
    when(usecase.fetchTrivia()).thenReturn(noContent);
    presenter.fetchRandomFacts();
    BehavioursVerifier.with(view)
    .showLoadingFirstHideLoadingAfter()
    .shouldShowEmptyState()
    .shouldNotShowErrorState();
    }

    View Slide

  88. public class BehavioursVerifier {
    private Object target; // Via factory method
    public BehavioursVerifier shouldShowErrorState() throws Exception {
    checkErrorStateView();
    ErrorStateView view = (ErrorStateView) target;
    verify(view.showErrorState(), oneTimeOnly()).run();
    return this;
    }
    // For each View and each behavior, check if bind / apply is possible
    private void checkEmptyStateView() {
    if (!(target instanceof EmptyStateView))
    throw new IllegalArgumentException("Not instance of EmptyStateView");
    }

    View Slide

  89. public class BehavioursVerifier {
    private Object target;
    public BehavioursVerifier shouldShowErrorState() throws Exception {
    checkErrorStateView();
    ErrorStateView view = (ErrorStateView) target;
    verify(view.showErrorState(), oneTimeOnly()).run();
    return this;
    }
    // For each View and each behavior, check if bind / apply is possible
    private void checkEmptyStateView() {
    if (!(target instanceof EmptyStateView))
    throw new IllegalArgumentException("Not instance of EmptyStateView");
    }

    View Slide

  90. public class BehavioursVerifier {
    private Object target;
    public BehavioursVerifier shouldShowErrorState() throws Exception {
    checkErrorStateView();
    ErrorStateView view = (ErrorStateView) target;
    verify(view.showErrorState(), oneTimeOnly()).run();
    return this;
    }
    // For each View and each behavior, check if bind / apply is possible
    private void checkEmptyStateView() {
    if (!(target instanceof EmptyStateView))
    throw new IllegalArgumentException("Not instance of EmptyStateView");
    }

    View Slide

  91. VANTAGENS
    APIs mais próximas à View agora são muito mais simples
    Testes mais simples e legíveis, muito próximos ao MVP sem Rx
    Menos boilerplate via Coordinator para ações na View que são
    relacionadas à condições de fluxo
    Coordinator pode ter quantos comportamentos se deseja
    independente à qual View for associado

    View Slide

  92. FINAL LAP

    View Slide

  93. Como eliminar as APIs pública
    e privada do Presenter para
    hooks de lifecycle de Activity
    ou Fragment ???

    View Slide

  94. public class ReactivePresenter {
    private final CompositeDisposable disposable = new CompositeDisposable();
    private V view;
    public void bind(V view) { this.view = view; }
    public void unbind() {
    disposable.dispose();
    this.view = null;
    }
    public CompositeDisposable subscriptions() {
    return disposable;
    }
    protected V view() { return view; }
    protected boolean isBinded() { return view != null; }
    }
    ????????

    View Slide

  95. Fazer com que a
    liberação de Disposable
    seja responsabilidade
    de algum colaborador
    que conheça o ciclo de
    vida do mecanismo de
    entrega

    View Slide

  96. View Slide

  97. VIEW PROTOCOL
    VIEW IMPL.
    PRESENTER
    FlowableSubscriber
    Flowable
    LifecycleStrategist
    Disposable DisposeStrategy
    LifecycleObserver
    LifecycleOwner
    Flowable
    (eg. Activity)

    View Slide

  98. public class DisposeStrategy implements LifecycleObserver {
    private CompositeDisposable composite = new CompositeDisposable();
    void addDisposable(Disposable toDispose) {
    composite.add(toDispose);
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) public void release() {
    composite.dispose();
    }
    }

    View Slide

  99. public class LifecycleStrategist {
    private DisposeStrategy strategy;
    public LifecycleStrategist(LifecycleOwner owner,
    DisposeStrategy strategy) {
    this.strategy = strategy;
    owner.getLifecycle().addObserver(strategy);
    }
    public void applyStrategy(Disposable toDispose) {
    strategy.addDisposable(toDispose);
    }
    }

    View Slide

  100. public class LifecycleStrategist {
    private DisposeStrategy strategy;
    public LifecycleStrategist(LifecycleOwner owner,
    DisposeStrategy strategy) {
    this.strategy = strategy;
    owner.getLifecycle().addObserver(strategy);
    }
    public void applyStrategy(Disposable toDispose) {
    strategy.addDisposable(toDispose);
    }
    }

    View Slide

  101. public class LifecycleStrategist {
    private DisposeStrategy strategy;
    public LifecycleStrategist(LifecycleOwner owner,
    DisposeStrategy strategy) {
    this.strategy = strategy;
    owner.getLifecycle().addObserver(strategy);
    }
    public void applyStrategy(Disposable toDispose) {
    strategy.addDisposable(toDispose);
    }
    }

    View Slide

  102. public class FactsPresenter {
    private GetRandomFacts usecase;
    private DisplayFactsView view;
    private BehavioursCoordinator coordinator;
    private ViewModelMapper mapper;
    public FactsPresenter(GetRandomFacts usecase,
    DisplayFactsView view,
    BehavioursCoordinator coordinator,
    LifecycleStrategist strategist,
    ViewModelMapper mapper) {
    this.usecase = usecase;
    this.view = view;
    this.coordinator = coordinator;
    this.strategist = strategist;
    this.mapper = mapper;
    }

    View Slide

  103. public void fetchRandomFacts() {
    Flowable dataFlow = coordinator
    .coordinateFlow(usecase.fetchTrivia())
    .map(fact -> mapper.translateFrom(fact));
    Disposable toDispose = view.subscribeInto(dataFlow);
    strategist.applyStrategy(toDispose)
    }

    View Slide

  104. VANTAGENS OBSERVADAS
    Presenter não precisa mais de API pública por motivos de ciclo
    de vida do mecanismo de entrega
    Fluxo pode ser construído 100% via DI de forma
    componetizada

    View Slide

  105. CONCLUSÕES

    View Slide

  106. LIÇÕES APRENDIDAS
    Escolher um modelo de arquitetura não é uma tarefa trivial
    Evoluir um modelo para obter vantagens de um paradigma
    (FRP) é ainda menos trivial
    Nem tudo são flores e não tenha medo de errar; adote
    iterações na evolução da arquitetura junto com seu time
    Novos problemas sempre aparecerão com as novas soluções

    View Slide

  107. TALK IS CHEAP

    View Slide

  108. https://github.com/ubiratansoares/reactive-architectures-playground
    Sample usando numbersapi.com
    100% RxJava2 + MVP, ilustrando conceitos vistos aqui
    Dagger 2.11, Full Android Support APIs
    Testes de unidade que importam
    Mais outras coisinhas (WIP)

    View Slide

  109. https://speakerdeck.com/ubiratansoares/evoluindo-arquiteturas-reativas

    View Slide

  110. UBIRATAN
    SOARES
    Computer Scientist by ICMC/USP
    Software Engineer, curious guy
    Google Developer Expert for Android
    Teacher, speaker, etc, etc

    View Slide

  111. OBRIGADO
    @ubiratanfsoares
    ubiratansoares.github.io
    https://br.linkedin.com/in/ubiratanfsoares

    View Slide