Padrões de arquitetura do Android, parte 2: Model-View-Presenter
Já é hora de nós, desenvolvedores, começarmos a pensar em como podemos aplicar bons padrões de arquitetura em nossos aplicativos Android. Para ajudar com isso, o Google oferece o Android Architecture Blueprints, onde Erik Hellman e eu trabalhamos juntos no exemplo MVP e RxJava. Vamos dar uma olhada em como o aplicamos e os prós e contras dessa abordagem.
GoogleSample/android-architecture
Conteudo
O padrão Model-View-Presenter
Aqui estão as funções de cada componente:
- Model – a camada de dados. Responsável por lidar com a lógica de negócios e comunicação com as camadas de rede e banco de dados.
- View – a camada de IU. Exibe os dados e notifica o apresentador sobre as ações do usuário.
- Presenter – recupera os dados do Modelo, aplica a lógica da IU e gerencia o estado da Visualização, decide o que exibir e reage às notificações de entrada do usuário na Visualização.
Como o View e o Presenter trabalham juntos, eles precisam ter uma referência um ao outro. Para tornar a unidade do Presenter testável com JUnit, a visualização é abstraída e uma interface para ela é usada. O relacionamento entre o Apresentador e sua Visualização correspondente é definido em uma classe de interface de Contrato, tornando o código mais legível e a conexão entre os dois mais fácil de entender.
O padrão Model-View-Presenter e RxJava nos projetos de arquitetura do Android
A amostra do blueprint é um aplicativo “To Do”. Ele permite que um usuário crie, leia, atualize e exclua tarefas “A Fazer”, bem como aplique filtros à lista de tarefas exibida. RxJava é usado para sair do thread principal e ser capaz de lidar com operações assíncronas.
Model
O model trabalha com as fontes de dados remotas e locais para obter e salvar os dados. É aqui que a lógica de negócios é tratada. Por exemplo, ao solicitar a lista de Tarefas, o model tentaria recuperá-las da fonte de dados local. Se estiver vazio, ele consultará a rede, salvará a resposta na fonte de dados local e retornará a lista.
A recuperação de tarefas é feita com a ajuda de RxJava:
public Observable<List<Task>> getTasks(){ ... }
O modelo recebe como parâmetros nas interfaces do construtor das fontes de dados locais e remotas, tornando o modelo completamente independente de quaisquer classes Android e, portanto, fácil de testar a unidade com JUnit. Por exemplo, para testar se getTasks solicita dados da fonte local, implementamos o seguinte teste:
@Mock private TasksDataSource mTasksRemoteDataSource; @Mock private TasksDataSource mTasksLocalDataSource; ... @Test public void getTasks_requestsAllTasksFromLocalDataSource() { // Dado que a fonte de dados local tem dados disponíveis setTasksAvailable(mTasksLocalDataSource, TASKS); // E a fonte de dados remota não tem nenhum dado disponível setTasksNotAvailable(mTasksRemoteDataSource); // Quando as tarefas são solicitadas do repositório de tarefas TestSubscriber<List<Task>> testSubscriber = new TestSubscriber<>(); mTasksRepository.getTasks().subscribe(testSubscriber); // Em seguida, as tarefas são carregadas da fonte de dados local verify(mTasksLocalDataSource).getTasks(); testSubscriber.assertValue(TASKS); }
View
O modo de exibição trabalha com o apresentador para exibir os dados e notifica o apresentador sobre as ações do usuário. Em atividades MVP, fragmentos e visualizações personalizadas do Android podem ser visualizações. Nossa escolha foi usar Fragments.
Todas as visualizações implementam a mesma interface BaseView que permite a configuração de um apresentador.
public interface BaseView<T> { void setPresenter(T presenter); }
A View notifica o Presenter de que está pronta para ser atualizada, chamando o método subscribem do Presenter em onResume. O modo de exibição chama presenter.unsubscribe () em onPause para informar ao apresentador que ele não está mais interessado em ser atualizado. Se a implementação da Visualização for uma visualização customizada do Android, então os métodos de inscrição e cancelamento de inscrição devem ser chamados em onAttachedToWindow e onDetachedFromWindow. As ações do usuário, como cliques em botões, irão disparar métodos correspondentes no Presenter, sendo este o que decide o que deve acontecer a seguir.
As visualizações são testadas com o Espresso. A tela de estatísticas, por exemplo, precisa exibir o número de tarefas ativas e concluídas. O teste que verifica se isso foi feito corretamente primeiro coloca algumas tarefas no TaskRepository; em seguida, inicia o StatisticsActivity e verifica o conteúdo das visualizações:
@Before public void setup() { // Dado algumas tarefas TasksRepository.destroyInstance(); TasksRepository repository = Injection.provideTasksRepository(InstrumentationRegistry.getContext()); repository.saveTask(new Task("Title1", "", false)); repository.saveTask(new Task("Title2", "", true)); // Preguiçosamente iniciar a atividade a partir da ActivityTestRule Intent startIntent = new Intent(); mStatisticsActivityTestRule.launchActivity(startIntent); } @Test public void Tasks_ShowsNonEmptyMessage() throws Exception { // Verifique se o texto das tarefas ativas e concluídas é exibido Context context = InstrumentationRegistry.getTargetContext(); String expectedActiveTaskText = context .getString(R.string.statistics_active_tasks); String expectedCompletedTaskText = context .getString(R.string.statistics_completed_tasks); onView(withText(containsString(expectedActiveTaskText))) .check(matches(isDisplayed())); onView(withText(containsString(expectedCompletedTaskText))) .check(matches(isDisplayed())); }
Presenter
O apresentador e sua visualização correspondente são criados pela atividade. As referências à View e ao TaskRepository – o Model – são fornecidas ao construtor do Presenter. Na implementação do construtor, o Presenter irá chamar o método setPresenter da View. Isso pode ser simplificado ao usar um framework de injeção de dependências que permite a injeção dos Presenters nas visualizações correspondentes, reduzindo o acoplamento das classes. A implementação do ToDo-MVP com Dagger é abordada em outro exemplo.
Todos os apresentadores implementam a mesma interface BasePresenter.
public interface BasePresenter { void subscribe(); void unsubscribe(); }
Quando o método de inscrição é chamado, o Apresentador começa a solicitar os dados do Modelo e, em seguida, aplica a lógica da IU aos dados recebidos e os configura para a Visualização. Por exemplo, no StatisticsPresenter, todas as tarefas são solicitadas do TaskRepository – então as tarefas recuperadas são usadas para calcular o número de tarefas ativas e concluídas. Esses números serão usados como parâmetros para o método showStatistics (int numberOfActiveTasks, int numberOfCompletedTasks) da View.
Um teste de unidade para verificar se de fato o método showStatistics é chamado com os valores corretos é fácil de implementar. Estamos simulando o TaskRepository e o StatisticsContract.View e fornecemos os objetos simulados como parâmetros para o construtor de um objeto StatisticsPresenter. A implementação do teste é:
@Test public void loadNonEmptyTasksFromRepository_CallViewToDisplay() { // Dado um StatisticsPresenter inicializado com // 1 tarefa ativa e 2 tarefas concluídas setTasksAvailable(TASKS); // Quando o carregamento de tarefas é solicitado mStatisticsPresenter.subscribe(); // Em seguida, os dados corretos são passados para a visualização verify(mStatisticsView).showStatistics(1, 2); }
A função do método de cancelamento de assinatura é limpar todas as assinaturas do Presenter, evitando assim vazamentos de memória.
Além de assinar e cancelar assinatura, cada apresentador expõe outros métodos, correspondentes às ações do usuário na Visualização. Por exemplo, o AddEditTaskPresenter adiciona métodos como createTask, que seriam chamados quando o usuário pressionasse o botão que cria uma nova tarefa. Isso garante que todas as ações do usuário – e, consequentemente, toda a lógica da IU – passem pelo Presenter e, portanto, possam ser testadas na unidade.
Desvantagens do padrão Model-View-Presenter
O padrão Model-View-Presenter traz consigo uma excelente separação de interesses. Embora seja um profissional, ao desenvolver um pequeno aplicativo ou protótipo, isso pode parecer uma sobrecarga. Para diminuir o número de interfaces usadas, alguns desenvolvedores removem a classe de interface Contrato e a interface do Apresentador.
Uma das armadilhas do MVP aparece ao mover a lógica da IU para o Presenter: agora ela se torna uma classe onisciente, com milhares de linhas de código. Para resolver isso, divida ainda mais o código e lembre-se de criar classes que tenham apenas uma responsabilidade e sejam testáveis por unidade.
Conclusão
O padrão Model-View-Controller tem duas desvantagens principais: em primeiro lugar, a View tem uma referência tanto para o Controller quanto para o Model; e em segundo lugar, não limita o manuseio da lógica da IU a uma única classe, sendo essa responsabilidade compartilhada entre o Controlador e a Visualização ou o Modelo. O padrão Model-View-Presenter resolve esses dois problemas quebrando a conexão que a View tem com o Model e criando apenas uma classe que lida com tudo relacionado à apresentação da View – o Presenter: uma única classe que é fácil de unir teste.
E se quisermos uma arquitetura baseada em eventos, onde o View reage às mudanças? Fique atento aos próximos padrões amostrados nos Blueprints de arquitetura do Android para ver como isso pode ser implementado. Até então, leia sobre a implementação do padrão Model-View-ViewModel no aplicativo de atualização.