Principes de base d'Android en Kotlin 05.1 : ViewModel et ViewModelFactory

Cet atelier de programmation fait partie du cours Principes de base d'Android en Kotlin. Vous tirerez pleinement parti de ce cours en suivant les ateliers de programmation dans l'ordre. Tous les ateliers de programmation du cours sont listés sur la page de destination des ateliers de programmation Principes de base d'Android en Kotlin.

Écran titre

Écran de jeu

Écran de score

Introduction

Dans cet atelier de programmation, vous allez découvrir l'un des composants d'architecture Android, ViewModel :

  • Vous utilisez la classe ViewModel pour stocker et gérer les données liées à l'UI tout en tenant compte du cycle de vie. La classe ViewModel permet aux données de survivre aux modifications de la configuration de l'appareil, telles que les rotations d'écran et les modifications de la disponibilité du clavier.
  • Vous utilisez la classe ViewModelFactory pour instancier et renvoyer l'objet ViewModel qui survit aux modifications de configuration.

Ce que vous devez déjà savoir

  • Vous savez comment créer des applications Android de base en langage Kotlin.
  • Comment utiliser le graphique de navigation pour implémenter la navigation dans votre application.
  • Ajouter du code pour naviguer entre les destinations de votre application et transmettre des données entre les destinations de navigation.
  • Vous comprenez le fonctionnement des cycles de vie des activités et des fragments.
  • Vous savez ajouter des informations de journalisation à une application et lire des journaux à l'aide de Logcat dans Android Studio.

Points abordés

  • Utiliser l'architecture d'application Android recommandée.
  • Utiliser les classes Lifecycle, ViewModel et ViewModelFactory dans votre application
  • Vous savez comment conserver les données de l'UI en modifiant la configuration de l'appareil.
  • Définition et utilisation du modèle de conception de méthode de fabrique
  • Comment créer un objet ViewModel à l'aide de l'interface ViewModelProvider.Factory.

Objectifs de l'atelier

  • Ajoutez un ViewModel à l'application pour enregistrer ses données et les conserver en cas de modification de la configuration.
  • Utilisez ViewModelFactory et le modèle de conception de méthode de fabrique pour instancier un objet ViewModel avec des paramètres de constructeur.

Dans les ateliers de programmation de la leçon 5, vous développez l'application GuessTheWord, en commençant par le code de démarrage. GuessTheWord est un jeu de charades à deux joueurs, dans lequel les joueurs collaborent pour obtenir le meilleur score possible.

Le premier joueur regarde les mots dans l'application et mime chacun d'eux à tour de rôle, en veillant à ne pas les montrer au deuxième joueur. Le deuxième joueur essaie de deviner le mot.

Pour jouer, le premier joueur ouvre l'application sur l'appareil et voit un mot, par exemple "guitare", comme indiqué sur la capture d'écran ci-dessous.

