ViewModel et l'état dans Compose

1. Avant de commencer

Dans les ateliers précédents, vous avez découvert le cycle de vie des activités et les problèmes de cycle de vie liés aux modifications de configuration. Lorsqu'une configuration est modifiée, vous pouvez enregistrer les données d'une application de différentes manières, par exemple en utilisant rememberSaveable ou en enregistrant l'état de l'instance. Cependant, ces options peuvent créer des problèmes. Dans la plupart des cas, vous pouvez utiliser rememberSaveable, mais vous devrez peut-être conserver la logique dans ou près des composables. Quand une application se développe, il convient de séparer les données et la logique des composables. Dans cet atelier de programmation, vous allez découvrir une méthode efficace pour concevoir votre application et préserver ses données lors des modifications de configuration grâce à la bibliothèque Android Jetpack, à ViewModel et aux principes fondamentaux de l'architecture des applications Android.

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 vous intéresse, comme la logique de l'application.

L'architecture des applications est l'ensemble des règles qui régit la conception d'une application. Comme le plan d'une maison, votre architecture fournit la structure de votre application. Avec une bonne architecture, votre code peut être robuste, flexible, évolutif, testable et facile à gérer au cours des années à venir. Le Guide de l'architecture des applications fournit des recommandations sur l'architecture des applications et les bonnes pratiques en la matière.

Dans cet atelier de programmation, vous allez apprendre à utiliser ViewModel, l'un des composants d'architecture des bibliothèques Android Jetpack capables de stocker les données de vos applications. Si le framework détruit et recrée les activités lors d'une modification de configuration, ou lorsque d'autres événements surviennent, les données stockées ne sont pas perdues. Cependant, les données seront perdues si l'activité est détruite suite à l'arrêt du processus. Le ViewModel ne met en cache les données que via des recréations liées à une activité rapide.

Conditions préalables

  • Vous maîtrisez Kotlin, y compris les fonctions, les lambdas et les composables sans état.
  • Vous disposez de connaissances de base en création de mises en page dans Jetpack Compose.
  • Vous disposez de connaissances de base en Material Design.

Points abordés

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

  • La dernière version d'Android Studio
  • Une connexion Internet pour télécharger le code de démarrage

2. Présentation de l'application

Présentation du jeu

L'application Unscramble est un jeu en solo qui repose sur le principe des anagrammes. L'application affiche un mot dont les lettres ont été mélangées, et le joueur doit retrouver le mot en utilisant toutes les lettres affichées. Si le mot est correct, le joueur marque des points. Dans le cas contraire, il peut retenter sa chance. L'application propose également une option permettant d'ignorer un mot. Dans le coin supérieur droit, l'application affiche le nombre de mots qui vous ont déjà été proposés au cours de la partie. Il y a 10 mots à deviner par partie.

Télécharger le code de démarrage

Pour commencer, téléchargez le code de démarrage :

Vous pouvez également cloner le dépôt GitHub du code :

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git
$ cd basic-android-kotlin-compose-training-unscramble
$ git checkout starter

Vous pouvez parcourir le code de démarrage dans le dépôt GitHub Unscramble.

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

Pour vous familiariser avec le code de démarrage, procédez comme suit :

  1. Dans Android Studio, ouvrez le projet contenant le code de démarrage.
  2. Exécutez l'appli sur un appareil Android ou sur un émulateur.
  3. Appuyez sur les boutons Submit (Envoyer) et Skip (Ignorer) pour tester l'appli.

Vous remarquerez des bugs dans l'appli. Le mot mélangé ne s'affiche pas, mais est codé en dur sur "scrambleun", et rien ne se passe lorsque vous appuyez sur ces boutons.

Dans cet atelier de programmation, vous allez implémenter la fonctionnalité de jeu à l'aide de l'architecture des applications Android.

Explication du code de démarrage

Le code de démarrage vous propose une mise en page prédéfinie pour l'écran du jeu. 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 à vous lancer.

WordsData.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é.

package com.example.android.unscramble.data

const val MAX_NO_OF_WORDS = 10
const val SCORE_INCREASE = 20

// Set with all the words for the Game
val allWords: Set<String> =
   setOf(
       "animal",
       "auto",
       "anecdote",
       "alphabet",
       "all",
       "awesome",
       "arise",
       "balloon",
       "basket",
       "bench",
      // ...
       "zoology",
       "zone",
       "zeal"
)

MainActivity.kt

Ce fichier contient principalement du code généré à partir d'un modèle. Le composable GameScreen est affiché dans le bloc setContent{}.

GameScreen.kt

Tous les composables de l'interface utilisateur sont définis dans le fichier GameScreen.kt. Les sections suivantes vous présentent quelques fonctions modulables.

GameStatus

GameStatus est une fonction composable qui affiche le score du jeu en bas de l'écran. La fonction composable contient un composable Text dans une Card. Pour l'instant, le score est codé en dur pour être 0.

1a7e4472a5638d61.png

// No need to copy, this is included in the starter code.

@Composable
fun GameStatus(score: Int, modifier: Modifier = Modifier) {
    Card(
        modifier = modifier
    ) {
        Text(
            text = stringResource(R.string.score, score),
            style = typography.headlineMedium,
            modifier = Modifier.padding(8.dp)
        )
    }
}

GameLayout

GameLayout est une fonction composable qui affiche la fonctionnalité principale du jeu, qui comprend le mot mélangé, les instructions du jeu et un champ de texte qui accepte les propositions de l'utilisateur.

b6ddb1f07f10df0c.png

Notez que le code GameLayout ci-dessous contient une colonne dans une Card avec trois éléments "enfant" : le mot mélangé, le texte des instructions et le champ de texte pour le mot OutlinedTextField de l'utilisateur. Pour le moment, le mot mélangé est codé en dur pour être scrambleun. Plus tard dans l'atelier, vous implémenterez une fonctionnalité permettant d'afficher un mot du fichier WordsData.kt.

// No need to copy, this is included in the starter code.

@Composable
fun GameLayout(modifier: Modifier = Modifier) {
   val mediumPadding = dimensionResource(R.dimen.padding_medium)
   Card(
       modifier = modifier,
       elevation = CardDefaults.cardElevation(defaultElevation = 5.dp)
   ) {
       Column(
           verticalArrangement = Arrangement.spacedBy(mediumPadding),
           horizontalAlignment = Alignment.CenterHorizontally,
           modifier = Modifier.padding(mediumPadding)
       ) {
           Text(
               modifier = Modifier
                   .clip(shapes.medium)
                   .background(colorScheme.surfaceTint)
                   .padding(horizontal = 10.dp, vertical = 4.dp)
                   .align(alignment = Alignment.End),
               text = stringResource(R.string.word_count, 0),
               style = typography.titleMedium,
               color = colorScheme.onPrimary
           )
           Text(
               text = "scrambleun",
               style = typography.displayMedium
           )
           Text(
               text = stringResource(R.string.instructions),
               textAlign = TextAlign.Center,
               style = typography.titleMedium
           )
           OutlinedTextField(
               value = "",
               singleLine = true,
               shape = shapes.large,
               modifier = Modifier.fillMaxWidth(),
               colors = TextFieldDefaults.textFieldColors(containerColor = colorScheme.surface),
               onValueChange = { },
               label = { Text(stringResource(R.string.enter_your_word)) },
               isError = false,
               keyboardOptions = KeyboardOptions.Default.copy(
                   imeAction = ImeAction.Done
               ),
               keyboardActions = KeyboardActions(
                   onDone = { }
               )
           )
       }
   }
}

Le composable OutlinedTextField est semblable au composable TextField des applications vu dans les précédents ateliers.

Il existe deux types de champs de texte :

  • Les champs de texte remplis
  • Champs de texte encadrés

3df34220c3d177eb.png

Les champs de texte encadrés sont moins visibles que les champs de texte remplis. Lorsqu'ils apparaissent au niveau des formulaires, qui réunissent de nombreux champs de texte, leur accentuation réduite facilite la mise en page.

Dans le code de démarrage, OutlinedTextField ne se met pas à jour lorsque l'utilisateur saisit une proposition. Vous allez mettre à jour cette fonctionnalité au cours de cet atelier.

GameScreen

Le composable GameScreen contient les fonctions composables GameStatus et GameLayout, le titre du jeu, le nombre de mots et les composables des boutons Submit (Envoyer) et Skip (Ignorer).

ac79bf1ed6375a27.png

@Composable
fun GameScreen() {
    val mediumPadding = dimensionResource(R.dimen.padding_medium)

    Column(
        modifier = Modifier
            .verticalScroll(rememberScrollState())
            .padding(mediumPadding),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        Text(
            text = stringResource(R.string.app_name),
            style = typography.titleLarge,
        )

        GameLayout(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentHeight()
                .padding(mediumPadding)
        )
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(mediumPadding),
            verticalArrangement = Arrangement.spacedBy(mediumPadding),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {

            Button(
                modifier = Modifier.fillMaxWidth(),
                onClick = { }
            ) {
                Text(
                    text = stringResource(R.string.submit),
                    fontSize = 16.sp
                )
            }

            OutlinedButton(
                onClick = { },
                modifier = Modifier.fillMaxWidth()
            ) {
                Text(
                    text = stringResource(R.string.skip),
                    fontSize = 16.sp
                )
            }
        }

        GameStatus(score = 0, modifier = Modifier.padding(20.dp))
    }
}

Les événements de clic de bouton ne sont pas implémentés dans le code de démarrage. Vous allez implémenter ces événements au cours de cet atelier.

FinalScoreDialog

Le composable FinalScoreDialog affiche une boîte de dialogue, c'est-à-dire une petite fenêtre qui invite l'utilisateur à effectuer les actions Play Again (Rejouer) ou Exit (Quitter le jeu). Plus tard dans cet atelier de programmation, vous allez implémenter une logique pour afficher cette boîte de dialogue à la fin du jeu.

dba2d9ea62aaa982.png

// No need to copy, this is included in the starter code.

@Composable
private fun FinalScoreDialog(
    score: Int,
    onPlayAgain: () -> Unit,
    modifier: Modifier = Modifier
) {
    val activity = (LocalContext.current as Activity)

    AlertDialog(
        onDismissRequest = {
           // Dismiss the dialog when the user clicks outside the dialog or on the back
           // button. If you want to disable that functionality, simply use an empty
           // onDismissRequest.
        },
        title = { Text(text = stringResource(R.string.congratulations)) },
        text = { Text(text = stringResource(R.string.you_scored, score)) },
        modifier = modifier,
        dismissButton = {
            TextButton(
                onClick = {
                    activity.finish()
                }
            ) {
                Text(text = stringResource(R.string.exit))
            }
        },
        confirmButton = {
            TextButton(onClick = onPlayAgain) {
                Text(text = stringResource(R.string.play_again))
            }
        }
    )
}

4. Notions fondamentales sur l'architecture des applications

L'architecture d'une application vous fournit les principes fondamentaux qui vous aideront à répartir les responsabilités de l'application dans 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. L'architecture peut également faciliter le travail 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

Le principe de contrôle de l'UI à partir d'un modèle indique 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 appli. Ils sont indépendants des éléments d'UI et des composants de votre appli et ne sont donc pas concernés par le cycle de vie de l'appli, ni par les préoccupations qui en découlent.

Compte tenu des principes fondamentaux de l'architecture des applications mentionnés dans la section précédente, chaque application doit comporter au moins deux couches :

  • Couche d'interface utilisateur : une couche qui affiche les données de l'application à l'écran, mais qui reste indépendante des données.
  • Couche de données : une couche qui stocke, récupère et expose les données de l'application.

Vous pouvez ajouter une autre couche, appelée couche de domaine, pour simplifier et réutiliser les interactions entre l'interface utilisateur et les couches de données. Cette couche est facultative et dépasse le cadre de cet atelier.

a4da6fa5c1c9fed5.png

Couche d'interface utilisateur

Le rôle de la couche d'interface utilisateur, ou couche de présentation, est d'afficher les données de l'application à l'écran. Chaque fois que les données changent en raison d'une interaction utilisateur (p. ex. l'appui sur un bouton), l'interface utilisateur doit se mettre à jour pour refléter les modifications.

La couche de l'interface utilisateur contient les composants suivants :

  • Éléments d'interface utilisateur : les composants qui affichent les données à l'écran. Pour créer ces éléments, utilisez Jetpack Compose.
  • Conteneurs d'états : les composants qui contiennent les données, les exposent à l'interface utilisateur et gèrent la logique de l'application. Par exemple, un ViewModel est un conteneur d'état.

6eaee5b38ec247ae.png

ViewModel

Le composant ViewModel conserve et expose l'état utilisé par l'UI. L'état de l'interface utilisateur correspond aux données d'application transformées par le ViewModel. ViewModel permet à votre application de suivre le principe fondamental de l'architecture consistant à contrôler l'UI à partir du modèle.

ViewModel stocke les données liées à l'application qui ne sont pas détruites lorsque le framework Android détruit et recrée l'activité. Contrairement à l'instance d'activité, les objets ViewModel ne sont pas détruits. L'application conserve automatiquement les objets ViewModel lors des modifications de configuration pour que les données détenues par ces objets soient immédiatement disponibles après la recomposition.

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.

État de l'interface utilisateur

L'interface utilisateur correspond à ce que l'utilisateur voit, tandis que l'état de l'interface utilisateur correspond à ce qu'il devrait voir selon l'application. L'interface utilisateur est la représentation visuelle de son état. Toute modification apportée à l'état de l'UI est immédiatement répercutée dans l'interface.

9cfedef1750ddd2c.png

L'UI est le résultat d'une liaison entre des éléments d'interface utilisateur et un état d'interface utilisateur.

// Example of UI state definition, do not copy over

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

Immuabilité

Dans l'exemple ci-dessus, la définition de l'état de l'interface utilisateur est immuable. Les objets immuables vous assurent que plusieurs sources ne modifient pas instantanément l'état de l'appli. Cette protection libère l'interface utilisateur, ce qui lui permet de se concentrer sur un seul rôle : lire l'état et mettre à jour les éléments de l'interface utilisateur en conséquence. Vous ne devez donc jamais modifier directement l'état de l'UI, sauf si l'UI elle-même est la seule source de ses données. Si vous ne respectez pas ce principe, alors il existera plusieurs sources de vérité pour une même information, ce qui entraînera des incohérences au niveau des données et des bugs.

5. Ajouter un ViewModel

Dans cette tâche, vous allez ajouter un ViewModel à votre application pour y stocker l'état de l'UI de votre jeu (mot mélangé, nombre de mots et score). Pour résoudre le problème dans le code de démarrage identifié à la section précédente, vous devez enregistrer les données du jeu dans ViewModel.

  1. Ouvrez build.gradle.kts (Module :app), faites défiler la page jusqu'au bloc dependencies et ajoutez la dépendance suivante pour ViewModel. Cette dépendance permet d'ajouter le ViewModel tenant compte du cycle de vie à votre appli Compose.
dependencies {
// other dependencies

    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
//...
}
  1. Dans le package ui, créez une classe/un fichier Kotlin appelé GameViewModel. Vous l'étendrez à partir de la classe ViewModel.
import androidx.lifecycle.ViewModel

class GameViewModel : ViewModel() {
}
  1. Dans le package ui, ajoutez une classe de modèle pour l'UI d'état, appelée GameUiState. Transformez-la en une classe de données et ajoutez une variable pour le mot mélangé.
data class GameUiState(
   val currentScrambledWord: String = ""
)

StateFlow

StateFlow est un flux observable de conteneur de données qui émet les mises à jour de l'état actuel et du nouvel état. Sa propriété value reflète la valeur de l'état actuel. Pour mettre à jour l'état et l'envoyer au flux, attribuez une nouvelle valeur à la propriété de valeur de la classe MutableStateFlow.

Dans Android, StateFlow fonctionne bien avec les classes qui doivent conserver un état immuable observable.

Un StateFlow peut être exposé à partir de GameUiState afin que les composables puissent écouter les mises à jour de l'état de l'interface utilisateur et faire en sorte que l'état de l'écran survive aux changements de configuration.

Dans la classe GameViewModel, ajoutez la propriété _uiState suivante.

import kotlinx.coroutines.flow.MutableStateFlow

// Game UI state
private val _uiState = MutableStateFlow(GameUiState())

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.

Pour la propriété var, 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. L'exemple suivant vous montre une propriété de support.

//Example code, no need to copy over

// 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 l'application soient privées pour 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.

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. Un type val ne peut pas avoir de setter. Comme il est immuable et en lecture seule, vous pouvez uniquement remplacer la méthode get(). Lorsqu'une classe externe accède à cette propriété, elle renvoie la valeur de _count, qui n'est pas modifiable. Cette propriété de support protège les données de l'application à l'intérieur de ViewModel contre les modifications indésirables et non sécurisées effectuées par les classes externes. Elle permet également aux appelants externes d'accéder à sa valeur de façon sécurisée.
  1. Dans le fichier GameViewModel.kt, ajoutez une propriété de support à uiState nommée _uiState. Nommez la propriété uiState et utilisez le type StateFlow<GameUiState>.

_uiState est désormais accessible et modifiable uniquement dans GameViewModel. L'interface utilisateur peut lire sa valeur à l'aide de la propriété en lecture seule, uiState. Vous pourrez corriger l'erreur d'initialisation à l'étape suivante.

import kotlinx.coroutines.flow.StateFlow

// Game UI state

// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> 
  1. Définissez uiState sur _uiState.asStateFlow().

Le asStateFlow() transforme ce flux d'état modifiable en un flux d'état en lecture seule.

import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

// Game UI state
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()

Afficher un mot mélangé aléatoire

