Padrões de arquitetura do Android, parte 2: Model-View-Presenter

Tempo de leitura: 5 minutes

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

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.