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
etButton
. - 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.
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.
- Accédez à la page du dépôt GitHub fournie pour le projet.
- 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.
- Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une fenêtre pop-up.
- 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.
- Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
- 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
- Lancez Android Studio.
- Dans la fenêtre Welcome to Android Studio (Bienvenue dans Android Studio), cliquez sur Open (Ouvrir).
Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > Open (Fichier > Ouvrir).
- 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).
- Double-cliquez sur le dossier de ce projet.
- Attendez qu'Android Studio ouvre le projet.
- Cliquez sur le bouton Run (Exécuter) pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.
Présentation du code de démarrage
- Dans Android Studio, ouvrez le projet contenant le code de démarrage.
- Exécutez l'application sur un appareil Android ou sur un émulateur.
- 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.
- 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 :
- Lorsque vous cliquez sur le bouton Confirmer, l'application ne vérifie pas le mot. Le joueur gagne des points à chaque fois.
- Il n'y a aucun moyen de mettre fin au jeu. L'application vous permet de continuer à jouer au-delà de 10 mots.
- 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
etstyles
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 vuesgame_fragment
est définie. - La fonction
onCreateView()
gonfle le code XML de mise en pagegame_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 queonSubmitWord()
, à 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()
etexitGame()
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 :
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 |
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. |
|
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
.
- Dans la fenêtre Android d'Android Studio, sous le dossier Scripts Gradle, ouvrez le fichier
build.gradle(Module:Unscramble.app)
. - Pour utiliser un
ViewModel
dans votre application, vérifiez que la dépendance de la bibliothèque ViewModel est présente dans le blocdependencies
. 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.
- 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).
- Donnez-lui le nom
GameViewModel
, puis sélectionnez Classe dans la liste. - Modifiez
GameViewModel
pour en faire une sous-classe deViewModel
.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 classeGameViewModel
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
.
- En haut de la classe
GameFragment
, ajoutez une propriété de typeGameViewModel
. - Initialisez
GameViewModel
à l'aide du délégué de propriété Kotlinby viewModels()
. La section suivante expliquera le fonctionnement plus en détail.
private val viewModel: GameViewModel by viewModels()
- 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
.
- Déplacez les variables de données
score
,currentWordCount
etcurrentScrambledWord
vers la classeGameViewModel
.
class GameViewModel : ViewModel() {
private var score = 0
private var currentWordCount = 0
private var currentScrambledWord = "test"
...
- 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
estprivate
(privée) et modifiable. Par conséquent, elle n'est accessible et modifiable que dans la classeViewModel
. 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éthodeget()
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 dansViewModel
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
- Dans
GameViewModel
, modifiez la déclarationcurrentScrambledWord
pour ajouter une propriété de support._currentScrambledWord
n'est désormais accessible et modifiable que dansGameViewModel
. Le contrôleur d'UIGameFragment
peut lire sa valeur à l'aide de la propriété en lecture seulecurrentScrambledWord
.
private var _currentScrambledWord = "test"
val currentScrambledWord: String
get() = _currentScrambledWord
- Dans
GameFragment
, mettez à jour la méthodeupdateNextWordOnScreen()
pour utiliser la propriétéviewModel
en lecture seule,currentScrambledWord
.
private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
- Dans
GameFragment
, supprimez le code des méthodesonSubmitWord()
etonSkipWord()
. 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 :
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
.
- Dans
GameViewModel.kt
, ajoutez un blocinit
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.
- Dans la classe
GameViewModel
, forcez la méthodeonCleared()
. LeViewModel
est détruit lorsque le fragment associé est dissocié ou lorsque l'activité se termine. Juste avant la destruction duViewModel
, le rappelonCleared()
est utilisé. - Ajoutez une instruction de journalisation dans
onCleared()
pour suivre le cycle de vie deGameViewModel
.
override fun onCleared() {
super.onCleared()
Log.d("GameFragment", "GameViewModel destroyed!")
}
- Dans
GameFragment
, dansonCreateView()
, 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 rappelonCreateView()
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
}
- Dans
GameFragment
, forcez la méthode de rappelonDetach()
, qui est appelée lorsque l'activité et le fragment correspondants sont détruits.
override fun onDetach() {
super.onDetach()
Log.d("GameFragment", "GameFragment destroyed!")
}
- Dans Android Studio, exécutez l'application, ouvrez la fenêtre Logcat et filtrez sur
GameFragment
. Notez queGameFragment
etGameViewModel
ont été créés.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created!
- 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, maisGameViewModel
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!
- Quittez le jeu ou sortez de l'application à l'aide de la flèche de retour.
GameViewModel
est détruit, et le rappelonCleared()
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 :
- Dans
GameViewModel,
, ajoutez une variable de classe de typeMutableList<String>
appeléewordsList
. Elle contiendra la liste des mots que vous utilisez dans le jeu afin d'éviter les répétitions. - 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
- Ajoutez une méthode
private
appeléegetNextWord()
au-dessus du blocinit
, sans aucun paramètre et qui ne renvoie rien. - Récupérez un mot aléatoire depuis la liste
allWordsList
et attribuez-le àcurrentWord
.
private fun getNextWord() {
currentWord = allWordsList.random()
}
- Dans
getNextWord()
, convertissez la chaînecurrentWord
en ensemble de caractères et attribuez-la à une nouvelle valeurval
appeléetempWord
. Pour mélanger les lettres, brassez les caractères de cet ensemble à l'aide de la méthode Kotlinshuffle()
.
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.
- 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()
}
- Ajoutez un bloc
if-else
pour vérifier si le mot a déjà été utilisé. SiwordsList
contientcurrentWord
, appelezgetNextWord()
. 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)
}
- 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.
- Exécutez l'application. Notez que le premier mot est toujours "test".
- Pour afficher un mot brouillé au lancement de l'application, vous devez appeler la méthode
getNextWord()
, qui met à jourcurrentScrambledWord
. Appelez la méthodegetNextWord()
dans le blocinit
deGameViewModel
.
init {
Log.d("GameFragment", "GameViewModel created!")
getNextWord()
}
- 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
- Exécutez l'application. Un mot brouillé devrait s'afficher dès le lancement. Génial !
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.
- Dans la classe
GameViewModel
, ajoutez une méthode appeléenextWord().
Récupérez le mot suivant dans la liste et renvoyeztrue
si le nombre de mots est inférieur à la valeurMAX_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.
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
- Boîte de dialogue d'alerte
- Titre (facultatif)
- Message
- 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.
- Commencez par ajouter une propriété de support à la variable
score
. DansGameViewModel
, modifiez la déclaration de la variablescore
comme suit.
private var _score = 0
val score: Int
get() = _score
- Dans
GameFragment
, ajoutez une fonction privée appeléeshowFinalScoreDialog()
. Pour créerMaterialAlertDialog
, utilisez la classeMaterialAlertDialogBuilder
, qui sera chargée de créer la boîte de dialogue étape par étape. Appelez le constructeurMaterialAlertDialogBuilder
en transmettant le contenu à l'aide de la méthoderequireContext()
du fragment. La méthoderequireContext()
renvoie unContext
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
.
- 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))
- 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))
- 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 transmettantfalse
.
.setCancelable(false)
- Ajoutez les deux boutons de texte EXIT (QUITTER) et PLAY AGAIN (REJOUER) à l'aide des méthodes
setNegativeButton()
etsetPositiveButton()
. Appelez respectivementexitGame()
etrestartGame()
à 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
.
- À la fin, ajoutez
show()
, qui crée et affiche la boîte de dialogue d'alerte.
.show()
- 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
- Si vous ne l'avez pas déjà fait, dans
GameFragment
, supprimez le code contenu dans la fonctiononSubmitWord()
, qui est appelée lorsque l'utilisateur appuie sur le bouton Submit (Envoyer). - Ajoutez une vérification de la valeur renvoyée par la méthode
viewModel.nextWord()
. Si la valeur esttrue
, un autre mot est disponible. Vous pouvez donc modifier la série de lettres à réorganiser qui s'affiche à l'écran avecupdateNextWordOnScreen()
. 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()
}
}
- 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.
- 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.
Ajouter une méthode d'assistance pour valider le mot proposé par le joueur
- Dans
GameViewModel
, ajoutez une méthode privée appeléeincreaseScore()
, sans paramètres ni valeur renvoyée. AjoutezSCORE_INCREASE
à la variablescore
.
private fun increaseScore() {
_score += SCORE_INCREASE
}
- Dans
GameViewModel
, ajoutez une méthode d'assistance appeléeisUserWordCorrect()
qui renvoie une valeurBoolean
(booléenne) et reçoit uneString
(chaîne de caractères), à savoir le mot du joueur, comme paramètre. - 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.
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.
- Dans
GameFragment,
, au début deonSubmitWord()
, créez un élémentval
appeléplayerWord
. Dans la variablebinding
, stockez le mot proposé par le joueur dans le champ de texte.
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
...
}
- Dans
onSubmitWord()
, sous la déclaration deplayerWord
, validez le mot du joueur. Ajoutez une instructionif
pour comparer la proposition à l'aide de la méthodeisUserWordCorrect()
, en transmettantplayerWord
. - Dans le bloc
if
, réinitialisez le champ de texte, puis appelezsetErrorTextField
en transmettant le paramètrefalse
. - 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()
}
}
}
- 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 blocif
ci-dessus, puis appelezsetErrorTextField()
en transmettant le paramètretrue
. Une fois terminée, votre méthodeonSubmitWord()
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)
}
}
- 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.
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.
- Comme pour
onSubmitWord()
, ajoutez une condition dans la méthodeonSkipWord()
. Si la valeur esttrue
, affichez le mot à l'écran et réinitialisez le champ de texte. Si la valeur estfalse
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()
}
}
- 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.
- Dans
GameViewModel
, effectuez un clic droit sur la variablecurrentWordCount
, puis sélectionnez Refactor > Rename (Refactoriser > Renommer). . Ajoutez un trait de soulignement au nouveau nom (_currentWordCount
). - Ajoutez un champ de support.
private var _currentWordCount = 0
val currentWordCount: Int
get() = _currentWordCount
- Dans
GameFragment
, dans la fonctiononCreateView()
, 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}")
- 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
- 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. - Pour réinitialiser les données de l'application, ajoutez une méthode appelée
reinitializeData()
dansGameViewModel
. Définissez le score et le nombre de mots sur0
. Effacez la liste de mots et appelez la méthodegetNextWord()
.
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
_score = 0
_currentWordCount = 0
wordsList.clear()
getNextWord()
}
- Dans
GameFragment
, en haut de la méthoderestartGame()
, appelez la méthode que vous venez de créer,reinitializeData()
.
private fun restartGame() {
viewModel.reinitializeData()
setErrorTextField(false)
updateNextWordOnScreen()
}
- 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.
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.
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
ouFragment
. 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 unViewModel
. - La classe
ViewModel
stocke et gère les données liées à l'UI. La classeViewModel
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
- Présentation de ViewModel
- Guide de l'architecture des applications
- Démonstration pratique des composants Material pour Android : les boîtes de dialogue
- Anatomie de la boîte de dialogue d'alerte
- MaterialAlertDialogBuilder
- Propriétés de support
- Composants d'architecture Android
- Boîtes de dialogue Material pour Android
- Propriétés et champs : getters, setters, const, lateinit