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

April 26, 2017
Tweet

More Decks by Ubiratan Soares

Other Decks in Programming

Transcript

  1. EVOLUINDO
    ARQUITETURAS
    REATIVAS
    Ubiratan Soares
    Julho / 2017

    View full-size slide

  2. O QUE É UMA
    ARQUITETURA EM
    MOBILE ?

    View full-size slide

  3. MVP
    MVVM
    VIPER
    FLUX REDUX
    CLEAN
    MVC

    MVI

    View full-size slide

  4. 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 full-size slide

  5. UMA QUEIXA COMUM
    NA COMUNIDADE
    MOBILE ?

    View full-size slide

  6. TEM PELO MENOS UM UNIT TEST NO APP?

    View full-size slide

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

    View full-size slide

  8. QUAL ARQUITETURA
    ESCOLHER ENTÃO ???

    View full-size slide

  9. NÃO HÁ
    SILVER
    BULLETS!

    View full-size slide

  10. ESTUDO DE CASO
    MVP / CLEAN

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  13. 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 full-size slide

  14. 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 full-size slide

  15. 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 full-size slide

  16. 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 full-size slide

  17. 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 full-size slide

  18. BRACE
    YOURSELVES
    RX
    IS COMING

    View full-size slide

  19. COMO
    ADICIONAR RX
    NESSA
    ARQUITETURA ??

    View full-size slide

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

    View full-size slide

  21. SUBSTITUIR
    CALLBACKS POR
    SEQUÊNCIAS
    OBSERVÁVEIS

    View full-size slide

  22. PRIMEIRA INTERAÇÃO
    CAMADA DE DADOS
    REATIVA

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  25. 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 full-size slide

  26. 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 full-size slide

  27. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  31. 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 full-size slide

  32. 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 full-size slide

  33. 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 full-size slide

  34. 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 full-size slide

  35. 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 full-size slide

  36. 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 full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

  42. @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 full-size slide

  43. 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 full-size slide

  44. 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 full-size slide

  45. TERCEIRA INTERAÇÃO
    REACTIVE VIEW
    SEGREGATION

    View full-size slide

  46. 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 full-size slide

  47. UI BEHAVIOR
    VIEW PROTOCOL
    UI BEHAVIOR UI BEHAVIOR
    UI BEHAVIOR . . .

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  50. - 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 full-size slide

  51. 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 full-size slide

  52. 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 full-size slide

  53. 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 full-size slide

  54. 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 full-size slide

  55. 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 full-size slide

  56. @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 full-size slide

  57. QUARTA INTERAÇÃO
    SIMPLIFICAR PARA
    ESCALAR

    View full-size slide

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

    View full-size slide

  59. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  62. 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 full-size slide

  63. 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 full-size slide

  64. 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 full-size slide

  65. 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 full-size slide

  66. 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 full-size slide

  67. 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 full-size slide

  68. 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 full-size slide

  69. 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 full-size slide

  70. 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 full-size slide

  71. 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 full-size slide

  72. 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 full-size slide

  73. 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 full-size slide

  74. 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 full-size slide

  75. 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 full-size slide

  76. 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 full-size slide

  77. 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 full-size slide

  78. 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 full-size slide

  79. 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 full-size slide

  80. 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 full-size slide

  81. 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 full-size slide

  82. 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 full-size slide

  83. 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 full-size slide

  84. 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 full-size slide

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

    View full-size slide

  86. 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 full-size slide

  87. 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 full-size slide

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

    View full-size slide

  89. 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 full-size slide

  90. 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 full-size slide

  91. 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 full-size slide

  92. 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 full-size slide

  93. 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 full-size slide

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

    View full-size slide

  95. 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 full-size slide

  96. 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 full-size slide

  97. TALK IS CHEAP

    View full-size slide

  98. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide