Découvrez le pattern ViewModel
Pour rappel, le pattern ViewModel a deux objectifs :
Exposer des données, ou états, représentant les informations dont l’interface a besoin.
Fournir des fonctions correspondant aux événements qui ont lieu sur l’interface et qui nécessitent la modification des données.
La classe ViewModel fournie par Google
Pour enrichir davantage ce pattern qui n’est pas spécifique à Android, Google fournit une classe, nommée également ViewModel
, qui a la capacité de stocker les données qu’elle contient en tenant compte du cycle de vie de la vue associée. Plus spécifiquement, grâce à cette classe ViewModel, les données stockées dans des variables vont pouvoir survivre aux modifications de configuration de notre application, telles que les rotations d'écran.
Pour utiliser cette classe, il suffit de l’étendre en déclarant par exemple public class QuizViewModel extends ViewModel {}
.
Créez une classe de type ViewModel
Commençons par organiser correctement notre projet. Il est d’usage de positionner tout le code relatif à la gestion de l’interface d’une fonctionnalité au même endroit, habituellement au sein d’un package nommé ui
.
Organisez le package ui
Vous pouvez donc créer un package ui
de la même manière que vous l’avez fait dans le chapitre précédent. Ensuite vous pouvez déplacer dans le package ui
tout le code de notre application relatif à la gestion de l’interface utilisateur, c’est-à-dire :
QuizFragment
;WelcomeFragment
.
Pour cela, il suffit de faire un drag & drop de ces fichiers dans le package cible.
Créez la classe QuizViewModel
Maintenant que tout est bien rangé, il est temps de créer cette fameuse classe de type ViewModel
que nous nommerons QuizViewModel
, en référence à la fonctionnalité de quiz qu’elle permet de gérer.
Comme une classe de type ViewModel gère la logique relative à une interface, nous pouvons ranger ce type de classe dans le package ui
. Dans notre cas, nous pouvons donc créer QuizViewModel
dans le package ui.quiz
.
À vous de jouer !
Vous allez pouvoir implémenter la classe QuizViewModel
. Elle étend la classe ViewModel
fournie par Android, et prend en paramètre la classe QuestionRepository
, le fameux médiateur qui nous fournit les données dont nous avons besoin : la liste de questions.
Voici ce que vous devriez obtenir :
public class QuizViewModel extends ViewModel {
private QuestionRepository questionRepository;
public QuizViewModel(QuestionRepository questionRepository) {
this.questionRepository = questionRepository;
}
}
Il est maintenant temps de compléter QuizViewModel
avec le code relatif à la logique du quiz. Pour cela, nous allons concrètement devoir identifier les états et les événements dont notre interface va avoir besoin.
Identifiez les états de l’écran de quiz
À l’échelle de l’écran de quiz de notre application, les états permettant de définir l’affichage sont :
la question en cours ;
savoir si l’utilisateur est arrivé à la dernière question, pour afficher le bouton “Finish” ;
le score en particulier pour afficher le score final.
Ces états doivent donc être partagés avec la vue ; dans notre cas, avec le fragment QuizFragment
. Pour les partager, nous allons utiliser le design pattern Observer.
Encore un pattern ? Mais à quoi sert-il ?
Le pattern Observer repose sur le principe suivant : un observable émet des informations. Un ou des observateurs reçoivent ces informations.
Autrement dit, lorsqu’une variable de type observable est modifiée, elle va notifier ses observateurs de ce changement. Les observateurs vont pouvoir réagir à ces changements en les reflétant dans l’interface.
Pour pouvoir créer une telle variable capable de notifier des observateurs, tout en tenant compte du cycle de vie particulier d’une application Android, Google fournit les LiveData
.
Exposez les états grâce aux LiveData
Un LiveData
peut contenir n’importe quel type de donnée : un type primitif comme un Integer
, ou un objet spécifique à notre application, comme Question
.
Pour créer un LiveData
, il faut utiliser la classe MutableLiveData
qui prend en paramètre une valeur initiale. Par exemple, pour initialiser l’état contenant la question en cours, ajoutez le code suivant dans la classe QuizViewModel
:
MutableLiveData<Question> currentQuestion = new MutableLiveData<Question>();
Ainsi déclarée, la valeur initiale contenue dans ce LiveData
est nulle, car nous n’avons rien renseigné en paramètre de l’objet MutableLiveData
.
Plus tard, pour mettre à jour la valeur contenue dans ce LiveData
, il suffira d’appeler la fonction postValue()
comme ceci :
currentQuestion.postValue(questions.get(1));
Pour lire le contenu d’un LiveData
, il suffit d’appeler la fonction getValue()
; par exemple : currentQuestion.getValue();
.
Pour le moment, restons dans la classe QuizViewModel
. Nous pouvons créer les deux autres états dont a besoin l’interface, soit :
un booléen permettant de savoir si l’utilisateur est arrivé à la dernière question, initialisé à faux ;
le score initialisé à 0.
Cela donne :
MutableLiveData<Integer> score = new MutableLiveData<Integer>(0);
MutableLiveData<Boolean> isLastQuestion = new MutableLiveData<Boolean>(false);
Les états de QuizViewModel
sont maintenant initialisés, et ils n’attendent plus que d’être modifiés suite aux événements ayant lieu sur l’interface.
Identifiez les événements de l’écran de quiz
À l’échelle de l’écran de quiz de notre application, les événements qui vont impacter l’état de l’interface sont :
lorsque l’utilisateur arrive pour la première fois sur le quiz, celui-ci démarre ;
lorsque l’utilisateur clique sur une réponse à une question, pour connaître la validité de la réponse ;
lorsque l'utilisateur clique sur “Next” pour passer à la question suivante.
Cela se traduit par la création de trois fonctions dans notre ViewModel :
startQuiz
;
isAnswerValid
;
nextQuestion
.
À vous de jouer !
Essayez de coder seul(e) ces trois fonctions. Si besoin, voici quelques pistes pour leur conception :
la fonction
startQuiz
récupère les questions viaQuestionRepository
, et les stockent dans une variable globale. Elle initialise également l’étatcurrentQuestion
avec la première question ;la fonction
isAnswerValid
prend en paramètre l’index de la réponse, et retourne un booléen selon que la réponse est vraie ou fausse. Pour savoir si la réponse est correcte, elle utilise l’étatcurrentQuestion
afin de récupérer l’index de bonne réponse. Si la réponse est correcte, elle met également à jour l’état correspondant au score ;la fonction
nextQuestion
met à jour l’étatcurrentQuestion
avec la question suivante, ainsi que l’étatisLastQuestion
dans le cas où l’utilisateur arrive à la dernière question. Cette fonction nécessite la création d’une variable globale contenant l’index de la question en cours.
Voici la correction :
public class QuizViewModel extends ViewModel {
private QuestionRepository questionRepository;
private List<Question> questions;
private Integer currentQuestionIndex = 0;
public QuizViewModel(QuestionRepository questionRepository) {
this.questionRepository = questionRepository;
}
MutableLiveData<Question> currentQuestion = new MutableLiveData<Question>();
MutableLiveData<Integer> score = new MutableLiveData<Integer>(0);
MutableLiveData<Boolean> isLastQuestion = new MutableLiveData<Boolean>(false);
public void startQuiz(){
questions = questionRepository.getQuestions();
currentQuestion.postValue(questions.get(0));
}
public void nextQuestion() {
Integer nextIndex = currentQuestionIndex + 1;
if(nextIndex >= questions.size()) {
return; // should not happened as the 'Next' button is not displayed at the last question
} else if (nextIndex == questions.size() - 1) {
isLastQuestion.postValue(true);
}
currentQuestionIndex = nextIndex;
currentQuestion.postValue(questions.get(currentQuestionIndex));
}
public Boolean isAnswerValid(Integer answerIndex) {
Question question = currentQuestion.getValue();
boolean isValid = question != null && question.getAnswerIndex() == answerIndex;
Integer currentScore = score.getValue();
if(currentScore != null && isValid) {
score.setValue(currentScore + 1);
}
return isValid;
}
}
En résumé
Il est d’usage de positionner tous les fichiers relatifs à l’interface d’une application au même endroit, par exemple un package nommé
ui
.Le
ViewModel
contient la logique spécifique à l’affichage d’un écran sous forme d’états et d’événements.Google fournit une classe
ViewModel
spécifique à Android, qui permet de stocker les variables qu’elle contient en tenant compte des changements de configuration de la vue qui la manipule.Google fournit les
LiveData
pour exposer les états à la vue sous forme d’observables.On initialise un
LiveData
via la classeMutableLiveData
. Pour lire sa valeur, on utilise la fonctiongetValue()
; pour remplacer sa valeur, on utilise la fonctionpostValue()
.
Bravo ! Vous avez parcouru un sacré chemin dans la structuration du code de votre application. C’est l’heure de la dernière étape, la dynamisation des composants de notre interface grâce aux états et événements que vous venez de créer dans QuizViewModel.