Slide 1

Slide 1 text

EVOLUINDO ARQUITETURAS REATIVAS Ubiratan Soares Julho / 2017

Slide 2

Slide 2 text

O QUE É UMA ARQUITETURA EM MOBILE ?

Slide 3

Slide 3 text

MVP MVVM VIPER FLUX REDUX CLEAN MVC … MVI

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

UMA QUEIXA COMUM NA COMUNIDADE MOBILE ?

Slide 7

Slide 7 text

TEM PELO MENOS UM UNIT TEST NO APP?

Slide 8

Slide 8 text

EM MOBILE, ARQUITETURA É CRÍTICA PARA TESTABILIDADE

Slide 9

Slide 9 text

QUAL ARQUITETURA ESCOLHER ENTÃO ???

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

NÃO HÁ SILVER BULLETS!

Slide 14

Slide 14 text

ESTUDO DE CASO MVP / CLEAN

Slide 15

Slide 15 text

PRESENTATION LAYER DATA LAYER DB REST ETC UI . . .

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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 } }

Slide 18

Slide 18 text

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) { … } } }); } }

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

BRACE YOURSELVES RX IS COMING

Slide 23

Slide 23 text

COMO ADICIONAR RX NESSA ARQUITETURA ??

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

SUBSTITUIR CALLBACKS POR SEQUÊNCIAS OBSERVÁVEIS

Slide 26

Slide 26 text

PRIMEIRA INTERAÇÃO CAMADA DE DADOS REATIVA

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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 !!!!

Slide 30

Slide 30 text

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”

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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();} }

Slide 37

Slide 37 text

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();} }

Slide 38

Slide 38 text

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();} }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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; } }

Slide 42

Slide 42 text

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; } }

Slide 43

Slide 43 text

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())); }

Slide 44

Slide 44 text

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())); }

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

@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()); }

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

TERCEIRA INTERAÇÃO REACTIVE VIEW SEGREGATION

Slide 51

Slide 51 text

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(); }

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

- 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 !!!

Slide 56

Slide 56 text

f(g(x))

Slide 57

Slide 57 text

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()); } }

Slide 58

Slide 58 text

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); } }

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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)); }

Slide 61

Slide 61 text

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!

Slide 62

Slide 62 text

@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()); }

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

QUARTA INTERAÇÃO SIMPLIFICAR PARA ESCALAR

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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)

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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 ); }

Slide 72

Slide 72 text

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() ); }

Slide 73

Slide 73 text

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() ); }

Slide 74

Slide 74 text

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 …

Slide 75

Slide 75 text

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!!!

Slide 76

Slide 76 text

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; }

Slide 77

Slide 77 text

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); } }

Slide 78

Slide 78 text

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); } }

Slide 79

Slide 79 text

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(); } }

Slide 80

Slide 80 text

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(); } }

Slide 81

Slide 81 text

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); } }

Slide 82

Slide 82 text

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); } }

Slide 83

Slide 83 text

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 …

Slide 84

Slide 84 text

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); }

Slide 85

Slide 85 text

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(); }

Slide 86

Slide 86 text

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(); }

Slide 87

Slide 87 text

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(); }

Slide 88

Slide 88 text

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"); }

Slide 89

Slide 89 text

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"); }

Slide 90

Slide 90 text

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"); }

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

FINAL LAP

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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; } } ????????

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

No content

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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(); } }

Slide 99

Slide 99 text

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); } }

Slide 100

Slide 100 text

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); } }

Slide 101

Slide 101 text

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); } }

Slide 102

Slide 102 text

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; }

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

CONCLUSÕES

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

TALK IS CHEAP

Slide 108

Slide 108 text

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)

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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