Stocker des données dans ViewModel

1. Avant de commencer

Dans les précédents ateliers de programmation, vous avez appris le cycle de vie des activités et des fragments, ainsi que les problèmes qui peuvent survenir lors des modifications de la configuration. Pour enregistrer les données de l'application, vous pouvez enregistrer l'état de l'instance, mais cette option n'est pas idéale. Dans cet atelier de programmation, vous allez découvrir une méthode efficace pour concevoir votre application et préserver ses données en cas de modification de la configuration grâce aux bibliothèques Android Jetpack.

Les bibliothèques Android Jetpack vous permettent de développer plus facilement des applications Android performantes. Elles vous aident à suivre les bonnes pratiques, vous évitent d'avoir à écrire du code récurrent et simplifient les tâches complexes. Vous pouvez ainsi vous concentrer sur le code qui fait la différence, comme la logique de l'application.

Les composants d'architecture Android, qui font partie des bibliothèques Android Jetpack, vous aident à concevoir des applications disposant d'une architecture robuste. Non seulement ils permettent de bénéficier de conseils sur l'architecture des applications, il s'agit de la bonne pratique recommandée.

L'architecture d'une application constitue un ensemble de règles de conception. Tout comme le plan d'une maison, votre architecture fournit la structure nécessaire à votre application. Une architecture performante peut rendre votre code robuste, flexible, évolutif et facile à gérer au cours des années à venir.

Dans cet atelier de programmation, vous allez apprendre à utiliser l'un de ces composants d'architecture, ViewModel, pour stocker les données de votre application. Si le framework détruit et recrée les activités et les fragments lors d'une modification de configuration, ou lorsque d'autres événements surviennent, les données stockées ne sont pas perdues.

Conditions préalables

  • Vous savez comment télécharger le code source depuis GitHub et l'ouvrir dans Android Studio.
  • Vous savez comment créer et exécuter une application Android de base en langage Kotlin, à l'aide d'activités et de fragments.
  • Vous savez utiliser les champs de texte Material et des widgets d'interface utilisateur courants, tels que TextView et Button.
  • Vous savez utiliser la liaison de vue dans une application.
  • Vous maîtrisez les principes de base du cycle 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

  • Présentation des principes de base de l'architecture des applications Android
  • Comment utiliser la classe ViewModel dans votre application
  • Vous savez comment conserver les données de l'interface utilisateur en modifiant la configuration de l'appareil à l'aide de ViewModel.
  • Les propriétés de sauvegarde en Kotlin
  • Comment utiliser l'élément MaterialAlertDialog, un composant de la bibliothèque Material Design

Objectifs de l'atelier

  • Créer une application de jeu Unscramble, dans laquelle l'utilisateur doit retrouver un mot à partir de lettres mélangées.

Ce dont vous avez besoin

  • Un ordinateur sur lequel est installé Android Studio
  • Le code de démarrage de l'application Unscramble.

2. Présentation de l'application de démarrage

Présentation du jeu

L'application Unscramble est un jeu solo qui repose sur le principe des anagrammes. L'application affiche une série de lettres, et le joueur doit trouver le mot en utilisant toutes les lettres. Si le mot est correct, le joueur marque des points. Dans le cas contraire, il peut tenter sa chance à nouveau. L'application propose également une option permettant d'ignorer un mot. Dans le coin supérieur gauche, l'application affiche le nombre de mots joués au cours de la partie. Il y a 10 mots par partie.

8edd6191a40a57e1.png 992bf57f066caf49.png b82a9817b5ec4d11.png

Télécharger le code de démarrage

Cet atelier de programmation fournit un code de démarrage que vous pouvez étendre avec les fonctionnalités qui y sont enseignées. Le code de démarrage peut contenir du code que vous avez déjà vu, ou non, dans les ateliers de programmation précédents. Vous en apprendrez davantage sur le code que vous ne connaissez pas dans les prochains ateliers de programmation.

Si vous utilisez le code de démarrage de GitHub, notez que le nom du dossier est android-basics-kotlin-unscramble-app-starter. Sélectionnez ce dossier lorsque vous ouvrez le projet dans Android Studio.

  1. Accédez à la page du dépôt GitHub fournie pour le projet.
  2. Vérifiez que le nom de la branche correspond à celui spécifié dans l'atelier de programmation. Par exemple, dans la capture d'écran suivante, le nom de la branche est main.

1e4c0d2c081a8fd2.png

  1. Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une fenêtre pop-up.

1debcf330fd04c7b.png

  1. Dans la fenêtre pop-up, cliquez sur le bouton Download ZIP (Télécharger le fichier ZIP) pour enregistrer le projet sur votre ordinateur. Attendez la fin du téléchargement.
  2. Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
  3. Double-cliquez sur le fichier ZIP pour le décompresser. Un dossier contenant les fichiers du projet est alors créé.

Ouvrir le projet dans Android Studio

  1. Lancez Android Studio.
  2. Dans la fenêtre Welcome to Android Studio (Bienvenue dans Android Studio), cliquez sur Open (Ouvrir).

d8e9dbdeafe9038a.png

Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > Open (Fichier > Ouvrir).

8d1fda7396afe8e5.png

  1. Dans l'explorateur de fichiers, accédez à l'emplacement du dossier du projet décompressé (il se trouve probablement dans le dossier Téléchargements).
  2. Double-cliquez sur le dossier de ce projet.
  3. Attendez qu'Android Studio ouvre le projet.
  4. Cliquez sur le bouton Run (Exécuter) 8de56cba7583251f.png pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.

Présentation du code de démarrage

  1. Dans Android Studio, ouvrez le projet contenant le code de démarrage.
  2. Exécutez l'application sur un appareil Android ou sur un émulateur.
  3. Jouez avec quelques mots en utilisant les boutons Confirmer et Passer. Notez que le fait d'appuyer sur ces boutons affiche le mot suivant et incrémente le nombre de mots en haut à gauche.
  4. Notez que le score n'augmente que si vous appuyez sur le bouton Confirmer.

Problèmes avec le code de démarrage

En jouant, vous avez peut-être remarqué les bugs suivants :

  1. Lorsque vous cliquez sur le bouton Confirmer, l'application ne vérifie pas le mot. Le joueur gagne des points à chaque fois.
  2. Il n'y a aucun moyen de mettre fin au jeu. L'application vous permet de continuer à jouer au-delà de 10 mots.
  3. L'écran du jeu affiche les lettres à réorganiser, le score du joueur et le nombre de mots. 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 les lettres à réorganiser, le score et le nombre de mots actuels sont perdus, et que le jeu redémarre depuis le début.

Principaux problèmes de l'application

L'application de démarrage n'enregistre ni l'état, ni les données de l'application et ne les restaure pas lorsque la configuration est modifiée, par exemple lorsque l'orientation de l'appareil change.

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.