Dans cette tâche, vous allez ajouter des méthodes d'assistance pour choisir un mot aléatoire dans WordsData.kt et mélanger les lettres.

  1. Dans GameViewModel, ajoutez une propriété appelée currentWord de type String pour enregistrer le mot mélangé actuel.
private lateinit var currentWord: String
  1. Ajoutez une méthode d'assistance pour choisir un mot aléatoire dans la liste et le lire en mode aléatoire. Nommez-le pickRandomWordAndShuffle() sans paramètre d'entrée et faites-lui renvoyer une String.
import com.example.unscramble.data.allWords

private fun pickRandomWordAndShuffle(): String {
   // Continue picking up a new random word until you get one that hasn't been used before
   currentWord = allWords.random()
   if (usedWords.contains(currentWord)) {
       return pickRandomWordAndShuffle()
   } else {
       usedWords.add(currentWord)
       return shuffleCurrentWord(currentWord)
   }
}

Android Studio signale une erreur pour la variable et la fonction non définies.

  1. Dans GameViewModel, ajoutez la propriété suivante après la propriété currentWord pour servir d'ensemble modifiable qui stockera les mots utilisés dans le jeu.
// Set of words used in the game
private var usedWords: MutableSet<String> = mutableSetOf()
  1. Ajoutez une autre méthode d'assistance pour lire le mot actuel shuffleCurrentWord() en mode aléatoire, qui accepte une String et renvoie la String dans un ordre aléatoire.
private fun shuffleCurrentWord(word: String): String {
   val tempWord = word.toCharArray()
   // Scramble the word
   tempWord.shuffle()
   while (String(tempWord).equals(word)) {
       tempWord.shuffle()
   }
   return String(tempWord)
}
  1. Ajoutez une fonction d'assistance appelée resetGame() permettant d'initialiser le jeu. Vous utiliserez cette fonction plus tard pour démarrer et redémarrer le jeu. Dans cette fonction, effacez tous les mots de l'ensemble usedWords et initialisez _uiState. Choisissez un nouveau mot pour currentScrambledWord avec pickRandomWordAndShuffle().
