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. 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
  2. public interface ViewDelegate { void displayResults(DataModel model); void networkingError(); void

    displayEmptyState(); void displayErrorState(); // More delegation }
  3. 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 } }
  4. 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) { … } } }); } }
  5. 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
  6. 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
  7. 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
  8. PRESENTATION LAYER DATA LAYER DB REST ETC UI . .

    . Callback(T) Callback(T) Callback(T)
  9. 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 !!!!
  10. 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”
  11. 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
  12. 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 }
  13. 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();} }
  14. 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();} }
  15. 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();} }
  16. 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
  17. 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
  18. 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; } }
  19. 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; } }
  20. 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())); }
  21. 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())); }
  22. 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
  23. 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
  24. @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()); }
  25. 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); } ...
  26. 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); } ...
  27. 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(); }
  28. 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(); }
  29. 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(); }
  30. - 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 !!!
  31. 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()); } }
  32. 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); } }
  33. 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
  34. 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)); }
  35. 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!
  36. @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()); }
  37. 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)
  38. REPENSANDO VIEW PASSIVA public interface EmptyStateView<T> { Function<Flowable<Unit>, Disposable> showEmptyState();

    Function<Flowable<Unit>, Disposable> hideEmptyState(); } public interface EmptyStateView { Action showEmptyState(); Action hideEmptyState(); }
  39. 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
  40. 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
  41. 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 ); }
  42. 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() ); }
  43. 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() ); }
  44. 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 …
  45. 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!!!
  46. 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; }
  47. 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); } }
  48. 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); } }
  49. 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(); } }
  50. 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(); } }
  51. 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); } }
  52. 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); } }
  53. 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 …
  54. 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); }
  55. 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(); }
  56. 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(); }
  57. 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(); }
  58. 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"); }
  59. 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"); }
  60. 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"); }
  61. 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
  62. Como eliminar as APIs pública e privada do Presenter para

    hooks de lifecycle de Activity ou Fragment ???
  63. 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; } } ????????
  64. Fazer com que a liberação de Disposable seja responsabilidade de

    algum colaborador que conheça o ciclo de vida do mecanismo de entrega
  65. 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(); } }
  66. 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); } }
  67. 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); } }
  68. 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); } }
  69. 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; }
  70. public void fetchRandomFacts() { Flowable<FactViewModel> dataFlow = coordinator .coordinateFlow(usecase.fetchTrivia()) .map(fact

    -> mapper.translateFrom(fact)); Disposable toDispose = view.subscribeInto(dataFlow); strategist.applyStrategy(toDispose) }
  71. 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
  72. 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
  73. 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)
  74. UBIRATAN SOARES Computer Scientist by ICMC/USP Software Engineer, curious guy

    Google Developer Expert for Android Teacher, speaker, etc, etc