Vous pouvez résoudre ces problèmes à l'aide des composants d'architecture Android que vous avez découverts tout au long de ce parcours de formation.

Explication du code de démarrage

Le code de démarrage que vous avez téléchargé dispose déjà d'une mise en page. Dans ce parcours, vous allez vous concentrer sur la mise en œuvre de la logique du jeu. Vous utiliserez des composants d'architecture pour implémenter la structure d'application recommandée et résoudre les problèmes mentionnés ci-dessus. Voici une présentation rapide de certains fichiers pour vous aider à commencer.

game_fragment.xml

  • Ouvrez res/layout/game_fragment.xml dans la vue Conception.
  • Ce fichier contient la mise en page du seul écran de votre application, à savoir l'écran de jeu.
  • Cette mise en page contient un champ de texte pour le mot, ainsi que des TextViews pour afficher le score et le nombre de mots. Elle contient également des instructions et les boutons Confirmer et Passer pour jouer.

main_activity.xml

Ce fichier définit la mise en page de l'activité principale avec un seul fragment, qui correspond au jeu.

Dossier res/values

Vous connaissez les fichiers de ressources de ce dossier.

  • colors.xml contient les couleurs du thème utilisées dans l'application.
  • strings.xml contient toutes les chaînes dont votre application a besoin.
  • Les dossiers themes et styles contiennent les éléments de personnalisation de l'interface utilisateur pour votre application.

MainActivity.kt

Ce fichier contient le code par défaut généré par le modèle pour définir la vue de contenu de l'activité en tant que main_activity.xml.

ListOfWords.kt

Ce fichier contient la liste des mots utilisés dans le jeu, ainsi que des constantes pour le nombre maximal de mots par partie et le nombre de points gagnés par le joueur pour chaque mot trouvé.

GameFragment.kt

Il s'agit du seul fragment de votre application, dans lequel la plupart des actions du jeu se déroulent :

  • Des variables sont définies pour les lettres à réorganiser (currentScrambledWord), le nombre de mots (currentWordCount) et le score (score).
  • Une instance d'objet de liaison appelée binding et ayant accès aux vues game_fragment est définie.
  • La fonction onCreateView() gonfle le code XML de mise en page game_fragment à l'aide de l'objet de liaison.
  • La fonction onViewCreated() configure les écouteurs de clics pour les boutons et met à jour l'interface utilisateur.
  • onSubmitWord() est l'écouteur de clics du bouton Submit (Envoyer). Cette fonction affiche la série de lettres suivante, efface le champ de texte et augmente le score et le nombre de mots sans valider le mot proposé par le joueur.
  • onSkipWord() est l'écouteur de clics du bouton Skip (Ignorer). Cette fonction met à jour l'UI de la même manière que onSubmitWord(), à l'exception du score.
  • getNextScrambledWord() est une fonction d'assistance qui sélectionne un mot aléatoire dans la liste fournie et brasse les lettres qu'il contient.
  • Les fonctions restartGame() et exitGame() permettent respectivement de redémarrer et d'arrêter le jeu. Vous les utiliserez plus tard.
  • setErrorTextField() efface le contenu du champ de texte et réinitialise l'état d'erreur.
  • updateNextWordOnScreen() affiche la série de lettres suivante.

3. Notions fondamentales sur l'architecture des applications

L'architecture fournit les principes directeurs qui vous permettent de répartir les responsabilités dans votre application entre les différentes classes. Une architecture d'application performante vous permet de faire évoluer votre projet et d'y ajouter des fonctionnalités supplémentaires par la suite. Elle facilite également la collaboration, si vous travaillez en équipe.

Les principes fondamentaux de l'architecture des applications sont la séparation des tâches et le contrôle de l'interface utilisateur à partir d'un modèle.

Séparation des tâches

Selon le principe de conception de séparation des tâches, l'application doit être divisée en classes, chacune ayant des responsabilités distinctes.

Contrôle de l'UI à partir d'un modèle

Un autre principe important est que vous devez contrôler votre UI à l'aide d'un modèle, de préférence un modèle persistant. Les modèles sont des composants chargés de gérer les données d'une application. Ils sont indépendants des Views et des composants de votre application et ne sont donc pas concernés par le cycle de vie de l'application, ni par les préoccupations qui en découlent.

Les classes ou composants principaux de l'architecture Android sont les contrôleurs d'UI (activités et fragments), ViewModel, LiveData et Room. Ces composants gèrent une partie de la complexité du cycle de vie et vous aident à éviter les problèmes liés à celui-ci. Vous découvrirez davantage d'informations sur LiveData et Room dans les ateliers de programmation suivants.

Ce schéma présente une partie de l'architecture :

597074ed0d08947b.png

Contrôleurs d'interface utilisateur (activités et fragments)

Les activités et les fragments sont des contrôleurs d'interface utilisateur. Ils permettent de créer des vues qui s'affichent à l'écran, de capturer les événements utilisateur et toute autre action liée à l'interface avec laquelle l'utilisateur interagit. Les données de l'application et toute logique de prise de décision concernant ces mêmes données ne doivent pas figurer dans ces classes.

Le système Android peut détruire les contrôleurs d'UI à tout moment en fonction de certaines interactions des utilisateurs ou en raison de conditions système, comme une mémoire insuffisante. Comme ces événements ne sont pas sous votre contrôle, vous ne devez pas stocker de données ni d'états de l'application dans ces contrôleurs. Au lieu de cela, vous devez ajouter la logique de prise de décision concernant les données dans votre ViewModel.

