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)

D4b7a3e2ed10f86e0b52498713ba2601?s=128

Ubiratan Soares

April 26, 2017
Tweet

Transcript

  1. EVOLUINDO ARQUITETURAS REATIVAS Ubiratan Soares Julho / 2017

  2. O QUE É UMA ARQUITETURA EM MOBILE ?

  3. MVP MVVM VIPER FLUX REDUX CLEAN MVC … MVI

  4. None
  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
  6. UMA QUEIXA COMUM NA COMUNIDADE MOBILE ?

  7. TEM PELO MENOS UM UNIT TEST NO APP?

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

  9. QUAL ARQUITETURA ESCOLHER ENTÃO ???

  10. None
  11. None
  12. None
  13. NÃO HÁ SILVER BULLETS!

  14. ESTUDO DE CASO MVP / CLEAN

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

    .
  16. public interface ViewDelegate { void displayResults(DataModel model); void networkingError(); void

    displayEmptyState(); void displayErrorState(); // More delegation }
  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 } }
  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) { … } } }); } }
  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
  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
  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
  22. BRACE YOURSELVES RX IS COMING

  23. COMO ADICIONAR RX NESSA ARQUITETURA ??

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

    . Callback(T) Callback(T) Callback(T)
  25. SUBSTITUIR CALLBACKS POR SEQUÊNCIAS OBSERVÁVEIS

  26. PRIMEIRA INTERAÇÃO CAMADA DE DADOS REATIVA

  27. REST GATEWAY VIEW DELEGATION VIEW DATA SOURCE Flowable<T> PRESENTER Callback(T)

    FlowableSubscriber<T> Disposable
  28. public interface EventsSource { Flowable<Message> fetchWith(MessageToFetchParameters params); Flowable<Message> sendMessage(MessageToSendParameters params);

    } ADEUS CALLBACKS !!!
  29. public class MessagesInfrastructure implements EventsSource { @Override public Flowable<Message> fetchWith(MessageToFetch

    params) { return restAPI.getMessages(params) .subscribeOn(Schedulers.io()) .map(PayloadMapper::map) .flatMap(Flowable::fromIterable); } @Override public Flowable<Message> sendMessage(MessageToSend params) { SendMessageToBody body = SendMessageToBody.convert(params); return restAPI.sendMessage(body) .subscribeOn(Schedulers.io()) .flatMap(emptyBody -> fetchWith(sameFrom(params))); } } Chained request, easy !!!!
  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”
  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
  32. SEGUNDA INTERAÇÃO CAMADA DE APRESENTAÇÃO REATIVA

  33. None
  34. REST GATEWAY VIEW DELEGATION VIEW DATA SOURCE Flowable<T> PRESENTER FlowableSubscriber<U>

    Flowable<U> Disposable
  35. public interface SomeView<T> { Function<Flowable<T>, Disposable> results(); Function<Flowable<Unit>, Disposable> showEmptyState();

    Function<Flowable<Unit>, Disposable> hideEmptyState(); Function<Flowable<Unit>, Disposable> showLoading(); Function<Flowable<Unit>, Disposable> hideLoading(); // More delegation }
  36. public static <T> Disposable bind(Flowable<T> flow, Function<Flowable<T>, Disposable> uiFunc) {

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

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

    return uiFunc.call(flow); } public static <T> Function<Flowable<T>, Disposable> uiFunction(Consumer<T> uiAction) { return uiFunction(uiAction, () -> {}); } public static <T> Function<Flowable<T>, Disposable> uiFunction(Consumer<T>uiAction, Action done) { try { return flowable -> flowable .observeOn(AndroidSchedulers.mainThread()) .subscribe( uiAction, throwable -> Logger.e(throwable.getMessage()), done ); } catch (Exception e) { throw new RuntimeException();} }
  39. public class MessagingActivity extends BaseActivity implements MessagesStreamView { @Override public

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

    Function<Flowable<String>, Disposable> restoreNotSentMessage() { return uiFunction(message -> { Toast.makeText(this, "Erro ao enviar mensagem", LENGTH_SHORT).show(); messageInput.setText(message); }); } @Override public Function<Flowable<String>, Disposable> enableComplaintOption() { return uiFunction(action -> optionView.setVisibility(VISIBLE)); } @Override public Function<Flowable<String>, Disposable> disableComplaintOption() { return uiFunction(action -> optionView.setVisibility(GONE)); } @Override public Function<Flowable<String>, Disposable> showEmptyState() { return uiFunction(action -> emptyStateContainer.setVisibility(VISIBLE)); } // More delegate methods
  41. public class ReactivePresenter<V> { 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; } }
  42. public class ReactivePresenter<V> { 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; } }
  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<HelpDeskEventViewModel> execution = source.sendMessage(parameters) .doOnSubscribe(this::prepareToLoad) .map(ViewModelMappers::map) .flatMap(Flowable::fromIterable) .doOnCompleted(this::finishLoadingMessages); subscriptions().add(bind(execution, view().onMessagesLoaded())); }
  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<HelpDeskEventViewModel> execution = source.sendMessage(parameters) .doOnSubscribe(this::prepareToSend) .map(ViewModelMappers::map) .flatMap(Flowable::fromIterable) .doOnCompleted(this::finishSendMessage); subscriptions().add(bind(execution, view().onMessagesLoaded())); }
  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
  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
  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()); }
  48. public class MockView implements SomeView { @Mock public Action resultsDeliveredAction;

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

    public MockFAQView() { MockitoAnnotations.initMocks(this); } @Override public Function<Flowable<ViewModel>, Disposable> onResults() { return flowable -> flowable.subscribe(resultsDeliveredAction); } ...
  50. TERCEIRA INTERAÇÃO REACTIVE VIEW SEGREGATION

  51. public interface SomeView<T> { Func1<Flowable<T>, Disposable> results(); Function<Flowable<Unit>, Disposable> showEmptyState();

    Function<Flowable<Unit>, Disposable> hideEmptyState(); Function<Flowable<Unit>, Disposable> showLoading(); Function<Flowable<Unit>, Disposable> hideLoading(); Function<Flowable<Unit>, Disposable> networkError(); Function<Flowable<Unit>, Disposable> networkUnavailable(); Function<Flowable<Unit>, Disposable> networkSlow(); }
  52. UI BEHAVIOR VIEW PROTOCOL UI BEHAVIOR UI BEHAVIOR UI BEHAVIOR

    . . .
  53. public interface EmptyStateView<T> { Function<Flowable<Unit>, Disposable> showEmptyState(); Function<Flowable<Unit>, Disposable> hideEmptyState();

    } public interface LoadingView<T> { Function<Flowable<Unit>, Disposable> showLoading(); Function<Flowable<Unit>, Disposable> hideLoading(); }
  54. public interface SomeView<T> extends LoadingView, EmptyStateView, NetworkingReporterView { Function<Flowable<T>, Disposable>

    displayResults(); } public interface NetworkingReporterView<T> { Function<Flowable<Unit>, Disposable> networkError(); Function<Flowable<Unit>, Disposable> networkUnavailable(); Function<Flowable<Unit>, Disposable> networkSlow(); }
  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 !!!
  56. f(g(x))

  57. public class LoadingWhenProcessing<T> implements FlowableTransformer<T, T> { private PublishSubject<Unit> 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<T> call(Flowable<T> upstream) { return upstream .doOnSubscribe(this::showLoading) .doOnTerminate(this::hideLoading); } private void hideLoading() { hide.onNext(Unit.instance()); } private void showLoading() { show.onNext(Unit.instance()); } }
  58. public class SomePresenter extends ReactivePresenter<SomeView> { // Hook all behaviors

    for view [ ... ] public void executeOperation() { bind(executionPipeline(), view().results()); } private Flowable<Data> executionPipeline() { return source.search() .compose(networkErrorFeedback) .compose(loadingWhenProcessing) .compose(coordinateRefresh) .compose(emptyStateWhenMissingData) .compose(errorWhenProblems) .map(DataViewModelMapper::map); } }
  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
  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)); }
  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!
  62. @Test public void shouldTransformView_WhenErrorAtStream() { loadingWhileFetching.bindLoadingContent(view); // When stream will

    propagate an error Flowable<String> 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()); }
  63. None
  64. QUARTA INTERAÇÃO SIMPLIFICAR PARA ESCALAR

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

    mais externas”
  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)
  67. REPENSANDO VIEW PASSIVA public interface EmptyStateView<T> { Function<Flowable<Unit>, Disposable> showEmptyState();

    Function<Flowable<Unit>, Disposable> hideEmptyState(); }
  68. REPENSANDO VIEW PASSIVA public interface EmptyStateView<T> { Function<Flowable<Unit>, Disposable> showEmptyState();

    Function<Flowable<Unit>, Disposable> hideEmptyState(); } public interface EmptyStateView { Action showEmptyState(); Action hideEmptyState(); }
  69. IMPL. VIEW PASSIVA (ANTES) @Override public Func1<Observable<Unit>, Subscription> showEmptyState() {

    return RxUi.uiFunction(unit -> emptyState.setVisibility(View.VISIBLE)); } @Override public Func1<Observable<Unit>, Subscription> hideEmptyState() { return RxUi.uiFunction(unit -> emptyState.setVisibility(View.GONE)); } @Override public Func1<Observable<Unit>, Subscription> showLoading() { return RxUi.uiFunction(unit -> progress.setVisibility(View.VISIBLE)); } @Override public Func1<Observable<Unit>, Subscription> hideLoading() { return RxUi.uiFunction(unit -> progress.setVisibility(View.GONE)); } // More delegation
  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
  71. ENTREGA DE DADOS (ANTES) public interface SomeView extends EmptyStateView, LoadingView,

    NetworkErrorReporterView { Function<Flowable<ViewModel>, Disposable> onResults(); } // At view implementation @Override public Function<Flowable<ViewModel>, Disposable> onResults() { return RxUi.uiFunction( model -> adapter.add(model), this::displayResults ); }
  72. ENTREGA DE DADOS (DEPOIS) public interface DisplayFactsView extends LoadingView, ErrorStateView,

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

    EmptyStateView { Disposable subscribeInto(Flowable<FactViewModel> flow); } @Override public Disposable subscribeInto(Flowable<FactViewModel> flow) { return flow .observeOn(AndroidSchedulers.mainThread()) .subscribe( model -> addToAdapter(model), throwable -> Logger.e(throwable.getMessage()), () -> displayResults() ); }
  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 …
  75. public class FactsPresenter { private GetRandomFacts usecase; private DisplayFactsView view;

    private BehavioursCoordinator<FactAboutNumber> 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!!!
  76. public class FactsPresenter { private GetRandomFacts usecase; private DisplayFactsView view;

    private BehavioursCoordinator<FactAboutNumber> 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; }
  77. public class BehavioursCoordinator<T> implements FlowableTransformer<T, T> { private AssignEmptyState<T> dealWithEmptyState;

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

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

    private Action atError; private ErrorPredicate errorPredicate; private Scheduler targetScheduler; // Constructor @Override public Publisher<T> apply(Flowable<T> 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(); } }
  80. public class HideAtStartShowAtError<T> implements FlowableTransformer<T, T> { private Action whenStart;

    private Action atError; private ErrorPredicate errorPredicate; private Scheduler targetScheduler; // Constructor @Override public Publisher<T> apply(Flowable<T> 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(); } }
  81. public class AssignEmptyState<T> implements FlowableTransformer<T, T> { EmptyStateView view; Scheduler

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

    uiScheduler; public AssignEmptyState(EmptyStateView view, Scheduler uiScheduler) { this.view = view; this.uiScheduler = uiScheduler; } @Override public Publisher<T> apply(Flowable<T> upstream) { HideAtStartShowAtError<T> delegate = new HideAtStartShowAtError<>( view.hideEmptyState(), view.showEmptyState(), error -> error instanceof ContentNotFoundError, uiScheduler ); return upstream.compose(delegate); } }
  83. public void fetchData() { if (isBinded()) { RxUi.bind(executionPipeline(), view().results()); }

    } private Flowable<SomeModel> executionPipeline() { return source.fetch() .compose(networkErrorFeedback) .compose(loadingWhileFetching) .compose(coordinateRefresh) .compose(emptyStateWhenMissingData) .compose(showErroState) .map(ViewModelMapper::map) .flatMap(Flowable::fromIterable); } ANTES …
  84. NOVO PIPELINE public void fetchRandomFacts() { Flowable<FactViewModel> 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); }
  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<String> empty = Flowable.empty(); empty.compose(assignErrorState).subscribe(); verify(hide, oneTimeOnly()).run(); verify(show, never()).run(); }
  86. TESTES LIMPOS (II) @Test public void shouldPresent_NoContentError_IntoView() throws Exception {

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

    Flowable<FactAboutNumber> noContent = Flowable.error(new ContentNotFoundError()); when(usecase.fetchTrivia()).thenReturn(noContent); presenter.fetchRandomFacts(); BehavioursVerifier.with(view) .showLoadingFirstHideLoadingAfter() .shouldShowEmptyState() .shouldNotShowErrorState(); }
  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"); }
  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"); }
  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"); }
  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
  92. FINAL LAP

  93. Como eliminar as APIs pública e privada do Presenter para

    hooks de lifecycle de Activity ou Fragment ???
  94. public class ReactivePresenter<V> { 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; } } ????????
  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
  96. None
  97. VIEW PROTOCOL VIEW IMPL. PRESENTER FlowableSubscriber<U> Flowable<U> LifecycleStrategist Disposable DisposeStrategy

    LifecycleObserver LifecycleOwner Flowable<T> (eg. Activity)
  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(); } }
  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); } }
  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); } }
  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); } }
  102. public class FactsPresenter { private GetRandomFacts usecase; private DisplayFactsView view;

    private BehavioursCoordinator<FactAboutNumber> 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; }
  103. public void fetchRandomFacts() { Flowable<FactViewModel> dataFlow = coordinator .coordinateFlow(usecase.fetchTrivia()) .map(fact

    -> mapper.translateFrom(fact)); Disposable toDispose = view.subscribeInto(dataFlow); strategist.applyStrategy(toDispose) }
  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
  105. CONCLUSÕES

  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
  107. TALK IS CHEAP

  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)
  109. https://speakerdeck.com/ubiratansoares/evoluindo-arquiteturas-reativas

  110. UBIRATAN SOARES Computer Scientist by ICMC/USP Software Engineer, curious guy

    Google Developer Expert for Android Teacher, speaker, etc, etc
  111. OBRIGADO @ubiratanfsoares ubiratansoares.github.io https://br.linkedin.com/in/ubiratanfsoares