Android Kotlin Fundamentals 05.1: ViewModel et ViewModelFactory

Cet atelier de programmation fait partie du cours Android Kotlin Fundamentals. Vous tirerez le meilleur parti de ce cours si vous suivez les ateliers de programmation dans l'ordre. Tous les ateliers de programmation du cours sont répertoriés sur la page de destination des ateliers de programmation Android Kotlin Fundamentals.

É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:

  • La classe ViewModel vous permet de stocker et de gérer les données liées à l'interface utilisateur en tenant compte de la notion de 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 apportées à 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

  • Créer des applications Android de base en Kotlin
  • Utiliser le graphique de navigation pour mettre en œuvre la navigation dans votre application
  • Comment ajouter du code pour naviguer entre les destinations de votre application et transmettre des données entre les destinations de navigation
  • Fonctionnement des cycles de vie des activités et des fragments
  • Ajouter des informations de journalisation à une application et lire les journaux à l'aide de Logcat dans Android Studio

Points abordés

Objectifs de l'atelier

  • Ajoutez un ViewModel à l'application pour enregistrer les données de l'application et les conserver après les modifications de configuration.
  • Utilisez ViewModelFactory et le modèle de conception avec la méthode par défaut pour instancier un objet ViewModel avec des paramètres de constructeur.

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

Le premier joueur analyse les mots dans l'application et les joue tous à tour de rôle. Veillez donc à ne pas afficher le mot au deuxième joueur. Le deuxième joueur essaie de deviner le mot.

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

