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
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) {
…
}
}
});
}
}
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
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
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);
}
...
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 !!!
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());
}
67. 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)
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 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);
}
}
84. 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 …
85. 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);
}
89. 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");
}
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. 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");
}
92. 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
105. 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
107. 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