Le premier joueur mime le mot, en veillant à ne pas le prononcer.

  • Lorsque le deuxième joueur devine le mot, le premier joueur appuie sur le bouton OK, ce qui incrémente le compteur et affiche le mot suivant.
  • Si le deuxième joueur ne parvient pas à deviner le mot, le premier joueur appuie sur le bouton Passer, ce qui diminue le nombre de mots d'un et passe au mot suivant.
  • Pour mettre fin à la partie, appuyez sur le bouton End Game (Mettre fin à la partie). (Cette fonctionnalité n'est pas incluse dans le code de démarrage du premier atelier de programmation de la série.)

Dans cette tâche, vous allez télécharger et exécuter l'application de démarrage, puis examiner le code.

Étape 1 : Premiers pas

  1. Téléchargez le code de démarrage de GuessTheWord et ouvrez le projet dans Android Studio.
  2. Exécutez l'application sur un appareil Android ou sur un émulateur.
  3. Appuyez sur les boutons. Notez que le bouton Ignorer affiche le mot suivant et diminue le score de un, tandis que le bouton OK affiche le mot suivant et augmente le score de un. Le bouton End Game (Terminer la partie) n'est pas implémenté. Par conséquent, rien ne se passe lorsque vous appuyez dessus.

Étape 2 : Parcourez le code

  1. Dans Android Studio, explorez le code pour comprendre le fonctionnement de l'application.
  2. Assurez-vous de consulter les fichiers décrits ci-dessous, qui sont particulièrement importants.

MainActivity.kt

Ce fichier ne contient que du code par défaut généré à partir du modèle.

res/layout/main_activity.xml

Ce fichier contient la mise en page principale de l'application. NavHostFragment héberge les autres fragments lorsque l'utilisateur parcourt l'application.

Fragments d'UI

Le code de démarrage comporte trois fragments dans trois packages différents sous le package com.example.android.guesstheword.screens :

  • title/TitleFragment pour l'écran de titre
  • game/GameFragment pour l'écran de jeu
  • score/ScoreFragment pour l'écran de score

screens/title/TitleFragment.kt

Le fragment de titre est le premier écran qui s'affiche au lancement de l'application. Un gestionnaire de clics est défini sur le bouton Play (Jouer) pour accéder à l'écran de jeu.

screens/game/GameFragment.kt

Il s'agit du fragment principal, dans lequel la plupart des actions du jeu se déroulent :

  • Des variables sont définies pour le mot actuel et le score actuel.
  • Le wordList défini dans la méthode resetList() est un exemple de liste de mots à utiliser dans le jeu.
  • La méthode onSkip() est le gestionnaire de clics du bouton Ignorer. Il diminue le score de 1, puis affiche le mot suivant à l'aide de la méthode nextWord().
  • La méthode onCorrect() est le gestionnaire de clics du bouton Got It (J'ai compris). Cette méthode est implémentée de la même manière que la méthode onSkip(). La seule différence est que cette méthode ajoute 1 au score au lieu de le soustraire.

screens/score/ScoreFragment.kt

ScoreFragment est le dernier écran du jeu. Il affiche le score final du joueur. Dans cet atelier de programmation, vous allez ajouter l'implémentation pour afficher cet écran et le score final.

res/navigation/main_navigation.xml

Le graphique de navigation montre comment les fragments sont connectés par la navigation :

  • À partir du fragment de titre, l'utilisateur peut accéder au fragment de jeu.
  • Depuis le fragment de jeu, l'utilisateur peut accéder au fragment de score.
  • À partir du fragment de score, l'utilisateur peut revenir au fragment de jeu.

Dans cette tâche, vous allez identifier les problèmes de l'application de démarrage GuessTheWord.

  1. Exécutez le code de démarrage et jouez avec quelques mots en appuyant sur Passer ou OK après chaque mot.
  2. L'écran du jeu affiche désormais un mot et le score actuel. Modifiez l'orientation de l'écran en faisant pivoter l'appareil (si vous utilisez un appareil physique) ou en effectuant l'opération correspondante sur l'émulateur. Notez que le score actuel est perdu.
  3. Jouez avec quelques mots supplémentaires. Lorsque l'écran du jeu s'affiche avec un score, fermez l'application, puis rouvrez-la. Vous remarquerez que le jeu redémarre depuis le début, car l'état de l'application n'est pas enregistré.
  4. Jouez avec quelques mots, puis appuyez sur le bouton Terminer la partie. Vous remarquerez qu'il ne se passe rien.

Problèmes dans l'application :

  • L'application de démarrage n'enregistre ni ne restaure l'état de l'application lorsque la configuration est modifiée, par exemple lorsque l'orientation de l'appareil change, ou lorsque l'application s'arrête et redémarre.
    Vous pouvez résoudre ce problème à l'aide du rappel onSaveInstanceState(). Toutefois, pour utiliser la méthode onSaveInstanceState(), vous devez écrire du code supplémentaire afin d'enregistrer l'état d'un groupe, mais aussi pour implémenter une logique permettant de récupérer cet état. De plus, la quantité de données pouvant être stockée est particulièrement réduite.
  • L'écran de jeu ne redirige pas vers l'écran de score lorsque l'utilisateur appuie sur le bouton Fin de partie.

Vous pouvez résoudre ces problèmes à l'aide des composants d'architecture d'application que vous avez découverts dans cet atelier de programmation.

Architecture de l'application

L'architecture d'application est une façon de concevoir les classes de vos applications et les relations entre elles, de sorte que le code soit organisé, fonctionne bien dans des scénarios particuliers et soit facile à utiliser. Dans cet ensemble de quatre ateliers de programmation, les améliorations que vous apportez à l'application GuessTheWord suivent les consignes de l'architecture des applications Android et utilisent les composants d'architecture Android. L'architecture de l'application Android est semblable au modèle architectural MVVM (Model-View-ViewModel).

L'application GuessTheWord suit le principe de conception de la séparation des tâches et est divisée en classes, chacune traitant d'un problème distinct. Dans ce premier atelier de programmation de la leçon, les classes avec lesquelles vous allez travailler sont un contrôleur d'UI, un ViewModel et un ViewModelFactory.

Contrôleur d'interface utilisateur

Un contrôleur d'UI est une classe basée sur l'UI, comme Activity ou Fragment. Un contrôleur d'UI ne doit contenir que la logique qui gère les interactions entre l'UI et le système d'exploitation, comme l'affichage des vues et la capture des saisies utilisateur. Ne placez pas de logique de prise de décision, comme la logique qui détermine le texte à afficher, dans le contrôleur d'UI.

Dans le code de démarrage de GuessTheWord, les contrôleurs d'UI sont les trois fragments : GameFragment, ScoreFragment, et TitleFragment. En suivant le principe de conception de la "séparation des préoccupations", GameFragment n'est responsable que du dessin des éléments du jeu à l'écran et de la détection des clics de l'utilisateur sur les boutons, et rien de plus. Lorsque l'utilisateur appuie sur un bouton, ces informations sont transmises à GameViewModel.

ViewModel

Un ViewModel contient les données à afficher dans un fragment ou une activité associés au ViewModel. Un ViewModel peut effectuer des calculs et des transformations simples sur les données afin que le contrôleur d'interface utilisateur puisse les afficher. Dans cette architecture, ViewModel prend les décisions.

GameViewModel contient des données telles que la valeur du score, la liste des mots et le mot actuel, car il s'agit des données à afficher à l'écran. Le GameViewModel contient également la logique métier permettant d'effectuer des calculs simples pour déterminer l'état actuel des données.

ViewModelFactory

Un ViewModelFactory instancie des objets ViewModel, avec ou sans paramètres de constructeur.

Dans les prochains ateliers de programmation, vous découvrirez d'autres composants d'architecture Android liés aux contrôleurs d'UI et à ViewModel.

La classe ViewModel est conçue pour stocker et gérer les données liées à l'UI. Dans cette application, chaque ViewModel est associé à un fragment.

Dans cette tâche, vous allez ajouter votre premier ViewModel à votre application, le GameViewModel pour le GameFragment. Vous découvrirez également ce que signifie le fait que ViewModel est sensible au cycle de vie.

Étape 1 : Ajoutez la classe GameViewModel

  1. Ouvrez le fichier build.gradle(module:app). Dans le bloc dependencies, ajoutez la dépendance Gradle pour ViewModel.

    Si vous utilisez la dernière version de la bibliothèque, l'application de solution devrait se compiler comme prévu. Si ce n'est pas le cas, essayez de résoudre le problème ou revenez à la version indiquée ci-dessous.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. Dans le dossier du package screens/game/, créez une classe Kotlin appelée GameViewModel.
  2. Configurez la classe GameViewModel pour qu'elle étende la classe abstraite ViewModel.
  3. Pour vous aider à mieux comprendre comment ViewModel est compatible avec le cycle de vie, ajoutez un bloc init avec une instruction log.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Étape 2 : Remplacez onCleared() et ajoutez une journalisation

Le ViewModel est détruit lorsque le fragment associé est dissocié ou lorsque l'activité se termine. Juste avant la destruction du ViewModel, le rappel onCleared() est utilisé pour nettoyer les ressources.

  1. Dans la classe GameViewModel, remplacez la méthode onCleared().
  2. Ajoutez une instruction de journalisation dans onCleared() pour suivre le cycle de vie de GameViewModel.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

Étape 3 : Associez GameViewModel au fragment de jeu

Un ViewModel doit être associé à un contrôleur d'UI. Pour les associer, vous devez créer une référence au ViewModel dans le contrôleur d'UI.

Au cours de cette étape, vous allez créer une référence de GameViewModel dans le contrôleur d'UI correspondant, soit GameFragment.

  1. Dans la classe GameFragment, ajoutez un champ de type GameViewModel au niveau supérieur en tant que variable de classe.
private lateinit var viewModel: GameViewModel

Étape 4 : Initialiser le ViewModel

Lors des changements de configuration, comme la rotation de l'écran, les contrôleurs d'UI tels que les fragments sont recréés. Toutefois, les instances ViewModel survivent. Si vous créez l'instance ViewModel à l'aide de la classe ViewModel, un nouvel objet est créé chaque fois que le fragment est recréé. Créez plutôt l'instance ViewModel à l'aide d'un ViewModelProvider.

Fonctionnement de ViewModelProvider :

  • ViewModelProvider renvoie un ViewModel existant, le cas échéant, ou en crée un s'il n'existe pas encore.
  • ViewModelProvider crée une instance ViewModel en association avec la portée donnée (une activité ou un fragment).
  • Le ViewModel créé est conservé tant que le champ d'application est actif. Par exemple, si le champ d'application est un fragment, le ViewModel est conservé jusqu'à ce que le fragment soit dissocié.

Initialisez ViewModel à l'aide de la méthode ViewModelProviders.of() pour créer un ViewModelProvider :

  1. Dans la classe GameFragment, initialisez la variable viewModel. Insérez ce code dans onCreateView(), après la définition de la variable de liaison. Utilisez la méthode ViewModelProviders.of() et transmettez le contexte GameFragment associé et la classe GameViewModel.
  2. Au-dessus de l'initialisation de l'objet ViewModel, ajoutez une instruction de journalisation pour enregistrer l'appel de la méthode ViewModelProviders.of().
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. Exécutez l'application. Dans Android Studio, ouvrez le volet Logcat et filtrez sur Game. Appuyez sur le bouton Play (Lecture) sur votre appareil ou votre émulateur. L'écran du jeu s'ouvre.

    Comme indiqué dans Logcat, la méthode onCreateView() de GameFragment appelle la méthode ViewModelProviders.of() pour créer GameViewModel. Les instructions de journalisation que vous avez ajoutées à GameFragment et GameViewModel s'affichent dans Logcat.

  1. Activez la rotation automatique sur votre appareil ou votre émulateur, puis modifiez l'orientation de l'écran plusieurs fois. GameFragment est détruit et recréé à chaque fois, donc ViewModelProviders.of() est appelé à chaque fois. Mais GameViewModel n'est créé qu'une seule fois, et il n'est pas recréé ni détruit à chaque appel.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. Quittez le jeu ou sortez du fragment de jeu. GameFragment est également détruit. Le GameViewModel associé est également détruit, et le rappel onCleared() est appelé.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

Le ViewModel survit aux modifications de configuration. Il est donc idéal pour les données qui doivent survivre à ces modifications :

  • Placez les données à afficher à l'écran et le code permettant de traiter ces données dans ViewModel.
  • Le ViewModel ne doit jamais contenir de références à des fragments, des activités ou des vues, car les activités, les fragments et les vues ne survivent pas aux modifications de configuration.

À titre de comparaison, voici comment les données de l'UI GameFragment sont gérées dans l'application de démarrage avant et après l'ajout de ViewModel :ViewModel

  • Avant d'ajouter ViewModel :
    Lorsque l'application subit un changement de configuration, comme une rotation de l'écran, le fragment de jeu est détruit et recréé. Les données sont perdues.
  • Après avoir ajouté ViewModel et déplacé les données d'UI du fragment de jeu dans ViewModel :
    Toutes les données dont le fragment a besoin pour s'afficher se trouvent désormais dans ViewModel. Lorsque la configuration de l'application est modifiée, ViewModel survit et les données sont conservées.

Dans cette tâche, vous allez déplacer les données de l'UI de l'application dans la classe GameViewModel, ainsi que les méthodes de traitement des données. Cela permet de conserver les données lors des modifications de configuration.

Étape 1 : Déplacez les champs de données et le traitement des données vers le ViewModel

Déplacez les champs de données et les méthodes suivants de GameFragment vers GameViewModel :

  1. Déplacez les champs de données word, score et wordList. Assurez-vous que word et score ne sont pas private.

    Ne déplacez pas la variable de liaison, GameFragmentBinding, car elle contient des références aux vues. Cette variable est utilisée pour développer la mise en page, configurer les écouteurs de clics et afficher les données à l'écran, qui sont les responsabilités du fragment.
  2. Déplacez les méthodes resetList() et nextWord(). Ces méthodes déterminent le mot à afficher à l'écran.
  3. Dans la méthode onCreateView(), déplacez les appels de méthode vers resetList() et nextWord() vers le bloc init de GameViewModel.

    Ces méthodes doivent se trouver dans le bloc init, car vous devez réinitialiser la liste de mots lorsque le ViewModel est créé, et non à chaque fois que le fragment est créé. Vous pouvez supprimer l'instruction de journalisation dans le bloc init de GameFragment.

Les gestionnaires de clics onSkip() et onCorrect() de GameFragment contiennent du code permettant de traiter les données et de mettre à jour l'UI. Le code permettant de mettre à jour l'UI doit rester dans le fragment, mais le code permettant de traiter les données doit être déplacé vers ViewModel.

Pour l'instant, placez les méthodes identiques aux deux endroits :

  1. Copiez les méthodes onSkip() et onCorrect() de GameFragment vers GameViewModel.
  2. Dans GameViewModel, assurez-vous que les méthodes onSkip() et onCorrect() ne sont pas private, car vous ferez référence à ces méthodes à partir du fragment.

Voici le code de la classe GameViewModel après refactoring :

class GameViewModel : ViewModel() {
   // The current word
   var word = ""
   // The current score
   var score = 0
   // The list of words - the front of the list is the next word to guess
   private lateinit var wordList: MutableList<String>

   /**
    * Resets the list of words and randomizes the order
    */
   private fun resetList() {
       wordList = mutableListOf(
               "queen",
               "hospital",
               "basketball",
               "cat",
               "change",
               "snail",
               "soup",
               "calendar",
               "sad",
               "desk",
               "guitar",
               "home",
               "railway",
               "zebra",
               "jelly",
               "car",
               "crow",
               "trade",
               "bag",
               "roll",
               "bubble"
       )
       wordList.shuffle()
   }

   init {
       resetList()
       nextWord()
       Log.i("GameViewModel", "GameViewModel created!")
   }
   /**
    * Moves to the next word in the list
    */
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           //Select and remove a word from the list
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
 /** Methods for buttons presses **/
   fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}

Voici le code de la classe GameFragment après refactoring :

/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {


   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {

       // Inflate view and obtain an instance of the binding class
       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProviders.of")
       viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }


   /** Methods for button click handlers **/

   private fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }


   /** Methods for updating the UI **/

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

Étape 2 : Mettez à jour les références aux gestionnaires de clics et aux champs de données dans GameFragment

  1. Dans GameFragment, mettez à jour les méthodes onSkip() et onCorrect(). Supprimez le code permettant de mettre à jour le score, puis appelez les méthodes onSkip() et onCorrect() correspondantes sur viewModel.
  2. Étant donné que vous avez déplacé la méthode nextWord() vers ViewModel, le fragment de jeu ne peut plus y accéder.

    Dans GameFragment, dans les méthodes onSkip() et onCorrect(), remplacez l'appel à nextWord() par updateScoreText() et updateWordText(). Ces méthodes affichent les données à l'écran.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. Dans GameFragment, mettez à jour les variables score et word pour utiliser les variables GameViewModel, car ces variables se trouvent désormais dans GameViewModel.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. Dans GameViewModel, à l'intérieur de la méthode nextWord(), supprimez les appels aux méthodes updateWordText() et updateScoreText(). Ces méthodes sont désormais appelées à partir de GameFragment.
  2. Compilez l'application et assurez-vous qu'elle ne comporte aucune erreur. Si vous rencontrez des erreurs, nettoyez et recompilez le projet.
  3. Exécutez l'application et essayez de deviner quelques mots. Faites pivoter l'appareil lorsque vous êtes sur l'écran du jeu. Notez que le score et le mot actuels sont conservés après le changement d'orientation.

Bravo ! Désormais, toutes les données de votre application sont stockées dans un ViewModel. Elles sont donc conservées lors des modifications de configuration.

Dans cette tâche, vous allez implémenter l'écouteur de clics pour le bouton End Game (Terminer la partie).

  1. Dans GameFragment, ajoutez une méthode appelée onEndGame(). La méthode onEndGame() sera appelée lorsque l'utilisateur appuiera sur le bouton End Game (Fin de partie).
private fun onEndGame() {
   }
  1. Dans GameFragment, à l'intérieur de la méthode onCreateView(), recherchez le code qui définit les écouteurs de clics pour les boutons Got It (OK) et Skip (Ignorer). Juste en dessous de ces deux lignes, définissez un écouteur de clics pour le bouton End Game (Fin de partie). Utilisez la variable de liaison binding. Dans l'écouteur de clics, appelez la méthode onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. Dans GameFragment, ajoutez une méthode appelée gameFinished() pour accéder à l'écran du score dans l'application. Transmettez le score en tant qu'argument à l'aide de Safe Args.
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. Dans la méthode onEndGame(), appelez la méthode gameFinished().
private fun onEndGame() {
   gameFinished()
}
  1. Exécutez l'application, jouez et parcourez quelques mots. Appuyez sur le bouton End Game (Terminer la partie). Notez que l'application accède à l'écran du score, mais que le score final n'est pas affiché. Vous résoudrez ce problème dans la prochaine tâche.

Lorsque l'utilisateur termine le jeu, ScoreFragment n'affiche pas le score. Vous souhaitez qu'un ViewModel contienne le score à afficher par le ScoreFragment. Vous transmettrez la valeur du score lors de l'initialisation de ViewModel à l'aide du modèle de méthode de fabrique.

Le modèle de méthode factory est un modèle de conception de création qui utilise des méthodes factory pour créer des objets. Une méthode de fabrique est une méthode qui renvoie une instance de la même classe.

Dans cette tâche, vous allez créer un ViewModel avec un constructeur paramétré pour le fragment de score et une méthode de fabrique pour instancier le ViewModel.

  1. Sous le package score, créez une classe Kotlin appelée ScoreViewModel. Cette classe sera le ViewModel du fragment de score.
  2. Étendez la classe ScoreViewModel à partir de ViewModel.. Ajoutez un paramètre de constructeur pour le score final. Ajoutez un bloc init avec une instruction de journalisation.
  3. Dans la classe ScoreViewModel, ajoutez une variable appelée score pour enregistrer le score final.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. Sous le package score, créez une autre classe Kotlin appelée ScoreViewModelFactory. Cette classe sera chargée d'instancier l'objet ScoreViewModel.
  2. Étendez la classe ScoreViewModelFactory depuis ViewModelProvider.Factory. Ajoutez un paramètre de constructeur pour le score final.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. Dans ScoreViewModelFactory, Android Studio affiche une erreur concernant un membre abstrait non implémenté. Pour résoudre l'erreur, remplacez la méthode create(). Dans la méthode create(), renvoyez l'objet ScoreViewModel nouvellement construit.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. Dans ScoreFragment, créez des variables de classe pour ScoreViewModel et ScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. Dans ScoreFragment, dans onCreateView(), après avoir initialisé la variable binding, initialisez viewModelFactory. Utilisez ScoreViewModelFactory. Transmettez le score final du bundle d'arguments en tant que paramètre de constructeur à ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. Dans onCreateView(, après avoir initialisé viewModelFactory, initialisez l'objet viewModel. Appelez la méthode ViewModelProviders.of(), transmettez le contexte du fragment de score associé et viewModelFactory. Cela créera l'objet ScoreViewModel à l'aide de la méthode de fabrique définie dans la classe viewModelFactory.
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. Dans la méthode onCreateView(), après avoir initialisé viewModel, définissez le texte de la vue scoreText sur le score final défini dans ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. Exécutez votre application et jouez. Parcourez certains ou tous les mots, puis appuyez sur Terminer la partie. Notez que le fragment de score affiche désormais le score final.

  1. Facultatif : Vérifiez les journaux ScoreViewModel dans Logcat en filtrant sur ScoreViewModel. La valeur du score doit s'afficher.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

Dans cette tâche, vous avez implémenté ScoreFragment pour utiliser ViewModel. Vous avez également appris à créer un constructeur paramétré pour un ViewModel à l'aide de l'interface ViewModelFactory.

Félicitations ! Vous avez modifié l'architecture de votre application pour utiliser l'un des composants d'architecture Android, ViewModel. Vous avez résolu le problème de cycle de vie de l'application, et les données du jeu survivent désormais aux modifications de configuration. Vous avez également appris à créer un constructeur paramétré pour créer un ViewModel à l'aide de l'interface ViewModelFactory.

Projet Android Studio : GuessTheWord

  • Les principes fondamentaux de l'architecture des applications Android recommandent de séparer les classes ayant des responsabilités distinctes.
  • Un contrôleur d'UI est une classe basée sur l'UI, comme Activity ou Fragment. Les contrôleurs d'UI ne doivent contenir que la logique qui gère les interactions entre l'UI et le système d'exploitation. Ils ne doivent pas contenir de données à afficher dans l'UI. Placez ces données dans un ViewModel.
  • La classe ViewModel stocke et gère les données liées à l'UI. La classe ViewModel permet aux données de survivre à des modifications de la configuration telles que les rotations d'écran.
  • ViewModel est l'un des composants d'architecture Android recommandés.
  • ViewModelProvider.Factory est une interface que vous pouvez utiliser pour créer un objet ViewModel.

Le tableau ci-dessous compare les contrôleurs d'UI avec les instances ViewModel qui contiennent leurs données :

Contrôleur d'UI

ViewModel

Le ScoreFragment que vous avez créé dans cet atelier de programmation est un exemple de contrôleur d'UI.

Un exemple de ViewModel est le ScoreViewModel que vous avez créé dans cet atelier de programmation.

Ne contient aucune donnée à afficher dans l'UI.

Contient les données que le contrôleur d'UI affiche dans l'UI.

Contient le code permettant d'afficher les données et le code d'événement utilisateur, tel que les écouteurs de clic.

Contient le code de traitement des données.

Détruit et recréé à chaque modification de la configuration.

Détruit uniquement lorsque le contrôleur d'UI associé disparaît définitivement (pour une activité, lorsque l'activité se termine, ou pour un fragment, lorsque le fragment est dissocié).

Contient des vues.

Ne doit jamais contenir de références à des activités, des fragments ou des vues, car ils ne survivent pas aux modifications de configuration, contrairement à ViewModel.

Contient une référence à l'ViewModel associé.

Ne contient aucune référence au contrôleur d'UI associé.

Cours Udacity :

Documentation pour les développeurs Android :

Autre :

Cette section répertorie les devoirs possibles pour les élèves qui suivent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. Il revient à l'enseignant d'effectuer les opérations suivantes :

  • Attribuer des devoirs si nécessaire
  • Indiquer aux élèves comment rendre leurs devoirs
  • Noter les devoirs

Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ne doivent pas hésiter à attribuer d'autres devoirs aux élèves s'ils le jugent nécessaire.

Si vous suivez cet atelier de programmation par vous-même, n'hésitez pas à utiliser ces devoirs pour tester vos connaissances.

Répondre aux questions suivantes

Question 1

Dans quelle classe devez-vous enregistrer les données de l'application pour éviter de perdre des données lorsque vous modifiez la configuration d'un appareil ?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Question 2

Un ViewModel ne doit jamais contenir de références à des fragments, des activités ou des vues. Vrai ou faux ?

  • Vrai
  • Faux

Question 3

À quel moment un ViewModel est-il détruit ?

  • Lorsque le contrôleur d'interface utilisateur associé est détruit et recréé lors d'un changement d'orientation de l'appareil.
  • Lors d'un changement d'orientation.
  • Lorsque le contrôleur d'interface utilisateur associé est terminé (s'il s'agit d'une activité) ou dissocié (s'il s'agit d'un fragment).
  • Lorsque l'utilisateur appuie sur le bouton "Retour".

Question 4

À quoi sert l'interface ViewModelFactory ?

  • Instanciation d'un objet ViewModel.
  • Conserver les données lors des changements d'orientation.
  • Actualiser les données affichées à l'écran.
  • Recevoir des notifications lorsque les données de l'application sont modifiées.

Passez à la leçon suivante : 5.2 : LiveData et observateurs LiveData

Pour obtenir des liens vers d'autres ateliers de programmation de ce cours, consultez la page de destination des ateliers de programmation Principes de base d'Android en Kotlin.