Le premier joueur joue le mot, en prenant soin de ne pas le prononcer.

  • Lorsque le deuxième joueur essaie de deviner le mot, le premier appuie sur le bouton OK, ce qui augmente le nombre de points et affiche le mot suivant.
  • S'il ne parvient pas à deviner le mot, le premier joueur appuie sur le bouton Ignorer, ce qui diminue le nombre de fois jusqu'à l'affichage du mot suivant.
  • Pour terminer la partie, appuyez sur le bouton Terminer le jeu. (Cette fonctionnalité n'est pas disponible dans le code de démarrage du premier atelier de programmation de cette série.)

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

Étape 1: Premiers pas

  1. Téléchargez le code de démarrage GuessTheWord, puis ouvrez le projet dans Android Studio.
  2. Exécutez l'application sur un appareil Android ou un émulateur.
  3. Appuyez sur les boutons. Notez que le bouton Ignorer affiche le mot suivant et diminue le score de 1. Le bouton OK permet d'afficher le mot suivant et d'augmenter le score de 1. Le bouton Mettre fin au jeu n'est pas implémenté. Si vous appuyez dessus, rien ne se passe.

Étape 2: Tutoriel pour le code

  1. Dans Android Studio, explorez le code pour vous faire une idée du fonctionnement de l'application.
  2. Veillez à consulter les fichiers décrits ci-dessous, particulièrement importants.

MainActivity.kt

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

res/layout/main_activity.xml

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

Fragments d'interface utilisateur

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

screen/title/TitreFragment.kt

Le fragment de titre est le premier écran affiché lorsque l'application est lancée. Un gestionnaire de clics est défini sur le bouton Jouer pour accéder à l'écran du jeu.

screen/game/GameFragment.kt

Principal fragment, dans lequel la plupart des actions du jeu ont lieu:

  • Les variables sont définies pour le mot actuel et le score actuel.
  • La wordList définie 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 de 1, puis affiche le mot suivant à l'aide de la méthode nextWord().
  • La méthode onCorrect() correspond au gestionnaire de clics du bouton Got It (OK). 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 soustraire.

écrans/score/FragmentFragment.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 afficher le score final.

res/navigation/main_navigation.xml

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

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

Dans cette tâche, vous allez trouver des problèmes avec l'application de départ GuessTheWord.

  1. Exécutez le code de démarrage et jouez en 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 ou l'émulateur. Vous remarquerez que le score actuel est perdu.
  3. Exécutez le jeu avec quelques mots supplémentaires. Lorsque l'écran du jeu s'affiche, fermez l'application, puis rouvrez-la. Notez que le jeu redémarre depuis le début, car l'état de l'application n'est pas enregistré.
  4. Jouez en quelques mots, puis appuyez sur le bouton End Game (Terminer le jeu). Notez qu'il ne se passe rien.

Problèmes dans l'application:

  • L'application de départ n'enregistre pas et ne restaure l'état d'une application lors de changements de configuration, 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(). Cependant, l'utilisation de la méthode onSaveInstanceState() nécessite d'écrire du code supplémentaire pour enregistrer l'état dans un groupe et de mettre en œuvre la logique permettant de récupérer cet état. De plus, la quantité de données pouvant être stockées est minimale.
  • L'écran de jeu ne s'affiche pas lorsque l'utilisateur appuie sur le bouton End Game (Terminer le jeu).

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

Architecture de l'application

L'architecture des applications est un moyen de concevoir vos applications et leurs classes, ainsi que les relations entre elles, de sorte que le code soit organisé, qu'il fonctionne bien dans des scénarios particuliers et qu'il soit facile à utiliser. Dans cet ensemble de quatre ateliers de programmation, les améliorations que vous apportez à l'application GuessTheWord respectent les consignes de l'architecture de l'application Android, et vous utilisez les composants d'architecture Android. L'architecture de l'application Android est semblable au modèle architectural de MVVM (model-view-viewmodel).

L'application GuessTheWord suit le principe de séparation des préoccupations et est divisée en classes, chaque classe étant traitée selon une préoccupation distincte. Dans ce premier atelier de programmation, vous allez utiliser un contrôleur d'interface utilisateur, un ViewModel et une ViewModelFactory.

contrôleur d'interface utilisateur

Un contrôleur d'interface utilisateur est une classe basée sur une interface utilisateur telle que Activity ou Fragment. Un contrôleur d'interface utilisateur ne doit contenir que la logique qui gère les interactions de l'interface utilisateur et du système d'exploitation telles que l'affichage des vues et la saisie des entrées utilisateur. N'utilisez pas de logique de prise de décision, comme celle qui détermine le texte à afficher, dans le contrôleur d'UI.

Dans le code de démarrage de GuessTheWord, les contrôleurs d'interface utilisateur sont les trois fragments: GameFragment, ScoreFragment, et TitleFragment. En suivant le principe de séparation des préoccupations, le GameFragment est uniquement chargé de dessiner les éléments du jeu à l'écran et de savoir quand l'utilisateur appuie sur les boutons, et rien de plus. Lorsque l'utilisateur appuie sur un bouton, ces informations sont transmises au GameViewModel.

ViewModel

Un ViewModel contient des données à afficher dans un fragment ou une activité associée au ViewModel. Un ViewModel peut effectuer des calculs et des transformations simples sur les données pour préparer les données à afficher par le contrôleur de l'interface utilisateur. Dans cette architecture, le ViewModel prend les décisions.

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

ViewModelFactory

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

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

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

Dans cette tâche, vous allez ajouter les premières ViewModel de l'application (GameViewModel) pour les GameFragment. Vous en apprendrez également plus sur la signification de ViewModel.

Étape 1: Ajouter 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 doit se compiler comme prévu. Si le problème persiste, essayez de résoudre le problème ou revenez à la version ci-dessous.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. Dans le dossier screens/game/ du package, créez une classe Kotlin appelée GameViewModel.
  2. Faites en sorte que la classe GameViewModel étend la classe abstraite ViewModel.
  3. Pour mieux comprendre comment ViewModel tient compte du cycle de vie, ajoutez un bloc init avec une instruction log.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Étape 2: Remplacez la règle onCleared() et ajoutez une journalisation

Le ViewModel est détruit lorsque le fragment associé est dissocié ou lorsque l'activité est terminée. Juste avant la destruction de ViewModel, le rappel onCleared() est appelé pour nettoyer les ressources.

  1. Dans la classe GameViewModel, ignorez 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 associer les deux, vous devez créer une référence à ViewModel dans le contrôleur d'interface utilisateur.

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

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

Étape 4: Initialisez le ViewModel

Lors de modifications de la configuration telles que les rotations d'écran, les contrôleurs d'interface tels que les fragments sont recréés. Cependant, les instances ViewModel survivent. Si vous créez l'instance ViewModel à l'aide de la classe ViewModel, un objet est créé à chaque recréation du fragment. Créez plutôt l'instance ViewModel à l'aide d'un ViewModelProvider.

Fonctionnement de ViewModelProvider:

  • ViewModelProvider renvoie un objet ViewModel existant s'il en existe un, ou s'il en existe un autre.
  • ViewModelProvider crée une instance ViewModel associée au champ d'application donné (activité ou fragment).
  • Le ViewModel créé est conservé tant que le champ d'application est actif. Par exemple, si la portée est un fragment, le ViewModel est conservé jusqu'à ce qu'il soit dissocié.

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

  1. Dans la classe GameFragment, initialisez la variable viewModel. Placez ce code dans onCreateView(), après la définition de la variable de liaison. Utilisez la méthode ViewModelProviders.of(), puis 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 consigner l'appel de 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 appliquez un filtre sur Game. Appuyez sur le bouton Lire sur votre appareil ou votre émulateur. L'écran du jeu s'ouvre.

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

  1. Activez le paramètre de 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. ViewModelProviders.of() est donc appelé à chaque fois. Cependant, GameViewModel n'est créé qu'une seule fois, et n'est pas recréé ni détruit pour 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 quittez-le. Le GameFragment est 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 s'agit donc d'un bon emplacement pour les données devant survivre aux modifications de configuration:

  • Placez les données à afficher et le code pour 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'interface utilisateur GameFragment sont gérées dans l'application de départ avant d'ajouter ViewModel et après ViewModel:

  • Avant d'ajouter ViewModel,
    lorsque l'application subit une modification de configuration telle qu'une rotation de l'écran, le fragment de jeu est détruit, puis recréé. Les données sont perdues.
  • Après avoir ajouté ViewModel et déplacé les données d'interface utilisateur du fragment de jeu dans ViewModel,
    toutes les données que le fragment doit afficher sont désormais ViewModel. Lorsque l'application subit une modification de configuration, le ViewModel persiste et les données sont conservées.

Dans cette tâche, vous allez déplacer les données de l'interface utilisateur de l'application dans la classe GameViewModel, ainsi que les méthodes de traitement des données. Ainsi, les données sont conservées en cas de modification de la configuration.

Étape 1: Déplacer les champs 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 GameFragmentBinding, car elle contient des références aux vues. Elle permet de gonfler la mise en page, de configurer les écouteurs de clics et d'afficher les données à l'écran, c'est-à-dire 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 lors de la création de ViewModel, et non chaque fois que le fragment est créé. Vous pouvez supprimer l'instruction de journal dans le bloc init de GameFragment.

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

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

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

Voici le code pour la classe GameViewModel, après refactorisation:

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 pour la classe GameFragment, après refactorisation:

/**
* 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 pour mettre à jour le score, et 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 de 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 elles 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 le GameViewModel, dans la méthode nextWord(), supprimez les appels aux méthodes updateWordText() et updateScoreText(). Ces méthodes sont maintenant appelées depuis GameFragment.
  2. Créez l'application et assurez-vous qu'elle ne contient pas d'erreurs. Si vous rencontrez des erreurs, nettoyez et recréez le projet.
  3. Exécutez l'application et jouez en utilisant quelques mots. Sur l'écran du jeu, faites pivoter l'appareil. Notez que le score actuel et le mot actuel sont conservés après le changement d'orientation.

Bravo ! Toutes les données de votre application sont désormais 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 le jeu).

  1. Dans GameFragment, ajoutez une méthode appelée onEndGame(). La méthode onEndGame() est appelée lorsque l'utilisateur appuie sur le bouton End Game (Terminer le jeu).
private fun onEndGame() {
   }
  1. Dans GameFragment, à l'intérieur de la méthode onCreateView(), localisez le code qui définit les écouteurs de clics pour les boutons OK et Passer. Juste en dessous de ces deux lignes, définissez un écouteur de clics pour le bouton End Game (Terminer le jeu). 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 de score. 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 au jeu et faites défiler les mots. Appuyez sur le bouton Terminer le jeu. Notez que l'application accède à l'écran de score, mais que le score final ne s'affiche pas. Vous allez résoudre ce problème à la tâche suivante.

Lorsque l'utilisateur termine le jeu, le ScoreFragment n'affiche pas le score. Vous souhaitez qu'une ViewModel tienne le score affiché par le ScoreFragment. Vous transmettrez la valeur de score lors de l'initialisation de ViewModel à l'aide du modèle de méthode d'usine.

Le modèle de méthode de fabrique est un modèle de conception créative qui utilise des méthodes de fabrique pour créer des objets. Une méthode d'usine 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 ViewModel.

  1. Sous le package score, créez une classe Kotlin appelée ScoreViewModel. Cette classe sera le ViewModel du fragment de score.
  2. Prolongez la classe ScoreViewModel de ViewModel. Ajoutez un paramètre 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. Prolongez la classe ScoreViewModelFactory de ViewModelProvider.Factory. Ajoutez un paramètre 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, ignorez la méthode create(). Dans la méthode create(), renvoyez l'objet ScoreViewModel qui vient d'être 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 la propriété ScoreViewModelFactory. Transmettez le score final du groupe d'arguments en tant que paramètre constructeur à ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. Dans onCreateView(, initialisez viewModelFactory, puis initialisez l'objet viewModel. Appelez la méthode ViewModelProviders.of(), transmettez le contexte du fragment de score associé et viewModelFactory. Cette commande crée l'objet ScoreViewModel à l'aide de la méthode d'usine 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 le ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. Exécutez votre application et jouez au jeu. Faites défiler une partie ou la totalité des mots, puis appuyez sur Arrêter le jeu. Notez que le fragment de score affiche maintenant le score final.

  1. Facultatif: Vérifiez les journaux ScoreViewModel dans le fichier logcat en filtrant sur ScoreViewModel. La valeur du score doit être affichée.
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 ViewModel en utilisant 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. Désormais, les données du jeu survivent aux modifications de configuration. Vous avez également appris à créer un constructeur paramétré pour créer une ViewModel à l'aide de l'interface ViewModelFactory.

Projet Android Studio: GuessTheWord

  • Les consignes de l'architecture des applications Android recommandent de séparer les classes qui ont des responsabilités différentes.
  • Un contrôleur d'interface utilisateur est une classe basée sur une interface utilisateur telle que Activity ou Fragment. Les contrôleurs UI ne doivent contenir que la logique qui gère les interactions avec l'interface utilisateur et le système d'exploitation. Ils ne doivent pas contenir de données à afficher dans l'interface utilisateur. Placez ces données dans un ViewModel.
  • La classe ViewModel stocke et gère les données liées à l'interface utilisateur. La classe ViewModel permet aux données de survivre aux modifications de 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'interface utilisateur aux instances ViewModel qui contiennent des données:

Contrôleur d'interface utilisateur

ViewModel

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

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

ne contient pas de données à afficher dans l'interface utilisateur.

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

Contient le code permettant d'afficher les données, ainsi que le code des événements utilisateur, tels que les écouteurs de clics.

Contient du code pour le traitement des données.

détruite et recréée à chaque modification de la configuration ;

Elle n'est détruite que lorsque le contrôleur de l'interface utilisateur disparaît définitivement, pour une activité, lorsqu'elle se termine ou pour un fragment détaché.

Contient des vues.

Ne doit jamais contenir de références à des activités, des fragments ou des vues, car ces éléments ne peuvent pas survivre aux modifications de configuration, contrairement à la fonction ViewModel.

Contient une référence au ViewModel associé.

ne contient aucune référence au contrôleur de l'interface utilisateur associé.

Cours Udacity:

Documentation pour les développeurs Android:

Autre :

Cette section répertorie les devoirs possibles pour les élèves qui effectuent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. C'est à l'enseignant de procéder comme suit:

  • Si nécessaire, rendez-les.
  • Communiquez aux élèves comment rendre leurs devoirs.
  • Notez les devoirs.

Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ils n'ont pas besoin d'attribuer les devoirs de leur choix.

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

Répondez à ces questions.

Question 1

Pour éviter de perdre des données lors d'une modification de la configuration de l'appareil, dans quelle classe devez-vous enregistrer les données de l'application ?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Question 2

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

  • Vrai
  • Faux

Question 3

Quand un ViewModel est-il détruit ?

  • Lorsque le contrôleur d'interface utilisateur associé est détruit et recréé en cas de 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 qu'il est dissocié (s'il s'agit d'un fragment).
  • Lorsque l'utilisateur appuie sur le bouton "Retour".

Question 4

À quoi sert l'interface ViewModelFactory ?

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

Démarrez la leçon suivante: 5.2: Observateurs LiveData et LiveData.

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