Par exemple, dans votre application Unscramble, la série de lettres à réorganiser, le score et le nombre de mots sont affichés dans un fragment (qui est un contrôleur d'UI). Le code de prise de décision, pour sa part, doit figurer dans votre ViewModel, par exemple ce qui permet de choisir la série de lettres suivante et de calculer le score ainsi que le nombre de mots.

ViewModel

Le ViewModel est un modèle des données d'application affiché dans des vues. Les modèles sont des composants chargés de gérer les données d'une application. Ils vous permettent de suivre l'un des principes fondamentaux de l'architecture en pilotant l'UI à partir d'un modèle.

Le ViewModel stocke les données liées à l'application qui ne sont pas détruites lorsque l'activité ou le fragment sont détruits et recréés par le framework Android. Les objets ViewModel sont automatiquement conservés (ils ne sont pas détruits comme l'activité ou une instance de fragment) lorsque la configuration change. Ainsi, les données qu'ils contiennent sont immédiatement disponibles pour l'activité ou l'instance de fragment suivante.

Pour implémenter ViewModel dans votre application, étendez la classe ViewModel, qui provient de la bibliothèque de composants d'architecture, et utilisez-la pour stocker les données de l'application.

En résumé :

Responsabilités associées aux fragments et aux activités (contrôleurs d'UI)

Responsabilités relevant de ViewModel

Les activités et les fragments sont responsables de l'affichage des vues et des données à l'écran. Ils doivent également réagir aux événements utilisateur.

ViewModel est responsable de la conservation et du traitement de toutes les données nécessaires au fonctionnement de l'UI. Cet élément ne doit jamais accéder à votre hiérarchie de vues (comme l'objet de liaison des vues) ni contenir de référence à une activité ou à un fragment.

4. Ajouter un ViewModel

Dans cette tâche, vous allez ajouter un ViewModel à votre application pour y stocker vos données (série de lettres à réorganiser, nombre de mots et score).

L'architecture de votre application se présentera comme suit : MainActivity contient un GameFragment qui accède aux informations du jeu à partir du GameViewModel.

2b29a13dde3481c3.png

  1. Dans la fenêtre Android d'Android Studio, sous le dossier Scripts Gradle, ouvrez le fichier build.gradle(Module:Unscramble.app).
  2. Pour utiliser un ViewModel dans votre application, vérifiez que la dépendance de la bibliothèque ViewModel est présente dans le bloc dependencies. Cette étape a déjà été effectuée pour vous. En fonction de la dernière version de la bibliothèque, le numéro dans le code généré peut être différent.
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'

Il est recommandé de toujours utiliser la dernière version de la bibliothèque, même si cet atelier de programmation en utilise une autre.

  1. Créez un fichier de classe Kotlin appelé GameViewModel. Dans la fenêtre Android, effectuez un clic droit sur le dossier ui.game. Sélectionnez New > Kotlin File/Class (Nouveau > Fichier/Classe Kotlin).

d48361a4f73d4acb.png

  1. Donnez-lui le nom GameViewModel, puis sélectionnez Classe dans la liste.
  2. Modifiez GameViewModel pour en faire une sous-classe de ViewModel. ViewModel étant une classe abstraite, vous devez l'étendre avant de pouvoir l'utiliser dans votre application. Prenez exemple sur la définition de la classe GameViewModel ci-dessous.
class GameViewModel : ViewModel() {
}

Associer le ViewModel au fragment

Pour associer un ViewModel à un contrôleur d'UI (activité ou fragment), créez une référence (objet) au ViewModel dans le contrôleur.

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

  1. En haut de la classe GameFragment, ajoutez une propriété de type GameViewModel.
  2. Initialisez GameViewModel à l'aide du délégué de propriété Kotlin by viewModels(). La section suivante expliquera le fonctionnement plus en détail.
private val viewModel: GameViewModel by viewModels()
  1. Importez androidx.fragment.app.viewModels, si Android Studio vous le demande.

Délégué de propriété Kotlin

En Kotlin, chaque propriété modifiable (var) dispose automatiquement de fonctions "getter" et "setter". Ces fonctions sont appelées lorsque vous attribuez une valeur à la propriété ou la lisez.

Pour une propriété en lecture seule (val), le fonctionnement est légèrement différent d'une propriété modifiable. Seule la fonction "getter" est générée par défaut. Cette fonction est appelée lorsque vous lisez la valeur d'une propriété en lecture seule.

La délégation de propriété en Kotlin vous aide à transférer la responsabilité "getter-setter" à une autre classe.

Cette classe (appelée classe déléguée) fournit les fonctions getter et setter de la propriété et gère ses modifications.

Un délégué de propriété est défini à l'aide de la clause by et d'une instance de classe déléguée :

// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()

Dans votre application, si vous initialisez le modèle de vue à l'aide du constructeur GameViewModel par défaut, comme ci-dessous :

private val viewModel = GameViewModel()

Alors l'application perdra l'état de la référence viewModel dès lors que la configuration de l'appareil sera modifiée. Par exemple, si vous faites pivoter l'appareil, l'activité est détruite, puis recréée. Vous disposez alors d'une nouvelle instance de modèle de vue, qui affiche à nouveau l'état initial.

Utilisez plutôt la délégation de propriété pour transférer la responsabilité de l'objet viewModel à une classe distincte appelée viewModels. En d'autres termes, lorsque vous accédez à l'objet viewModel, il est géré en interne par la classe déléguée, viewModels. Elle crée l'objet viewModel lors du premier accès, puis conserve sa valeur si la configuration est modifiée avant de la renvoyer sur demande.

5. Déplacer des données vers le ViewModel

En séparant les données d'UI de votre contrôleur (vos classes Activity et Fragment), vous pouvez bénéficier d'un meilleur suivi du principe de responsabilité unique que nous avons détaillé ci-dessus. Vos activités et fragments sont responsables de l'affichage des vues et des données à l'écran, tandis que votre ViewModel est chargé de conserver et de traiter toutes les données nécessaires au fonctionnement de l'UI.

Dans cette tâche, vous allez déplacer les variables de données de la classe GameFragment vers la classe GameViewModel.

  1. Déplacez les variables de données score, currentWordCount et currentScrambledWord vers la classe GameViewModel.
class GameViewModel : ViewModel() {

    private var score = 0
    private var currentWordCount = 0
    private var currentScrambledWord = "test"
...
  1. Des erreurs s'affichent pour indiquer des références non résolues. En effet, les propriétés étant privées et uniquement accessibles par ViewModel, votre contrôleur d'interface utilisateur n'y a pas accès. Voyons comment corriger ces erreurs.

Pour résoudre ce problème, vous ne pouvez pas définir les modificateurs de visibilité des propriétés sur public, car les données ne doivent pas être modifiables par d'autres classes. Il s'agirait d'une approche risquée, car une classe extérieure pourrait modifier les données d'une manière inattendue, qui ne respecte pas les règles de jeu spécifiées dans le modèle de vue. Par exemple, une classe externe serait en mesure de remplacer score par une valeur négative.

Dans le ViewModel, les données doivent être modifiables. Elles doivent donc être private et var. En dehors de ViewModel, les données doivent être lisibles, mais pas modifiables. Elles doivent donc être présentées comme public et val. Pour obtenir ce résultat, Kotlin dispose d'une fonctionnalité appelée propriété de support.

Propriété de support

Une propriété de support vous permet de renvoyer un élément d'un getter qui n'est pas l'objet exact.

Vous savez déjà que pour chaque propriété, le framework Kotlin génère des getters et des setters.

Pour les méthodes getter et setter, vous pouvez forcer l'une de ces méthodes (ou les deux) afin de personnaliser leur comportement. Pour implémenter une propriété de support, vous devez forcer la méthode getter afin de renvoyer une version en lecture seule de vos données. Exemple de propriété de support :

// Declare private mutable variable that can only be modified
// within the class it is declared.
private var _count = 0

// Declare another public immutable field and override its getter method.
// Return the private property's value in the getter method.
// When count is accessed, the get() function is called and
// the value of _count is returned.
val count: Int
   get() = _count

Imaginons que vous souhaitiez que les données de votre application soient privées et uniquement accessibles par le ViewModel.

Dans la classe ViewModel :

  • La propriété _count est private (privée) et modifiable. Par conséquent, elle n'est accessible et modifiable que dans la classe ViewModel. La convention consiste à ajouter un trait de soulignement au début de la propriété private.

En dehors de la classe ViewModel :

  • Dans Kotlin, le modificateur de visibilité par défaut est public. count est donc public et accessible à partir d'autres classes, comme les contrôleurs d'interface utilisateur. Étant donné que seule la méthode get() est forcée, cette propriété est immuable et en lecture seule. Lorsqu'une classe externe accède à cette propriété, elle renvoie la valeur de _count, qui n'est pas modifiable. Les données de l'application stockées dans ViewModel sont ainsi protégées contre les modifications indésirables et non sécurisées effectuées par les classes externes. Toutefois, les appelants externes peuvent accéder aux valeurs de manière sécurisée.

Ajouter une propriété de support à currentScrambledWord

  1. Dans GameViewModel, modifiez la déclaration currentScrambledWord pour ajouter une propriété de support. _currentScrambledWord n'est désormais accessible et modifiable que dans GameViewModel. Le contrôleur d'UI GameFragment peut lire sa valeur à l'aide de la propriété en lecture seule currentScrambledWord.
private var _currentScrambledWord = "test"
val currentScrambledWord: String
   get() = _currentScrambledWord
  1. Dans GameFragment, mettez à jour la méthode updateNextWordOnScreen() pour utiliser la propriété viewModel en lecture seule, currentScrambledWord.
private fun updateNextWordOnScreen() {
   binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
  1. Dans GameFragment, supprimez le code des méthodes onSubmitWord() et onSkipWord(). Vous les implémenterez plus tard. Vous devriez maintenant pouvoir compiler le code sans que des erreurs surviennent.

6. Cycle de vie d'un ViewModel

Le framework conserve l'élément ViewModel tant que le champ d'application de l'activité ou du fragment est actif. Un ViewModel n'est pas détruit si son propriétaire est détruit lors d'une modification de la configuration, par exemple la rotation de l'écran. La nouvelle instance du propriétaire se reconnecte à l'instance ViewModel existante, comme illustré par le schéma suivant :

91227008b74bf4bb.png

Comprendre le cycle de vie d'un ViewModel

Ajoutez un système de journalisation à GameViewModel et GameFragment pour mieux comprendre le cycle de vie de ViewModel.

  1. Dans GameViewModel.kt, ajoutez un bloc init avec une instruction de journalisation.
class GameViewModel : ViewModel() {
   init {
       Log.d("GameFragment", "GameViewModel created!")
   }

   ...
}

Kotlin fournit le bloc d'initialisation (également appelé bloc init), qui permet de configurer le code nécessaire à l'initialisation d'une instance d'objet. Les blocs d'initialisation sont précédés du mot clé init, suivi d'accolades {}. Ce bloc de code est exécuté lorsque l'instance d'objet est créée et initialisée pour la première fois.

  1. Dans la classe GameViewModel, forcez la méthode onCleared(). 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é.
  2. Ajoutez une instruction de journalisation dans onCleared() pour suivre le cycle de vie de GameViewModel.
override fun onCleared() {
    super.onCleared()
    Log.d("GameFragment", "GameViewModel destroyed!")
}
  1. Dans GameFragment, dans onCreateView(), après avoir obtenu une référence à l'objet de liaison, ajoutez une instruction de journalisation pour enregistrer la création du fragment. Le rappel onCreateView() est déclenché lors de la première création du fragment, ainsi qu'à chaque nouvelle création lorsque des événements surviennent, par exemple une modification de la configuration.
override fun onCreateView(
   inflater: LayoutInflater, container: ViewGroup?,
   savedInstanceState: Bundle?
): View {
   binding = GameFragmentBinding.inflate(inflater, container, false)
   Log.d("GameFragment", "GameFragment created/re-created!")
   return binding.root
}
  1. Dans GameFragment, forcez la méthode de rappel onDetach(), qui est appelée lorsque l'activité et le fragment correspondants sont détruits.
override fun onDetach() {
    super.onDetach()
    Log.d("GameFragment", "GameFragment destroyed!")
}
  1. Dans Android Studio, exécutez l'application, ouvrez la fenêtre Logcat et filtrez sur GameFragment. Notez que GameFragment et GameViewModel ont été créés.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameViewModel created!
  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, mais GameViewModel n'est créé qu'une seule fois, et il n'est pas recréé ni détruit à chaque appel.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameViewModel created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
  1. Quittez le jeu ou sortez de l'application à l'aide de la flèche de retour. GameViewModel est détruit, et le rappel onCleared() est appelé. GameFragment est également détruit.
com.example.android.unscramble D/GameFragment: GameViewModel destroyed!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!

7. Remplir le ViewModel

Dans cette tâche, vous allez continuer à remplir le GameViewModel à l'aide de méthodes d'assistance pour obtenir la série de lettres suivante, valider le mot du joueur pour augmenter le score et vérifier le nombre de mots pour mettre fin à la partie.

Initialisation tardive

Généralement, lorsque vous déclarez une variable, vous lui attribuez immédiatement une valeur initiale. Toutefois, dans certains cas, vous devrez l'initialiser ultérieurement. Pour effectuer cette opération en langage Kotlin, utilisez le mot clé lateinit, ce qui signifie une "initialisation tardive". Si vous avez la certitude de pouvoir initialiser la propriété avant de l'utiliser, vous pouvez la déclarer avec lateinit. Aucune mémoire n'est allouée à la variable tant qu'elle n'est pas initialisée. Si vous essayez d'accéder à la variable avant de l'avoir initialisée, l'application plante.

Obtenir le mot suivant

Créez la méthode getNextWord() dans la classe GameViewModel avec les fonctionnalités suivantes :

  • Obtenir un mot aléatoire à partir de la liste allWordsList et l'attribuer à currentWord.
  • Mélanger les lettres de currentWord et l'attribuer à currentScrambledWord.
  • Gérer le cas où le mot avec les lettres mélangées est identique au mot d'origine.
  • S'assurer de ne pas afficher le même mot deux fois au cours d'une même partie.

Dans la classe GameViewModel, réalisez les opérations suivantes :

  1. Dans GameViewModel,, ajoutez une variable de classe de type MutableList<String> appelée wordsList. Elle contiendra la liste des mots que vous utilisez dans le jeu afin d'éviter les répétitions.
  2. Ajoutez une autre variable de classe appelée currentWord, qui contiendra le mot que le joueur tente de déchiffrer. Utilisez le mot clé lateinit, car vous initialiserez cette propriété plus tard.
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
  1. Ajoutez une méthode private appelée getNextWord() au-dessus du bloc init, sans aucun paramètre et qui ne renvoie rien.
  2. Récupérez un mot aléatoire depuis la liste allWordsList et attribuez-le à currentWord.
private fun getNextWord() {
   currentWord = allWordsList.random()
}
  1. Dans getNextWord(), convertissez la chaîne currentWord en ensemble de caractères et attribuez-la à une nouvelle valeur val appelée tempWord. Pour mélanger les lettres, brassez les caractères de cet ensemble à l'aide de la méthode Kotlin shuffle().
val tempWord = currentWord.toCharArray()
tempWord.shuffle()

Un Array est semblable à une MutableList, mais sa taille est fixée lors de l'initialisation. Un Array ne peut pas augmenter ni réduire sa taille (pour redimensionner un ensemble, vous devez en créer une copie), tandis qu'une MutableList peut recevoir des fonctions add() et remove() afin d'en augmenter ou diminuer la taille.

  1. Il peut arriver qu'en réorganisant les lettres, vous obteniez exactement le même mot qu'au départ. Ajoutez la boucle while suivante à l'appel afin de réorganiser les lettres autant de fois que nécessaire pour obtenir un mot différent de celui d'origine.
while (String(tempWord).equals(currentWord, false)) {
    tempWord.shuffle()
}
  1. Ajoutez un bloc if-else pour vérifier si le mot a déjà été utilisé. Si wordsList contient currentWord, appelez getNextWord(). Dans le cas contraire, modifiez la valeur de _currentScrambledWord pour y appliquer le mot brouillé, augmentez le nombre de mots, puis ajoutez le nouveau mot à wordsList.
if (wordsList.contains(currentWord)) {
    getNextWord()
} else {
    _currentScrambledWord = String(tempWord)
    ++currentWordCount
    wordsList.add(currentWord)
}
  1. Pour vous aider, voici la méthode getNextWord() une fois terminée.
/*
* Updates currentWord and currentScrambledWord with the next word.
*/
private fun getNextWord() {
   currentWord = allWordsList.random()
   val tempWord = currentWord.toCharArray()
   tempWord.shuffle()

   while (String(tempWord).equals(currentWord, false)) {
       tempWord.shuffle()
   }
   if (wordsList.contains(currentWord)) {
       getNextWord()
   } else {
       _currentScrambledWord = String(tempWord)
       ++currentWordCount
       wordsList.add(currentWord)
   }
}

Faire une initialisation tardive de currentScrambledWord

À ce stade, vous avez créé la méthode getNextWord() pour obtenir le mot suivant et en réorganiser les lettres. Vous l'appellerez lors de la première initialisation de GameViewModel. Utilisez le bloc init pour initialiser les propriétés lateinit de la classe, notamment le mot actuel. Normalement, le premier mot affiché à l'écran devrait être brouillé et non test.

  1. Exécutez l'application. Notez que le premier mot est toujours "test".
  2. Pour afficher un mot brouillé au lancement de l'application, vous devez appeler la méthode getNextWord(), qui met à jour currentScrambledWord. Appelez la méthode getNextWord() dans le bloc init de GameViewModel.
init {
    Log.d("GameFragment", "GameViewModel created!")
    getNextWord()
}
  1. Ajoutez le modificateur lateinit à la propriété _currentScrambledWord. Ajoutez une mention explicite pour préciser le type de données (String), étant donné qu'aucune valeur initiale n'est fournie.
private lateinit var _currentScrambledWord: String
  1. Exécutez l'application. Un mot brouillé devrait s'afficher dès le lancement. Génial !

8edd6191a40a57e1.png

Ajouter une méthode d'assistance

Ajoutez ensuite une méthode d'assistance pour traiter et modifier les données dans ViewModel. Vous utiliserez cette méthode dans les tâches suivantes.

  1. Dans la classe GameViewModel, ajoutez une méthode appelée nextWord(). Récupérez le mot suivant dans la liste et renvoyez true si le nombre de mots est inférieur à la valeur MAX_NO_OF_WORDS.
/*
* Returns true if the current word count is less than MAX_NO_OF_WORDS.
* Updates the next word.
*/
fun nextWord(): Boolean {
    return if (currentWordCount < MAX_NO_OF_WORDS) {
        getNextWord()
        true
    } else false
}

8. Boîtes de dialogue

Avec le code de démarrage, la partie ne se termine jamais, même après avoir joué 10 mots. Modifiez votre application de sorte que la partie se termine après 10 mots, puis ajoutez une boîte de dialogue avec le score final. Vous allez également proposer à l'utilisateur de rejouer ou de quitter le jeu.

62aa368820ffbe31.png

C'est la première fois que vous ajoutez une boîte de dialogue à une application. Une boîte de dialogue est une petite fenêtre (écran) qui invite l'utilisateur à prendre une décision ou à saisir des informations supplémentaires. Normalement, une boîte de dialogue ne remplit pas tout l'écran, et les utilisateurs doivent réaliser une action avant de pouvoir continuer. Android propose différents types de boîtes de dialogue. Dans cet atelier de programmation, vous découvrirez les boîtes de dialogue d'alerte.

Anatomie d'une boîte de dialogue d'alerte

f8650ca15e854fe4.png

  1. Boîte de dialogue d'alerte
  2. Titre (facultatif)
  3. Message
  4. Boutons de texte

Implémentation de la boîte de dialogue avec le score final

Utilisez MaterialAlertDialog dans la bibliothèque de composants Material Design pour ajouter à votre application une boîte de dialogue qui respecte les consignes Material Design. Étant donné qu'une boîte de dialogue est liée à l'interface utilisateur, GameFragment sera chargé de créer et d'afficher la boîte de dialogue avec le score final.

  1. Commencez par ajouter une propriété de support à la variable score. Dans GameViewModel, modifiez la déclaration de la variable score comme suit.
private var _score = 0
val score: Int
   get() = _score
  1. Dans GameFragment, ajoutez une fonction privée appelée showFinalScoreDialog(). Pour créer MaterialAlertDialog, utilisez la classe MaterialAlertDialogBuilder, qui sera chargée de créer la boîte de dialogue étape par étape. Appelez le constructeur MaterialAlertDialogBuilder en transmettant le contenu à l'aide de la méthode requireContext() du fragment. La méthode requireContext() renvoie un Context non nul.
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
   MaterialAlertDialogBuilder(requireContext())
}

Comme son nom l'indique, Context fait référence au contexte ou à l'état actuel d'une application, d'une activité ou d'un fragment. Il contient des informations sur l'activité, le fragment ou l'application en question. Il est communément utilisé pour accéder aux ressources, aux bases de données et à d'autres services système. Au cours de cette étape, vous allez transmettre le contexte du fragment pour créer la boîte de dialogue d'alerte.

Si Android Studio vous le demande, importez (import) com.google.android.material.dialog.MaterialAlertDialogBuilder.

  1. Ajoutez le code permettant de définir le titre de la boîte de dialogue d'alerte. Utilisez une ressource de chaîne du fichier strings.xml.
MaterialAlertDialogBuilder(requireContext())
   .setTitle(getString(R.string.congratulations))
  1. Configurez le message pour afficher le score final. Pour ce faire, utilisez la version en lecture seule de la variable de score (viewModel.score) que vous avez ajoutée précédemment.
   .setMessage(getString(R.string.you_scored, viewModel.score))
  1. Empêchez l'annulation de la boîte de dialogue d'alerte lorsque l'utilisateur appuie sur la touche Retour en utilisant la méthode setCancelable() et en transmettant false.
    .setCancelable(false)
  1. Ajoutez les deux boutons de texte EXIT (QUITTER) et PLAY AGAIN (REJOUER) à l'aide des méthodes setNegativeButton() et setPositiveButton(). Appelez respectivement exitGame() et restartGame() à partir des lambdas.
    .setNegativeButton(getString(R.string.exit)) { _, _ ->
        exitGame()
    }
    .setPositiveButton(getString(R.string.play_again)) { _, _ ->
        restartGame()
    }

Cette syntaxe est peut-être nouvelle pour vous, mais il s'agit d'un raccourci pour setNegativeButton(getString(R.string.exit), { _, _ -> exitGame()}), où la méthode setNegativeButton() reçoit deux paramètres : une String (chaîne de caractères) et une fonction, DialogInterface.OnClickListener() qui peut être exprimée sous la forme d'un lambda. Lorsque le dernier argument transmis est une fonction, vous pouvez placer l'expression lambda en dehors des parenthèses. C'est ce qu'on appelle la syntaxe lambda de fin. Les deux manières d'écrire le code (avec le lambda à l'intérieur ou à l'extérieur des parenthèses) sont acceptables. Il en va de même pour la fonction setPositiveButton.

  1. À la fin, ajoutez show(), qui crée et affiche la boîte de dialogue d'alerte.
      .show()
  1. Pour vous aider, voici la méthode complète pour showFinalScoreDialog() :
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
   MaterialAlertDialogBuilder(requireContext())
       .setTitle(getString(R.string.congratulations))
       .setMessage(getString(R.string.you_scored, viewModel.score))
       .setCancelable(false)
       .setNegativeButton(getString(R.string.exit)) { _, _ ->
           exitGame()
       }
       .setPositiveButton(getString(R.string.play_again)) { _, _ ->
           restartGame()
       }
       .show()
}

9. Implémenter l'écouteur de clics du bouton "Submit" (Envoyer)

Dans cette tâche, vous allez utiliser le ViewModel et la boîte de dialogue d'alerte que vous avez ajoutée pour implémenter la logique du jeu pour l'écouteur de clics du bouton Submit (Envoyer).

Afficher les lettres à réorganiser

  1. Si vous ne l'avez pas déjà fait, dans GameFragment, supprimez le code contenu dans la fonction onSubmitWord(), qui est appelée lorsque l'utilisateur appuie sur le bouton Submit (Envoyer).
  2. Ajoutez une vérification de la valeur renvoyée par la méthode viewModel.nextWord(). Si la valeur est true, un autre mot est disponible. Vous pouvez donc modifier la série de lettres à réorganiser qui s'affiche à l'écran avec updateNextWordOnScreen(). Dans le cas contraire, la partie est terminée. Vous pouvez donc afficher la boîte de dialogue d'alerte avec le score final.
private fun onSubmitWord() {
    if (viewModel.nextWord()) {
        updateNextWordOnScreen()
    } else {
        showFinalScoreDialog()
    }
}
  1. Exécutez l'application ! Jouez avec quelques mots. N'oubliez pas que vous ne pouvez pas ignorer de mot, car vous n'avez pas encore implémenté le bouton Passer.
  2. Notez que le champ de texte n'est pas mis à jour. Le joueur doit donc supprimer manuellement le mot précédent. De plus, le score final affiché par la boîte de dialogue d'alerte est toujours zéro. Vous allez corriger ces bugs au cours des étapes suivantes.

a4c660e212ce2c31.png 12a42987a0edd2c4.png

Ajouter une méthode d'assistance pour valider le mot proposé par le joueur

  1. Dans GameViewModel, ajoutez une méthode privée appelée increaseScore(), sans paramètres ni valeur renvoyée. Ajoutez SCORE_INCREASE à la variable score.
private fun increaseScore() {
   _score += SCORE_INCREASE
}
  1. Dans GameViewModel, ajoutez une méthode d'assistance appelée isUserWordCorrect() qui renvoie une valeur Boolean (booléenne) et reçoit une String (chaîne de caractères), à savoir le mot du joueur, comme paramètre.
  2. Dans isUserWordCorrect(), validez le mot du joueur et augmentez le score si la réponse est correcte. Le score final qui s'affiche dans la boîte de dialogue d'alerte sera ainsi modifié.
fun isUserWordCorrect(playerWord: String): Boolean {
   if (playerWord.equals(currentWord, true)) {
       increaseScore()
       return true
   }
   return false
}

Mettre à jour le champ de texte

Afficher les erreurs dans le champ de texte

Pour les champs de texte Material, TextInputLayout comprend une fonctionnalité permettant d'afficher des messages d'erreur. Par exemple, dans le champ de texte suivant, la couleur du libellé est modifiée, une icône d'erreur et un message d'erreur s'affichent, etc.

520cc685ae1317ac.png

Pour afficher une erreur dans le champ de texte, vous pouvez définir le message de manière dynamique dans le code ou de manière statique dans le fichier de mise en page. L'exemple ci-dessous montre comment définir et réinitialiser l'erreur dans le code :

// Set error text
passwordLayout.error = getString(R.string.error)

// Clear error text
passwordLayout.error = null

Dans le code de démarrage, vous constaterez que la méthode d'assistance setErrorTextField(error: Boolean) est déjà définie pour vous aider à configurer et à réinitialiser l'erreur dans le champ de texte. Appelez cette méthode avec un paramètre d'entrée true ou false, selon que vous souhaitez qu'une erreur s'affiche dans le champ de texte ou non.

Extrait du code de démarrage

private fun setErrorTextField(error: Boolean) {
   if (error) {
       binding.textField.isErrorEnabled = true
       binding.textField.error = getString(R.string.try_again)
   } else {
       binding.textField.isErrorEnabled = false
       binding.textInputEditText.text = null
   }
}

Dans cette tâche, vous allez implémenter la méthode onSubmitWord(). Lors de la confirmation d'un mot, comparez la proposition de l'utilisateur au mot d'origine. Si le mot est correct, passez au mot suivant (ou affichez la boîte de dialogue si la partie est terminée). Si le mot est incorrect, il reste affiché et une erreur apparaît dans le champ de texte.

  1. Dans GameFragment,, au début de onSubmitWord(), créez un élément val appelé playerWord. Dans la variable binding, stockez le mot proposé par le joueur dans le champ de texte.
private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()
    ...
}
  1. Dans onSubmitWord(), sous la déclaration de playerWord, validez le mot du joueur. Ajoutez une instruction if pour comparer la proposition à l'aide de la méthode isUserWordCorrect(), en transmettant playerWord.
  2. Dans le bloc if, réinitialisez le champ de texte, puis appelez setErrorTextField en transmettant le paramètre false.
  3. Déplacez le code existant dans le bloc if.
private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()

    if (viewModel.isUserWordCorrect(playerWord)) {
        setErrorTextField(false)
        if (viewModel.nextWord()) {
            updateNextWordOnScreen()
        } else {
            showFinalScoreDialog()
        }
    }
}
  1. Si le mot proposé par l'utilisateur est incorrect, un message d'erreur s'affiche dans le champ de texte. Ajoutez un bloc else au bloc if ci-dessus, puis appelez setErrorTextField() en transmettant le paramètre true. Une fois terminée, votre méthode onSubmitWord() devrait ressembler à ceci :
