Android Architecture Patterns Part 2:Model-View-Presenter
It’s about time we developers start thinking about how we can apply good architecture patterns in our Android apps. Pour aider à cela, Google propose des plans d’architecture Android, où Erik Hellman et moi avons travaillé ensemble sur l’échantillon MVP &RxJava. Voyons comment nous l’avons appliquée et les avantages et les inconvénients de cette approche.
Voici les rôles de chaque composant :
- Modèle – la couche de données. Responsable de la gestion de la logique métier et de la communication avec les couches réseau et base de données.
- Vue – la couche d’interface utilisateur. Affiche les données et informe le présentateur des actions de l’utilisateur.
- Presenter – récupère les données du Modèle, applique la logique de l’interface utilisateur et gère l’état de la Vue, décide de ce qu’il faut afficher et réagit aux notifications d’entrée de l’utilisateur de la Vue.
Comme la Vue et le Présentateur travaillent en étroite collaboration, ils doivent avoir une référence l’un à l’autre. Pour rendre l’unité Presenter testable avec JUnit, la Vue est abstraite et une interface est utilisée pour elle. La relation entre le Présentateur et sa Vue correspondante est définie dans une classe d’interface Contract
, ce qui rend le code plus lisible et la connexion entre les deux plus facile à comprendre.
Le modèle Model-View-Presenter &RxJava dans les plans d’architecture Android
L’exemple de plan est une application ”À faire”. Il permet à un utilisateur de créer, lire, mettre à jour et supprimer des tâches « À faire”, ainsi que d’appliquer des filtres à la liste de tâches affichée. RxJava est utilisé pour quitter le thread principal et pouvoir gérer les opérations asynchrones.
Modèle
Le modèle fonctionne avec les sources de données distantes et locales pour obtenir et enregistrer les données. C’est là que la logique métier est gérée. Par exemple, lors de la demande de la liste des Task
, le modèle essaierait de les récupérer à partir de la source de données locale. S’il est vide, il interrogera le réseau, enregistrera la réponse dans la source de données locale, puis renverra la liste.
La récupération des tâches se fait à l’aide de RxJava:
public Observable<List<Task>> getTasks(){
...
}
Le Modèle reçoit comme paramètres dans les interfaces constructeur des sources de données locales et distantes, ce qui rend le Modèle complètement indépendant de toutes les classes Android et donc facile à tester avec JUnit. Par exemple, pour tester que getTasks
demande des données à la source locale, nous avons implémenté le test suivant :
@Mock
private TasksDataSource mTasksRemoteDataSource;
@Mock
private TasksDataSource mTasksLocalDataSource;
...
@Test
public void getTasks_requestsAllTasksFromLocalDataSource() {
// Given that the local data source has data available
setTasksAvailable(mTasksLocalDataSource, TASKS);
// And the remote data source does not have any data available
setTasksNotAvailable(mTasksRemoteDataSource);
// When tasks are requested from the tasks repository
TestSubscriber<List<Task>> testSubscriber =
new TestSubscriber<>();
mTasksRepository.getTasks().subscribe(testSubscriber); // Then tasks are loaded from the local data source
verify(mTasksLocalDataSource).getTasks();
testSubscriber.assertValue(TASKS);
}
View
La vue fonctionne avec le Présentateur pour afficher les données et elle informe le Présentateur des actions de l’utilisateur. Dans les Activités MVP, les Fragments et les vues Android personnalisées peuvent être des vues. Notre choix a été d’utiliser des Fragments.
Toutes les vues implémentent la même interface BaseView qui permet de définir un présentateur.
public interface BaseView<T> {
void setPresenter(T presenter);
}
La Vue informe le Présentateur qu’elle est prête à être mise à jour en appelant la méthode subscribe
du Présentateur dans onResume
. La vue appelle presenter.unsubscribe()
dans onPause
pour indiquer au Présentateur qu’il n’est plus intéressé à être mis à jour. Si l’implémentation de la Vue est une vue personnalisée Android, les méthodes subscribe
et unsubscribe
doivent être appelées sur onAttachedToWindow
et onDetachedFromWindow
. Les actions de l’utilisateur, comme les clics sur les boutons, déclencheront les méthodes correspondantes dans le Présentateur, c’est celui qui décide de la suite des choses.
Les vues sont testées avec Espresso. L’écran statistiques, par exemple, doit afficher le nombre de tâches actives et terminées. Le test qui vérifie que cela est fait correctement place d’abord certaines tâches dans le TaskRepository
; puis lance le StatisticsActivity
et vérifie le contenu des vues :
@Before
public void setup() {
// Given some tasks
TasksRepository.destroyInstance();
TasksRepository repository = Injection.provideTasksRepository( InstrumentationRegistry.getContext()); repository.saveTask(new Task("Title1", "", false));
repository.saveTask(new Task("Title2", "", true)); // Lazily start the Activity from the ActivityTestRule
Intent startIntent = new Intent();
mStatisticsActivityTestRule.launchActivity(startIntent);
}@Test
public void Tasks_ShowsNonEmptyMessage() throws Exception {
// Check that the active and completed tasks text is displayed
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()));
}
Présentateur
Le Présentateur et sa Vue correspondante sont créés par l’Activité. Les références à la Vue et au TaskRepository
– le Modèle – sont données au constructeur du Présentateur. Dans l’implémentation du constructeur, le présentateur appellera la méthode setPresenter
de la vue. Cela peut être simplifié lors de l’utilisation d’un framework d’injection de dépendances qui permet l’injection des présentateurs dans les vues correspondantes, réduisant ainsi le couplage des classes. La mise en œuvre du ToDo-MVP avec Dagger est couverte dans un autre échantillon.
Tous les présentateurs implémentent la même interface BasePresenter.
public interface BasePresenter {
void subscribe();
void unsubscribe();
}
Lorsque la méthode subscribe
est appelée, le Présentateur commence à demander les données du modèle, puis il applique la logique de l’interface utilisateur aux données reçues et les définit dans la Vue. Par exemple, dans le StatisticsPresenter
, toutes les tâches sont demandées à partir du TaskRepository
– puis les tâches récupérées sont utilisées pour calculer le nombre de tâches actives et terminées. Ces nombres seront utilisés comme paramètres pour la méthode showStatistics(int numberOfActiveTasks, int numberOfCompletedTasks)
de la Vue.
Un test unitaire pour vérifier qu’en effet la méthode showStatistics
est appelée avec les valeurs correctes est facile à implémenter. Nous nous moquons du TaskRepository
et du StatisticsContract.View
et donnons les objets moqués comme paramètres au constructeur d’un objet StatisticsPresenter
. L’implémentation du test est la suivante :
@Test
public void loadNonEmptyTasksFromRepository_CallViewToDisplay() {
// Given an initialized StatisticsPresenter with
// 1 active and 2 completed tasks
setTasksAvailable(TASKS); // When loading of Tasks is requested
mStatisticsPresenter.subscribe(); // Then the correct data is passed on to the view
verify(mStatisticsView).showStatistics(1, 2);
}
Le rôle de la méthode unsubscribe
est d’effacer tous les abonnements du Présentateur, évitant ainsi les fuites de mémoire.
En dehors de subscribe
et unsubscribe
, chaque présentateur expose d’autres méthodes, correspondant aux actions de l’utilisateur dans la Vue. Par exemple, AddEditTaskPresenter
, ajoute des méthodes comme createTask
, qui seraient appelées lorsque l’utilisateur appuie sur le bouton qui crée une nouvelle tâche. Cela garantit que toutes les actions de l’utilisateur – et par conséquent toute la logique de l’interface utilisateur – passent par le présentateur et peuvent ainsi être testées à l’unité.
Inconvénients du Modèle-Vue-Présentateur
Le Modèle-Vue-Présentateur apporte une très bonne séparation des préoccupations. Bien que ce soit à coup sûr un pro, lors du développement d’une petite application ou d’un prototype, cela peut sembler une surcharge. Pour diminuer le nombre d’interfaces utilisées, certains développeurs suppriment la classe d’interface Contract
et l’interface du présentateur.
L’un des pièges de MVP apparaît lors du déplacement de la logique de l’interface utilisateur vers le Présentateur: cela devient maintenant une classe omnisciente, avec des milliers de lignes de code. Pour résoudre ce problème, divisez encore plus le code et n’oubliez pas de créer des classes qui n’ont qu’une seule responsabilité et sont testables à l’unité.
Conclusion
Le modèle Modèle-Vue-Contrôleur présente deux inconvénients principaux : premièrement, la Vue fait référence à la fois au Contrôleur et au Modèle; et deuxièmement, cela ne limite pas la gestion de la logique de l’interface utilisateur à une seule classe, cette responsabilité étant partagée entre le Contrôleur et la Vue ou le Modèle. Le modèle Modèle-Vue-Présentateur résout ces deux problèmes en rompant la connexion de la Vue avec le Modèle et en créant une seule classe qui gère tout ce qui concerne la présentation de la Vue — le Présentateur : une seule classe facile à tester unitairement.
Que se passe-t-il si nous voulons une architecture basée sur les événements, où la Vue réagit aux changements ? Restez à l’écoute pour les prochains modèles échantillonnés dans les plans d’architecture Android pour voir comment cela peut être implémenté. D’ici là, découvrez notre implémentation de modèle Model-View-ViewModel dans l’application upday.