fun resetGame() {
   usedWords.clear()
   _uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
  1. Ajoutez un bloc init au GameViewModel et appelez resetGame() à partir de celui-ci.
init {
   resetGame()
}

Lorsque vous compilez votre appli, vous ne verrez toujours aucune modification dans l'interface utilisateur. Vous ne transmettez pas les données du ViewModel aux composables de GameScreen.

6. Structurer votre interface utilisateur Compose

Dans Compose, le seul moyen de mettre à jour l'UI est de modifier l'état de l'appli. Vous pouvez contrôler l'état de l'interface utilisateur. À chaque modification de l'interface utilisateur, Compose recrée les parties de l'arborescence qui ont été modifiées. Les composables peuvent accepter des événements "state" et "expose". Par exemple, un objet TextField/OutlinedTextField accepte une valeur et expose un rappel onValueChange qui demande au gestionnaire de rappel de modifier la valeur.

//Example code no need to copy over

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

Étant donné que les composables acceptent des événements "state" et "expose", le modèle de flux de données unidirectionnel s'adapte bien à Jetpack Compose. Ce guide explique comment implémenter le modèle de flux de données unidirectionnel, implémenter des événements et des conteneurs d'état, et utiliser les ViewModel dans Compose.

Flux de données unidirectionnel

Un flux de données unidirectionnel (UDF) est un modèle de conception dans lequel l'état redescend et les événements remontent. En suivant le flux de données unidirectionnel, vous pouvez dissocier les composables qui affichent l'état dans l'interface utilisateur des parties de votre application qui stockent et modifient l'état.

La boucle de mise à jour de l'UI pour une application utilisant un flux de données unidirectionnel se présente comme suit :

  • Événement : une partie de l'interface utilisateur génère un événement et le fait remonter (p. ex. un clic sur le bouton transmis au ViewModel qui va le gérer), ou un événement transmis à partir d'autres couches de votre application (p. ex. un message qui indique l'expiration de la session utilisateur).
  • Modification d'état : un gestionnaire d'événements peut modifier l'état.
  • État de l'affichage : le conteneur d'état transmet l'état, et l'interface utilisateur l'affiche.

61eb7bcdcff42227.png

L'utilisation du modèle de flux de données unidirectionnel (UDF) pour l'architecture des applications a les conséquences suivantes :

  • ViewModel conserve et expose l'état utilisé par l'UI.
  • L'état de l'interface utilisateur correspond aux données d'application transformées par ViewModel.
  • L'interface utilisateur informe ViewModel des événements d'utilisateurs.
  • ViewModel gère les actions de l'utilisateur et met à jour l'état.
  • L'état mis à jour est renvoyé à l'interface utilisateur, puis affiché.
  • Ce processus se répète pour tout événement qui provoque une mutation d'état.

Transmettre les données

Transmettez l'instance du ViewModel à l'interface utilisateur, c'est-à-dire de GameViewModel à GameScreen() dans le fichier GameScreen.kt. Dans GameScreen(), utilisez l'instance du ViewModel pour accéder à uiState à l'aide de collectAsState().

La fonction collectAsState() collecte les valeurs de ce StateFlow et représente sa dernière valeur à l'aide de State. StateFlow.value est utilisée comme valeur initiale. Chaque fois qu'une nouvelle valeur est ajoutée dans StateFlow, la valeur renvoyée State se met à jour, ce qui entraîne la recomposition de chaque utilisation de State.value.

  1. Dans la fonction GameScreen, transmettez un deuxième argument du type GameViewModel avec une valeur par défaut de viewModel().
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun GameScreen(
   gameViewModel: GameViewModel = viewModel()
) {
   // ...
}

de93b81a92416c23.png

  1. Dans la fonction GameScreen(), ajoutez une variable appelée gameUiState. Utilisez le délégué by et appelez collectAsState() sur uiState.

Grâce à cette approche, lorsque la valeur uiState est modifiée, la recomposition a lieu pour les composables utilisant la valeur gameUiState.

import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue

@Composable
fun GameScreen(
   // ...
) {
   val gameUiState by gameViewModel.uiState.collectAsState()
   // ...
}
  1. Transmettez gameUiState.currentScrambledWord au composable GameLayout(). Vous ajouterez l'argument à une étape ultérieure. Pour le moment, vous pouvez ignorer l'erreur.
GameLayout(
   currentScrambledWord = gameUiState.currentScrambledWord,
   modifier = Modifier
       .fillMaxWidth()
       .wrapContentHeight()
       .padding(mediumPadding)
)
  1. Ajoutez currentScrambledWord comme autre paramètre à la fonction composable GameLayout().
@Composable
fun GameLayout(
   currentScrambledWord: String,
   modifier: Modifier = Modifier
) {
}
  1. Mettez à jour la fonction composable GameLayout() pour afficher currentScrambledWord. Définissez le paramètre text du premier champ de texte de la colonne sur currentScrambledWord.
@Composable
fun GameLayout(
   // ...
) {
   Column(
       verticalArrangement = Arrangement.spacedBy(24.dp)
   ) {
       Text(
           text = currentScrambledWord,
           fontSize = 45.sp,
           modifier = modifier.align(Alignment.CenterHorizontally)
       )
    //... 
    }
}
  1. Exécutez et créez l'application. Le mot mélangé doit s'afficher.

6d93a8e1ba5dad6f.png

Afficher le mot à deviner

Dans le composable GameLayout(), la mise à jour de la proposition de l'utilisateur est l'un des rappels d'événement qui passent de GameScreen à ViewModel. Les données gameViewModel.userGuess seront transférées de ViewModel vers GameScreen.

Les rappels d'événement, onKeyboardDone et onUserGuessChanged sont transmis de l'UI à ViewModel

  1. Dans le fichier GameScreen.kt, dans le composable GameLayout(), définissez onValueChange sur onUserGuessChanged et onKeyboardDone() sur l'action clavier onDone. Vous corrigerez les erreurs à l'étape suivante.
OutlinedTextField(
   value = "",
   singleLine = true,
   modifier = Modifier.fillMaxWidth(),
   onValueChange = onUserGuessChanged,
   label = { Text(stringResource(R.string.enter_your_word)) },
   isError = false,
   keyboardOptions = KeyboardOptions.Default.copy(
       imeAction = ImeAction.Done
   ),
   keyboardActions = KeyboardActions(
       onDone = { onKeyboardDone() }
   ),
  1. Dans la fonction composable GameLayout(), ajoutez deux autres arguments : le lambda onUserGuessChanged accepte un argument String et ne renvoie rien, tandis que onKeyboardDone ne prend rien et ne renvoie rien.
@Composable
fun GameLayout(
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   currentScrambledWord: String,
   modifier: Modifier = Modifier,
   ) {
}
  1. Dans l'appel de fonction GameLayout(), ajoutez des arguments lambda pour onUserGuessChanged et onKeyboardDone.
GameLayout(
   onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
   onKeyboardDone = { },
   currentScrambledWord = gameUiState.currentScrambledWord,
)

Vous allez bientôt définir la méthode updateUserGuess dans GameViewModel.

  1. Dans le fichier GameViewModel.kt, ajoutez une méthode appelée updateUserGuess() qui accepte un argument String, le mot proposé par l'utilisateur. Dans la fonction, mettez à jour le userGuess avec la valeur guessedWord transmise.
  fun updateUserGuess(guessedWord: String){
     userGuess = guessedWord
  }

Vous allez ensuite ajouter userGuess dans le ViewModel.

  1. Dans le fichier GameViewModel.kt, ajoutez une propriété var appelée userGuess. Utilisez mutableStateOf() pour que Compose observe cette valeur et définisse la valeur initiale sur "".
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

var userGuess by mutableStateOf("")
   private set
  1. Dans le fichier GameScreen.kt, dans GameLayout(), ajoutez un autre paramètre String pour userGuess. Définissez le paramètre value de OutlinedTextField sur userGuess.
fun GameLayout(
   currentScrambledWord: String,
   userGuess: String,
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   modifier: Modifier = Modifier
) {
   Column(
       verticalArrangement = Arrangement.spacedBy(24.dp)
   ) {
       //...
       OutlinedTextField(
           value = userGuess,
           //..
       )
   }
}
  1. Dans la fonction GameScreen, mettez à jour l'appel de fonction GameLayout() pour inclure le paramètre userGuess.
GameLayout(
   currentScrambledWord = gameUiState.currentScrambledWord,
   userGuess = gameViewModel.userGuess,
   onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
   onKeyboardDone = { },
   //...
)
  1. Créez et exécutez votre application.
  2. Essayez de deviner le mot et entrez une proposition. Le champ de texte permet d'afficher la proposition de l'utilisateur.

ed10c7f522495a.png

7. Vérifier le mot à deviner et modifier le score

Dans cette tâche, vous implémenterez une méthode permettant de vérifier la proposition de l'utilisateur, puis de mettre à jour le score du jeu ou d'afficher une erreur. Vous mettrez à jour l'interface d'état du jeu pour afficher le nouveau score et le nouveau mot plus tard.

  1. Dans GameViewModel, ajoutez une autre méthode appelée checkUserGuess().
  2. Dans la fonction checkUserGuess(), ajoutez un bloc if else pour vérifier que la proposition de l'utilisateur est identique à celle de currentWord. Réinitialisez userGuess pour vider la chaîne.
fun checkUserGuess() {
   
   if (userGuess.equals(currentWord, ignoreCase = true)) {
   } else {
   }
   // Reset user guess
   updateUserGuess("")
}
  1. Si la réponse de l'utilisateur est incorrecte, définissez isGuessedWordWrong sur true. MutableStateFlow<T>. update() met à jour MutableStateFlow.value avec la valeur spécifiée.
import kotlinx.coroutines.flow.update

   if (userGuess.equals(currentWord, ignoreCase = true)) {
   } else {
       // User's guess is wrong, show an error
       _uiState.update { currentState ->
           currentState.copy(isGuessedWordWrong = true)
       }
   }
  1. Dans la classe GameUiState, ajoutez un Boolean appelé isGuessedWordWrong et initialisez-le sur false.
data class GameUiState(
   val currentScrambledWord: String = "",
   val isGuessedWordWrong: Boolean = false,
)

Vous transmettez ensuite le rappel d'événement checkUserGuess() de GameScreen à ViewModel lorsque l'utilisateur clique sur le bouton Submit (Envoyer) ou sur la touche "OK" du clavier. Transmettez les données, gameUiState.isGuessedWordWrong de ViewModel à GameScreen, pour définir l'erreur dans le champ de texte.

7f05d04164aa4646.png

  1. Dans le fichier GameScreen.kt, à la fin de la fonction composable GameScreen(), appelez gameViewModel.checkUserGuess() dans l'expression lambda onClick du bouton Submit (Envoyer).
Button(
   modifier = modifier
       .fillMaxWidth()
       .weight(1f)
       .padding(start = 8.dp),
   onClick = { gameViewModel.checkUserGuess() }
) {
   Text(stringResource(R.string.submit))
}
  1. Dans la fonction composable GameScreen(), mettez à jour l'appel de fonction GameLayout() pour transmettre gameViewModel.checkUserGuess() dans l'expression lambda onKeyboardDone.
GameLayout(
   currentScrambledWord = gameUiState.currentScrambledWord,
   userGuess = gameViewModel.userGuess,
   onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
   onKeyboardDone = { gameViewModel.checkUserGuess() }
)
  1. Dans la fonction composable GameLayout(), ajoutez un paramètre de fonction pour Boolean : isGuessWrong. Définissez le paramètre isError de OutlinedTextField sur isGuessWrong pour afficher l'erreur dans le champ de texte si la proposition est incorrecte.
fun GameLayout(
   currentScrambledWord: String,
   isGuessWrong: Boolean,
   userGuess: String,
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   modifier: Modifier = Modifier
) {
   Column(
       // ,...
       OutlinedTextField(
           // ...
           isError = isGuessWrong,
           keyboardOptions = KeyboardOptions.Default.copy(
               imeAction = ImeAction.Done
           ),
           keyboardActions = KeyboardActions(
               onDone = { onKeyboardDone() }
           ),
       )
}
}
  1. Dans la fonction composable GameScreen(), mettez à jour l'appel de la fonction GameLayout() pour transmettre isGuessWrong.
GameLayout(
   currentScrambledWord = gameUiState.currentScrambledWord,
   userGuess = gameViewModel.userGuess,
   onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
   onKeyboardDone = { gameViewModel.checkUserGuess() },
   isGuessWrong = gameUiState.isGuessedWordWrong,
   // ...
)
  1. Créez et exécutez votre application.
  2. Saisissez une proposition incorrecte et cliquez sur Submit (Envoyer). Notez que le champ de texte devient rouge pour indiquer une erreur.

a1bc55781d627b38.png

Le libellé du champ de texte indique toujours "Enter your word" (Saisissez votre mot). Pour le rendre plus ergonomique, vous devez ajouter un texte d'erreur qui indique que le mot est incorrect.

  1. Dans le fichier GameScreen.kt, dans le composable GameLayout(), mettez à jour le paramètre de libellé du champ de texte en fonction de isGuessWrong, comme suit :
OutlinedTextField(
   // ...
   label = {
       if (isGuessWrong) {
           Text(stringResource(R.string.wrong_guess))
       } else {
           Text(stringResource(R.string.enter_your_word))
       }
   },
   // ...
)
  1. Dans le fichier strings.xml, ajoutez une chaîne au libellé d'erreur.
<string name="wrong_guess">Wrong Guess!</string>
  1. Créez et exécutez à nouveau votre application.
  2. Saisissez une proposition incorrecte et cliquez sur Submit (Envoyer). Observez le libellé d'erreur.

8c17eb61e9305d49.png

8. Modifier le score et le nombre de mots

Dans cette tâche, vous allez mettre à jour le score et le nombre de mots au fil de la partie de l'utilisateur. Le score doit faire partie de _ uiState.

  1. Dans GameUiState, ajoutez une variable score et initialisez-la avec la valeur zéro.
data class GameUiState(
   val currentScrambledWord: String = "",
   val isGuessedWordWrong: Boolean = false,
   val score: Int = 0
)
  1. Pour mettre à jour la valeur du score, dans GameViewModel, dans la fonction checkUserGuess(), dans la condition if, lorsque la réponse de l'utilisateur est correcte, augmentez la valeur score.
import com.example.unscramble.data.SCORE_INCREASE

fun checkUserGuess() {
   if (userGuess.equals(currentWord, ignoreCase = true)) {
       // User's guess is correct, increase the score
       val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
   } else {
       //...
   }
}
  1. Dans GameViewModel, ajoutez une autre méthode appelée updateGameState pour mettre à jour le score, augmenter le nombre de mots actuel et choisir un nouveau mot dans le fichier WordsData.kt. Ajoutez un Int nommé updatedScore en guise de paramètre. Mettez à jour les variables d'interface d'état du jeu comme suit :
private fun updateGameState(updatedScore: Int) {
   _uiState.update { currentState ->
       currentState.copy(
           isGuessedWordWrong = false,
           currentScrambledWord = pickRandomWordAndShuffle(),
           score = updatedScore
       )
   }
}
  1. Dans la fonction checkUserGuess(), si la réponse de l'utilisateur est correcte, appelez updateGameState avec le score mis à jour pour préparer le jeu pour le prochain tour.
fun checkUserGuess() {
   if (userGuess.equals(currentWord, ignoreCase = true)) {
       // User's guess is correct, increase the score
       // and call updateGameState() to prepare the game for next round
       val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
       updateGameState(updatedScore)
   } else {
       //...
   }
}

La fonction checkUserGuess() terminée doit se présenter comme suit :

fun checkUserGuess() {
   if (userGuess.equals(currentWord, ignoreCase = true)) {
       // User's guess is correct, increase the score
       // and call updateGameState() to prepare the game for next round
       val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
       updateGameState(updatedScore)
   } else {
       // User's guess is wrong, show an error
       _uiState.update { currentState ->
           currentState.copy(isGuessedWordWrong = true)
       }
   }
   // Reset user guess
   updateUserGuess("")
}

Ensuite, comme pour le score, vous devez mettre à jour le nombre de mots joués.

  1. Ajoutez une autre variable pour le nombre indiqué dans GameUiState. Appelez-la currentWordCount et initialisez-la à 1.
data class GameUiState(
   val currentScrambledWord: String = "",
   val currentWordCount: Int = 1,
   val score: Int = 0,
   val isGuessedWordWrong: Boolean = false,
)
  1. Dans la fonction updateGameState() du fichier GameViewModel.kt, augmentez le nombre de mots, comme indiqué ci-dessous. La fonction updateGameState() est appelée pour préparer le jeu pour le prochain tour.
private fun updateGameState(updatedScore: Int) {
   _uiState.update { currentState ->
       currentState.copy(
           //...
           currentWordCount = currentState.currentWordCount.inc(),
           )
   }
}

Score de réussite et nombre de mots

Procédez comme suit pour transmettre les données de score et de nombre de mots de ViewModel à GameScreen.

546e101980380f80.png

  1. Dans le fichier GameScreen.kt, dans la fonction composable GameLayout(), ajoutez le nombre de mots en tant qu'argument et transmettez les arguments de format wordCount à l'élément de texte.
fun GameLayout(
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   wordCount: Int,
   //...
) {
   //...

   Card(
       //...
   ) {
       Column(
           // ...
       ) {
           Text(
               //..
               text = stringResource(R.string.word_count, wordCount),
               style = typography.titleMedium,
               color = colorScheme.onPrimary
           )

// ...

}
  1. Mettez à jour l'appel de fonction GameLayout() pour inclure le nombre de mots.
GameLayout(
   userGuess = gameViewModel.userGuess,
   wordCount = gameUiState.currentWordCount,
   //...
)
  1. Dans la fonction composable GameScreen(), mettez à jour l'appel de fonction GameStatus() pour inclure les paramètres score. Transmettez le score à partir de gameUiState.
GameStatus(score = gameUiState.score, modifier = Modifier.padding(20.dp))
  1. Créez et exécutez l'application.
  2. Saisissez le mot à deviner et cliquez sur Submit (Envoyer). Le score et le nombre de mots sont mis à jour.
  3. Cliquez sur Skip (Ignorer). Rien ne change.

Pour implémenter la fonctionnalité "Ignorer", vous devez transmettre le rappel de l'événement "Ignorer" à GameViewModel.

  1. Dans le fichier GameScreen.kt, dans la fonction modulable GameScreen(), appelez gameViewModel.skipWord() dans l'expression lambda onClick.

Android Studio affiche une erreur, car vous n'avez pas encore implémenté la fonction. Vous allez corriger cette erreur à l'étape suivante en ajoutant la méthode skipWord(). Lorsque l'utilisateur ignore un mot, vous devez mettre à jour les variables du jeu et préparer le jeu pour le prochain tour.

OutlinedButton(
   onClick = { gameViewModel.skipWord() },
   modifier = Modifier.fillMaxWidth()
) {
   //...
}
  1. Dans GameViewModel, ajoutez la méthode skipWord().
  2. Dans la fonction skipWord(), appelez updateGameState() pour transmettre le score et réinitialiser la proposition de l'utilisateur.
fun skipWord() {
   updateGameState(_uiState.value.score)
   // Reset user guess
   updateUserGuess("")
}
  1. Exécutez votre application et jouez. Vous devriez à présent pouvoir ignorer des mots.

e87bd75ba1269e96.png

Vous pouvez toujours jouer au-delà de 10 mots. Lors de votre prochaine tâche, vous vous occuperez de la dernière partie du jeu.

9. Gérer le dernier tour du jeu

Dans l'implémentation actuelle, les utilisateurs peuvent ignorer ou jouer au-delà de 10 mots. Dans cette tâche, vous allez ajouter une logique pour terminer le jeu.

d3fd67d92c5d3c35.png

Pour implémenter la logique de fin de jeu, vous devez d'abord vérifier si l'utilisateur a atteint le nombre maximal de mots.

  1. Dans GameViewModel, ajoutez un bloc if-else et déplacez le corps de la fonction existante dans le bloc else.
  2. Ajoutez une condition if pour vérifier que la taille de usedWords est égale à MAX_NO_OF_WORDS.
import com.example.android.unscramble.data.MAX_NO_OF_WORDS

private fun updateGameState(updatedScore: Int) {
   if (usedWords.size == MAX_NO_OF_WORDS){
       //Last round in the game
   } else{
       // Normal round in the game
       _uiState.update { currentState ->
           currentState.copy(
               isGuessedWordWrong = false,
               currentScrambledWord = pickRandomWordAndShuffle(),
               currentWordCount = currentState.currentWordCount.inc(),
               score = updatedScore
           )
       }
   }
}
  1. Dans le bloc if, ajoutez l'indicateur Boolean isGameOver et attribuez la valeur true à l'indicateur pour signaler la fin du jeu.
  2. Mettez à jour score et réinitialisez isGuessedWordWrong dans le bloc if. Le code suivant montre à quoi doit ressembler votre fonction :
private fun updateGameState(updatedScore: Int) {
   if (usedWords.size == MAX_NO_OF_WORDS){
       //Last round in the game, update isGameOver to true, don't pick a new word
       _uiState.update { currentState ->
           currentState.copy(
               isGuessedWordWrong = false,
               score = updatedScore,
               isGameOver = true
           )
       }
   } else{
       // Normal round in the game
       _uiState.update { currentState ->
           currentState.copy(
               isGuessedWordWrong = false,
               currentScrambledWord = pickRandomWordAndShuffle(),
               currentWordCount = currentState.currentWordCount.inc(),
               score = updatedScore
           )
       }
   }
}
  1. Dans GameUiState, ajoutez la variable Boolean isGameOver et attribuez-lui ensuite la valeur false.
data class GameUiState(
   val currentScrambledWord: String = "",
   val currentWordCount: Int = 1,
   val score: Int = 0,
   val isGuessedWordWrong: Boolean = false,
   val isGameOver: Boolean = false
)
  1. Exécutez votre application et jouez. Vous ne pouvez pas jouer plus de 10 mots.

ac8a12e66111f071.png

Une fois la partie terminée, nous vous recommandons d'en informer l'utilisateur et de lui demander s'il souhaite rejouer. Vous implémenterez cette fonctionnalité dans votre prochaine tâche.

Afficher la boîte de dialogue de fin de jeu

Dans cette tâche, vous allez transmettre des données isGameOver à GameScreen depuis le ViewModel et l'utiliser pour afficher une boîte de dialogue d'alerte incluant des options pour arrêter ou redémarrer le jeu.

Une boîte de dialogue est une petite fenêtre 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

eb6edcdd0818b900.png

  1. Conteneur
  2. Icône (facultatif)
  3. Titre (facultatif)
  4. Texte d'accompagnement
  5. Séparateur (facultatif)
  6. Actions

Le fichier GameScreen.kt du code de démarrage fournit déjà une fonction affichant une boîte de dialogue d'alerte. Celle-ci inclut des options permettant de quitter ou redémarrer le jeu.

78d43c7aa01b414d.png

@Composable
private fun FinalScoreDialog(
   onPlayAgain: () -> Unit,
   modifier: Modifier = Modifier
) {
   val activity = (LocalContext.current as Activity)

   AlertDialog(
       onDismissRequest = {
           // Dismiss the dialog when the user clicks outside the dialog or on the back
           // button. If you want to disable that functionality, simply use an empty
           // onDismissRequest.
       },
       title = { Text(stringResource(R.string.congratulations)) },
       text = { Text(stringResource(R.string.you_scored, 0)) },
       modifier = modifier,
       dismissButton = {
           TextButton(
               onClick = {
                   activity.finish()
               }
           ) {
               Text(text = stringResource(R.string.exit))
           }
       },
       confirmButton = {
           TextButton(
               onClick = {
                   onPlayAgain()
               }
           ) {
               Text(text = stringResource(R.string.play_again))
           }
       }
   )
}

Dans cette fonction, les paramètres title et text affichent le titre et le texte d'accompagnement dans la boîte de dialogue d'alerte. dismissButton et confirmButton sont les boutons de texte. Dans le paramètre dismissButton, vous affichez le texte Exit (Quitter) et arrêtez l'application en terminant l'activité. Dans le paramètre confirmButton, vous redémarrez le jeu et affichez le texte Play Again (Rejouer).

a24f59b84a178d9b.png

  1. Dans la fonction FinalScoreDialog() du fichier GameScreen.kt, notez le paramètre du score afin d'afficher le score du jeu dans la boîte de dialogue d'alerte.
@Composable
private fun FinalScoreDialog(
   score: Int,
   onPlayAgain: () -> Unit,
   modifier: Modifier = Modifier
) {
  1. Dans la fonction FinalScoreDialog(), notez l'utilisation de l'expression lambda du paramètre text pour utiliser score comme argument de format du texte de la boîte de dialogue.
text = { Text(stringResource(R.string.you_scored, score)) }
  1. Dans le fichier GameScreen.kt, à la fin de la fonction composable GameScreen(), après le bloc Column, ajoutez une condition if pour vérifier gameUiState.isGameOver.
  2. Dans le bloc if, affichez la boîte de dialogue d'alerte. Appelez FinalScoreDialog() en transmettant les éléments score et gameViewModel.resetGame() pour le rappel de l'événement onPlayAgain.
if (gameUiState.isGameOver) {
   FinalScoreDialog(
       score = gameUiState.score,
       onPlayAgain = { gameViewModel.resetGame() }
   )
}

resetGame() est un rappel d'événement transmis de GameScreen à ViewModel.

  1. Dans le fichier GameViewModel.kt, le rappel de la fonction resetGame() initialise _uiState et choisit un nouveau mot.
fun resetGame() {
   usedWords.clear()
   _uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
  1. Créez et exécutez votre application.
  2. Jouez au jeu jusqu'à la fin et consultez la boîte de dialogue d'alerte qui affiche les options Exit (Quitter) ou Play Again (Rejouer). Essayez chacune de ces options.

c6727347fe0db265.png

10. L'état dans la rotation des appareils

Dans les précédents ateliers de programmation, vous avez découvert les modifications de configuration dans Android. Lorsqu'une modification de configuration se produit, Android redémarre toute l'activité, en exécutant tous les rappels de démarrage du cycle de vie.

Le ViewModel stocke les données liées à l'application qui ne sont pas détruites lorsque le framework Android détruit et recrée l'activité. Les objets ViewModel sont conservés automatiquement et ne sont pas détruits, comme l'instance d'activité lorsque la configuration change. Les données qu'ils contiennent sont immédiatement disponibles après la recomposition.

Dans cette tâche, vous allez vérifier si l'application conserve StateUI lors d'un changement de configuration.

  1. Exécutez l'application et commencez à jouer. Faites pivoter l'appareil du mode Portrait au mode Paysage, ou inversement.
  2. Notez que les données enregistrées dans l'interface utilisateur d'état de ViewModel sont conservées lors du changement de configuration.

4a63084643723724.png

4134470d435581dd.png

11. Télécharger le code de solution

Pour télécharger le code de cet atelier de programmation terminé, utilisez les commandes Git suivantes :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git
$ cd basic-android-kotlin-compose-training-unscramble
$ git checkout viewmodel

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.

Si vous le souhaitez, vous pouvez consulter le code de solution de cet atelier de programmation sur GitHub.

12. Conclusion

Félicitations ! Vous avez terminé l'atelier de programmation. Vous comprenez maintenant que 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.

N'oubliez pas de partager le fruit de vos efforts sur les réseaux sociaux avec le hashtag #AndroidBasics.

En savoir plus