private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()

    if (viewModel.isUserWordCorrect(playerWord)) {
        setErrorTextField(false)
        if (viewModel.nextWord()) {
            updateNextWordOnScreen()
        } else {
            showFinalScoreDialog()
        }
    } else {
        setErrorTextField(true)
    }
}
  1. Exécutez votre application et jouez avec quelques mots. Si votre réponse est correcte, elle est effacée lorsque vous utilisez le bouton Submit (Envoyer). Dans le cas contraire, le message "Try again!" (Réessayez) s'affiche. Notez que le bouton Passer ne fonctionne toujours pas. Au cours de l'étape suivante, vous allez régler ce problème.

a10c7d77aa26b9db.png

10. Implémenter le bouton Passer

Dans cette tâche, vous allez implémenter onSkipWord(), qui gère l'action associée au bouton Passer.

  1. Comme pour onSubmitWord(), ajoutez une condition dans la méthode onSkipWord(). Si la valeur est true, affichez le mot à l'écran et réinitialisez le champ de texte. Si la valeur est false et qu'il ne reste plus de mots dans cette partie, affichez la boîte de dialogue d'alerte avec le score final.
/*
* Skips the current word without changing the score.
*/
private fun onSkipWord() {
    if (viewModel.nextWord()) {
        setErrorTextField(false)
        updateNextWordOnScreen()
    } else {
        showFinalScoreDialog()
    }
}
  1. Exécutez votre application et jouez une partie. Notez que les boutons Passer et Confirmer fonctionnent comme prévu. Parfait !

11. Vérifier que ViewModel conserve les données

Pour cette tâche, ajoutez un système de journalisation à GameFragment pour vérifier que les données de votre application sont conservées dans ViewModel lorsque la configuration est modifiée. Pour accéder à currentWordCount dans GameFragment, vous devez utiliser une propriété de support pour rendre une version accessible en lecture seule.

  1. Dans GameViewModel, effectuez un clic droit sur la variable currentWordCount, puis sélectionnez Refactor > Rename (Refactoriser > Renommer). . Ajoutez un trait de soulignement au nouveau nom (_currentWordCount).
  2. Ajoutez un champ de support.
private var _currentWordCount = 0
val currentWordCount: Int
   get() = _currentWordCount
  1. Dans GameFragment, dans la fonction onCreateView(), au-dessus de l'instruction return, ajoutez un autre journal pour enregistrer les données de l'application, le mot, le score et le nombre de mots.
Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
       "Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
  1. Dans Android Studio, ouvrez Logcat, puis filtrez sur GameFragment. Exécutez l'application et essayez de deviner quelques mots. Modifiez l'orientation de votre appareil. Le fragment (contrôleur d'UI) est détruit et recréé. Observez les journaux. Vous pouvez désormais voir le score et le nombre de mots augmenter !
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameViewModel created!
com.example.android.unscramble D/GameFragment: Word: oimfnru Score: 0 WordCount: 1
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9

Notez que les données de l'application sont conservées dans ViewModel lors des changements d'orientation. Au cours des prochains ateliers de programmation, vous mettrez à jour la valeur du score et le nombre de mots dans l'interface utilisateur à l'aide de LiveData et de la liaison de données.

12. Modifier la logique de redémarrage du jeu

  1. Exécutez l'application à nouveau et jouez à tous les mots. Dans la boîte de dialogue d'alerte Congratulations! (Félicitations !), cliquez sur PLAY AGAIN (REJOUER). L'application ne vous laissera pas rejouer, car le nombre de mots a atteint la valeur MAX_NO_OF_WORDS. Pour rejouer, vous devez réinitialiser le nombre de mots.
  2. Pour réinitialiser les données de l'application, ajoutez une méthode appelée reinitializeData() dans GameViewModel. Définissez le score et le nombre de mots sur 0. Effacez la liste de mots et appelez la méthode getNextWord().
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
   _score = 0
   _currentWordCount = 0
   wordsList.clear()
   getNextWord()
}
  1. Dans GameFragment, en haut de la méthode restartGame(), appelez la méthode que vous venez de créer, reinitializeData().
private fun restartGame() {
   viewModel.reinitializeData()
   setErrorTextField(false)
   updateNextWordOnScreen()
}
  1. Exécutez à nouveau votre application. Jouez une partie. Dans la boîte de dialogue de félicitations, cliquez sur Play Again (Rejouer). Vous devriez pouvoir relancer une partie !

Une fois terminée, voici à quoi devrait ressembler votre application. Le jeu affiche dix mots dont les lettres ont été mélangées de manière aléatoire, et que le joueur doit réorganiser. Vous pouvez utiliser le bouton Passer pour ignorer un mot, ou saisir une réponse avant d'appuyer sur Confirmer. Si votre réponse est correcte, le score augmente. Si la réponse est incorrecte, un état d'erreur s'affiche dans le champ de texte. Le nombre de mots augmente également à chaque nouveau mot.

Notez que le score et le nombre de mots affichés à l'écran ne fonctionnent pas encore correctement. Toutefois, les informations sont toujours stockées dans le modèle de vue et conservées lors des modifications de configuration, par exemple une rotation de l'appareil. Au cours des prochains ateliers de programmation, vous mettrez à jour le score et le nombre de mots à l'écran.

f332979d6f63d0e5.png 2803d4855f5d401f.png

Une fois que vous avez joué 10 mots, la partie se termine et une boîte de dialogue d'alerte s'affiche avec votre score final et des boutons permettant de quitter le jeu ou de rejouer.

d8e0111f5f160ead.png

Félicitations ! Vous avez créé votre premier ViewModel et enregistré des données.

13. Code de solution

GameFragment.kt

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.android.unscramble.R
import com.example.android.unscramble.databinding.GameFragmentBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder

/**
 * Fragment where the game is played, contains the game logic.
 */
class GameFragment : Fragment() {

    private val viewModel: GameViewModel by viewModels()

    // Binding object instance with access to the views in the game_fragment.xml layout
    private lateinit var binding: GameFragmentBinding

    // Create a ViewModel the first time the fragment is created.
    // If the fragment is re-created, it receives the same GameViewModel instance created by the
    // first fragment

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout XML file and return a binding object instance
        binding = GameFragmentBinding.inflate(inflater, container, false)
        Log.d("GameFragment", "GameFragment created/re-created!")
        Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
                "Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Setup a click listener for the Submit and Skip buttons.
        binding.submit.setOnClickListener { onSubmitWord() }
        binding.skip.setOnClickListener { onSkipWord() }
        // Update the UI
        updateNextWordOnScreen()
        binding.score.text = getString(R.string.score, 0)
        binding.wordCount.text = getString(
            R.string.word_count, 0, MAX_NO_OF_WORDS)
    }

    /*
    * Checks the user's word, and updates the score accordingly.
    * Displays the next scrambled word.
    * After the last word, the user is shown a Dialog with the final score.
    */
    private fun onSubmitWord() {
        val playerWord = binding.textInputEditText.text.toString()

        if (viewModel.isUserWordCorrect(playerWord)) {
            setErrorTextField(false)
            if (viewModel.nextWord()) {
                updateNextWordOnScreen()
            } else {
                showFinalScoreDialog()
            }
        } else {
            setErrorTextField(true)
        }
    }

    /*
    * Skips the current word without changing the score.
    */
    private fun onSkipWord() {
        if (viewModel.nextWord()) {
            setErrorTextField(false)
            updateNextWordOnScreen()
        } else {
            showFinalScoreDialog()
        }
    }

    /*
     * Gets a random word for the list of words and shuffles the letters in it.
     */
    private fun getNextScrambledWord(): String {
        val tempWord = allWordsList.random().toCharArray()
        tempWord.shuffle()
        return String(tempWord)
    }

    /*
    * Creates and shows an AlertDialog with the final score.
    */
    private fun showFinalScoreDialog() {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle(getString(R.string.congratulations))
            .setMessage(getString(R.string.you_scored, viewModel.score))
            .setCancelable(false)
            .setNegativeButton(getString(R.string.exit)) { _, _ ->
                exitGame()
            }
            .setPositiveButton(getString(R.string.play_again)) { _, _ ->
                restartGame()
            }
            .show()
    }

    /*
     * Re-initializes the data in the ViewModel and updates the views with the new data, to
     * restart the game.
     */
    private fun restartGame() {
        viewModel.reinitializeData()
        setErrorTextField(false)
        updateNextWordOnScreen()
    }

    /*
     * Exits the game.
     */
    private fun exitGame() {
        activity?.finish()
    }

    override fun onDetach() {
        super.onDetach()
        Log.d("GameFragment", "GameFragment destroyed!")
    }

    /*
    * Sets and resets the text field error status.
    */
    private fun setErrorTextField(error: Boolean) {
        if (error) {
            binding.textField.isErrorEnabled = true
            binding.textField.error = getString(R.string.try_again)
        } else {
            binding.textField.isErrorEnabled = false
            binding.textInputEditText.text = null
        }
    }

    /*
     * Displays the next scrambled word on screen.
     */
    private fun updateNextWordOnScreen() {
        binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
    }
}

GameViewModel.kt

import android.util.Log
import androidx.lifecycle.ViewModel

/**
 * ViewModel containing the app data and methods to process the data
 */
class GameViewModel : ViewModel(){
    private var _score = 0
    val score: Int
        get() = _score

    private var _currentWordCount = 0
    val currentWordCount: Int
        get() = _currentWordCount

    private lateinit var _currentScrambledWord: String
    val currentScrambledWord: String
        get() = _currentScrambledWord

    // List of words used in the game
    private var wordsList: MutableList<String> = mutableListOf()
    private lateinit var currentWord: String

    init {
        Log.d("GameFragment", "GameViewModel created!")
        getNextWord()
    }

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

    /*
    * Updates currentWord and currentScrambledWord with the next word.
    */
    private fun getNextWord() {
        currentWord = allWordsList.random()
        val tempWord = currentWord.toCharArray()
        tempWord.shuffle()

        while (String(tempWord).equals(currentWord, false)) {
            tempWord.shuffle()
        }
        if (wordsList.contains(currentWord)) {
            getNextWord()
        } else {
            _currentScrambledWord = String(tempWord)
            ++_currentWordCount
            wordsList.add(currentWord)
        }
    }

    /*
    * Re-initializes the game data to restart the game.
    */
    fun reinitializeData() {
       _score = 0
       _currentWordCount = 0
       wordsList.clear()
       getNextWord()
    }

    /*
    * Increases the game score if the player's word is correct.
    */
    private fun increaseScore() {
        _score += SCORE_INCREASE
    }

    /*
    * Returns true if the player word is correct.
    * Increases the score accordingly.
    */
    fun isUserWordCorrect(playerWord: String): Boolean {
        if (playerWord.equals(currentWord, true)) {
            increaseScore()
            return true
        }
        return false
    }

    /*
    * Returns true if the current word count is less than MAX_NO_OF_WORDS
    */
    fun nextWord(): Boolean {
        return if (_currentWordCount < MAX_NO_OF_WORDS) {
            getNextWord()
            true
        } else false
    }
}

14. Résumé

  • Les principes fondamentaux de l'architecture des applications Android recommandent de séparer les classes ayant des responsabilités distinctes et de piloter l'interface utilisateur à partir d'un modèle.
  • Un contrôleur d'UI est une classe qui dépend de l'UI, par exemple Activity ou Fragment. Ces contrôleurs doivent contenir uniquement la logique qui gère les interactions entre l'UI et le système d'exploitation. Ils ne doivent pas être la source des données qui s'affichent dans l'UI. Ces données et toute logique associée doivent être stocké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.

15. En savoir plus