Présentation de l'état dans Compose

1. Avant de commencer

Cet atelier de programmation vous présente la notion d'état et la façon dont Jetpack Compose peut l'utiliser et le manipuler.

En pratique, on entend par "état" toute valeur pouvant évoluer au fil du temps dans une application. Cette définition est très vaste et s'applique autant à une base de données qu'à une variable de votre application. Vous vous familiariserez avec les bases de données dans une prochaine leçon. Pour l'instant, rappelez-vous juste qu'une base de données est une collection organisée d'informations structurées, comme les fichiers présents sur votre ordinateur.

Toutes les applications Android présentent des états à l'utilisateur. Voici quelques exemples d'états que vous pouvez trouver dans les applications Android :

  • Message qui s'affiche quand une connexion réseau n'a pas pu être établie.
  • Formulaires, comme les formulaires d'inscription. L'état peut indiquer que le formulaire a été renseigné et envoyé.
  • Commandes tactiles telles que les boutons. L'état peut indiquer que le bouton n'a pas été utilisé, qu'il est en cours d'utilisation, comme sur un écran d'animation, ou qu'il a été utilisé, comme avec une action onClick.

Dans cet atelier de programmation, vous découvrirez comment envisager les états et comment les exploiter lorsque vous utilisez Compose. Pour ce faire, vous créerez une application de calcul du pourboire appelée "Tip Time" à l'aide de ces éléments d'interface utilisateur Compose intégrés :

  • Un composable TextField pour saisir et modifier le texte.
  • Un composable Text pour afficher le texte.
  • Un composable Spacer pour afficher un espace vide entre les éléments d'interface utilisateur.

À la fin de cet atelier de programmation, vous aurez créé un outil interactif de calcul de pourboire. Il déterminera automatiquement le montant du pourboire lorsque vous saisirez le coût du service. Cette image illustre la version finale de l'application :

d6c6ed627ffa4.png

Conditions préalables

  • Vous connaissez les bases de Compose (annotation @Composable, entre autres).
  • Vous connaissez les principes fondamentaux des mises en page Compose, comme les composables de mise en page Row et Column.
  • Vous connaissez les bases des modificateurs, tels que la fonction Modifier.padding().
  • Vous connaissez bien le composable Text.

Points abordés

  • Rôle des états dans une interface utilisateur
  • Utilisation de l'état par Compose pour afficher les données
  • Ajout d'une zone de texte à l'application
  • Comment hisser un état

Objectifs de l'atelier

  • Application de calcul du pourboire appelée "Tip Time" qui détermine le montant du pourboire en fonction du coût du service.

Ce dont vous avez besoin

  • Un ordinateur avec accès à Internet et un navigateur Web
  • Connaissance de Kotlin
  • La dernière version d'Android Studio

2. Premiers pas

  1. Découvrez l'outil Google de calcul des pourboires en ligne. Notez qu'il ne s'agit que d'un exemple. Il ne s'agit pas de l'application Android que vous créerez dans ce cours.

b7d1ae0f60c4ba2e.png 19b877bbeca9ef9.png

  1. Saisissez des valeurs différentes dans les champs Bill (Facture) et Tip % (Pourcentage de pourboire). Les valeurs du pourboire et les montants totaux changent.

c793ff18ad2060e9.png

Notez que lorsque vous saisissez les valeurs, le pourboire et le total sont mis à jour. À la fin de cet atelier, vous développerez une application de calcul de pourboire similaire sur Android.

Dans ce parcours, vous créerez une simple application Android de calcul de pourboire.

Les développeurs travaillent souvent de cette manière : ils préparent une version simple et opérationnelle de l'application (même si elle ne semble pas très esthétique), puis ils y ajoutent des fonctionnalités et la rendent plus agréable à l'œil par la suite.

À la fin de cet atelier de programmation, votre application de calcul de pourboire ressemblera à ces captures d'écran. Lorsque l'utilisateur saisit un montant de facture, votre application affiche une suggestion de pourboire. Pour le moment, le pourcentage de pourboire est codé en dur sur une valeur correspondant à 15 %. Dans l'atelier de programmation suivant, vous continuerez à travailler sur votre application et ajouterez d'autres fonctionnalités, comme la personnalisation du pourcentage de pourboire.

3. Obtenir le code de démarrage

Le code de démarrage est le code prédéfini qui peut servir de point de départ pour un nouveau projet. Il peut également vous aider à vous concentrer sur les nouveaux concepts présentés dans cet atelier de programmation.

Pour utiliser le code de démarrage, commencez par le télécharger ici :

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

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

Vous pouvez parcourir le code dans le dépôt GitHub TipTime.

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'application sur un appareil Android ou sur un émulateur.
  3. Deux composants de texte s'affichent. L'un sert simplement de libellé, et l'autre affiche le montant du pourboire.

78e9ba2ba645b19e.png

Explication du code de démarrage

Le code de démarrage contient déjà les composables de texte. Dans cet atelier, vous allez ajouter un champ de texte pour recevoir les entrées des utilisateurs. Voici une présentation rapide de certains fichiers pour vous aider à vous lancer.

res > values > strings.xml

<resources>
   <string name="app_name">Tip Time</string>
   <string name="calculate_tip">Calculate Tip</string>
   <string name="bill_amount">Bill Amount</string>
   <string name="tip_amount">Tip Amount: %s</string>
</resources>

Il s'agit du fichier string.xml dans les ressources. Il contient toutes les chaînes que vous utiliserez dans cette application.

MainActivity

Ce fichier contient principalement du code généré à partir du modèle, ainsi que les fonctions suivantes :

  • La fonction TipTimeLayout() contient un élément Column avec les deux composables de texte que vous voyez dans les captures d'écran. Un composable spacer permet également d'ajouter de l'espace pour des raisons esthétiques.
  • La fonction calculateTip() reçoit le montant de la facture et calcule un pourboire de 15 %. Le paramètre tipPercent est défini sur une valeur d'argument par défaut de 15.0. Par conséquent, le pourboire par défaut est actuellement de 15 %. Dans l'atelier de programmation suivant, le montant du pourboire dépendra du choix de l'utilisateur.
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = stringResource(R.string.calculate_tip),
            modifier = Modifier
                .padding(bottom = 16.dp, top = 40.dp)
                .align(alignment = Alignment.Start)
        )
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        Spacer(modifier = Modifier.height(150.dp))
    }
}
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
   val tip = tipPercent / 100 * amount
   return NumberFormat.getCurrencyInstance().format(tip)
}

Dans le bloc Surface() de la fonction onCreate(), la fonction TipTimeLayout() est appelée. La mise en page de l'application s'affiche sur l'appareil ou l'émulateur.

override fun onCreate(savedInstanceState: Bundle?) {
   //...
   setContent {
       TipTimeTheme {
           Surface(
           //...
           ) {
               TipTimeLayout()
           }
       }
   }
}

Dans le bloc TipTimeTheme de la fonction TipTimeLayoutPreview(), la fonction TipTimeLayout() est appelée. La mise en page de l'application s'affiche dans Design (Conception) et dans le volet Split (Diviser).

@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
   TipTimeTheme {
       TipTimeLayout()
   }
}

83ddead6f1179fbc.png

Recevoir les entrées de l'utilisateur

Dans cette section, vous ajouterez l'élément d'interface qui permettra à l'utilisateur de saisir le montant de la facture dans l'application, comme illustré dans l'image ci-dessous :

cc51b428369a893d.png

Votre application utilise un style et un thème personnalisés.

Les styles et les thèmes des ensembles d'attributs qui spécifient l'apparence d'un seul élément d'interface utilisateur. Un style peut par exemple spécifier la couleur de la police, sa taille et sa couleur d'arrière-plan, et ces valeurs seront appliquées à l'ensemble de l'application. Dans d'autres ateliers de programmation, nous verrons comment implémenter ces éléments dans votre application. Pour l'instant, cela a déjà été fait afin d'obtenir un rendu plus esthétique.

Pour mieux visualiser la différence, voici une comparaison côte à côte du rendu final de l'application avec et sans thème personnalisé.

Sans thème personnalisé.

Avec un thème personnalisé.

La fonction composable TextField permet à l'utilisateur de saisir du texte dans une application. Dans l'image ci-dessous, vous pouvez voir par exemple la zone de texte sur l'écran de connexion de l'application Gmail :

Écran de téléphone avec l'application Gmail avec un champ de texte pour l'e-mail

Ajoutez le composable TextField à l'application :

  1. Dans le fichier MainActivity.kt, ajoutez une fonction composable EditNumberField() qui accepte un paramètre Modifier.
  2. Dans le corps de la fonction EditNumberField(), sous TipTimeLayout(), ajoutez un élément TextField qui accepte un paramètre nommé value défini sur une chaîne vide et un paramètre nommé onValueChange défini sur une expression lambda vide :
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   TextField(
      value = "",
      onValueChange = {},
      modifier = modifier
   )
}
  1. Notez les paramètres que vous avez transmis :
  • Le paramètre value est une zone de texte qui affiche la valeur de chaîne que vous transmettez ici.
  • Le paramètre onValueChange est le rappel lambda qui est déclenché lorsque l'utilisateur saisit du texte dans la zone à cet effet.
  1. Importez cette fonction :
import androidx.compose.material3.TextField
  1. Dans le composable TipTimeLayout(), sur la ligne située après la première fonction composable de texte, appelez la fonction EditNumberField() en transmettant le modificateur suivant :
import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun TipTimeLayout() {
   Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
   ) {
       Text(
           ...
       )
       EditNumberField(modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth())
       Text(
           ...
       )
       ...
   }
}

La zone de texte s'affiche à l'écran.

  1. Dans le volet Design (Conception), vous devriez voir le texte Calculate Tip, une zone de texte vide et le composable de texte Tip Amount.

2f2ef25c956e357f.png

4. Utiliser l'état dans Compose

Dans une application, l'état correspond à toute valeur susceptible de changer au fil du temps. Dans cette activité, il s'agit du montant de la facture.

Ajoutez une variable pour stocker cet état :

  1. Au début de la fonction EditNumberField(), utilisez le mot clé val pour ajouter une variable amountInput et lui attribuer la valeur "0" :
val amountInput = "0"

Il s'agit de l'état de l'application correspondant au montant de la facture.

  1. Définissez le paramètre nommé value sur une valeur amountInput :
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. Vérifiez l'aperçu. La zone de texte affiche la valeur définie pour la variable d'état, comme illustré dans cette image :

ecbf5f5015668e.png

  1. Exécutez l'application dans l'émulateur et essayez de saisir une autre valeur. L'état codé en dur reste le même, car le composable TextField ne se met pas à jour tout seul. Il se met à jour lorsque son paramètre value change, lequel est défini sur la propriété amountInput.

La variable amountInput représente l'état de la zone de texte. Un état codé en dur n'est pas utile, car il ne peut pas être modifié et ne reflète pas l'entrée utilisateur. Vous devez mettre à jour l'état de l'application lorsque l'utilisateur modifie le montant de la facture.

5. Composition

Les composables d'une application décrivent une interface utilisateur qui affiche une colonne de texte, un espace vide et une zone de texte. Le texte affiche un élément Calculate Tip, et la zone de texte affiche une valeur 0 ou toute valeur par défaut.

Compose est un framework d'interface utilisateur déclaratif, ce qui signifie que vous déclarez la façon dont l'UI doit apparaître dans le code. Si vous souhaitez que la zone de texte affiche initialement la valeur 100, vous devez définir la valeur 100 comme valeur initiale dans le code des composables.

Que se passe-t-il si vous souhaitez que votre interface utilisateur change pendant que l'application est en cours d'exécution ou lorsque l'utilisateur interagit avec elle ? Par exemple, si vous souhaitez remplacer la variable amountInput par la valeur saisie par l'utilisateur et l'afficher dans la zone de texte ? C'est là qu'intervient un processus appelé recomposition, qui permet de mettre à jour la composition de l'application.

Le champ Composition est une description de l'interface utilisateur créée par Compose lorsqu'il exécute des composables. Les applications Compose appellent des fonctions modulables pour convertir les données en UI. En cas de changement d'état, Compose réexécute les fonctions modulables concernées avec le nouvel état, ce qui crée une UI mise à jour. Ce processus est appelé recomposition. Compose planifie une recomposition pour vous.

Lorsque Compose exécute vos composables pour la première fois lors de la composition initiale, il suit les composables que vous appelez pour décrire votre UI dans une composition. La recomposition désigne le moment où Compose réexécute les composables qui ont pu changer en réponse à des modifications de données, puis met à jour la composition pour refléter ces modifications.

La composition ne peut être produite que par une composition initiale et mise à jour par une recomposition. Le seul moyen de modifier la composition est la recomposition. Pour ce faire, Compose doit connaître l'état à suivre. Ainsi, lorsqu'il reçoit une mise à jour, il peut planifier la recomposition. Dans votre cas, il s'agit de la variable amountInput. Chaque fois que sa valeur changera, Compose programmera une recomposition.

Vous pouvez utiliser les types State et MutableState dans Compose pour rendre l'état observable ou suivi par Compose. Le type State est immuable. Vous ne pouvez donc que lire sa valeur, tandis que le type MutableState est modifiable. Vous pouvez utiliser la fonction mutableStateOf() pour créer un élément MutableState observable. Elle reçoit une valeur initiale en tant que paramètre encapsulé dans un objet State, ce qui rend son élément value observable.

La valeur renvoyée par la fonction mutableStateOf() :

  • contient l'état, qui correspond au montant de la facture ;
  • est modifiable, de sorte que sa valeur peut être ajustée ;
  • est observable, de sorte que Compose observe toute modification apportée à la valeur et déclenche une recomposition pour mettre à jour l'UI.

Ajoutez un état de coût de service :

  1. Dans la fonction EditNumberField(), remplacez le mot clé val avant la variable d'état amountInput par var :
var amountInput = "0"

Ce changement rend amountInput modifiable.

  1. Utilisez le type MutableState<String> au lieu de la variable String codée en dur pour que Compose sache qu'il doit suivre l'état amountInput, puis transmettre une chaîne "0", qui correspond à la valeur par défaut initiale de la variable d'état amountInput :
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

var amountInput: MutableState<String> = mutableStateOf("0")

L'initialisation de amountInput peut également être écrite comme suit avec l'inférence de type :

var amountInput = mutableStateOf("0")

La fonction mutableStateOf() reçoit une valeur "0" initiale en tant qu'argument, ce qui rend amountInput observable. Un avertissement de compilation est alors généré dans Android Studio, mais vous résoudrez bientôt ce problème :

Creating a state object during composition without using remember.
  1. Dans la fonction composable TextField, utilisez la propriété amountInput.value :
TextField(
   value = amountInput.value,
   onValueChange = {},
   modifier = modifier
)

Compose effectue le suivi de chaque composable qui lit les propriétés d'état value et déclenche une recomposition lorsque son élément value change.

Le rappel onValueChange est déclenché lorsque l'entrée de la zone de texte change. Dans l'expression lambda, la variable it contient la nouvelle valeur.

  1. Dans l'expression lambda du paramètre nommé onValueChange, définissez la propriété amountInput.value sur la variable it :
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput = mutableStateOf("0")
   TextField(
       value = amountInput.value,
       onValueChange = { amountInput.value = it },
       modifier = modifier
   )
}

Vous mettez à jour l'état de TextField (à savoir, de la variable amountInput) lorsque TextField vous informe d'une modification du texte via la fonction de rappel onValueChange.

  1. Exécutez l'application et saisissez du texte dans la zone de texte. Comme l'illustre cette image, la zone de texte affiche toujours une valeur 0 :

3a2c62f8ec55e339.gif

Lorsque l'utilisateur saisit du texte dans la zone à cet effet, le rappel onValueChange est appelé et la variable amountInput est remplacée par la nouvelle valeur. L'état amountInput est suivi par Compose. Dès lors, lorsque sa valeur changera, la recomposition sera planifiée, et la fonction modulable EditNumberField() sera réexécutée. Dans cette fonction modulable, la variable amountInput est réinitialisée et revient donc à sa valeur 0 initiale. Ainsi, la zone de texte affiche une valeur 0.

Avec le code que vous avez ajouté, les changements d'état entraînent la planification des recompositions.

Cependant, vous devez conserver la valeur de la variable amountInput lors des recompositions pour qu'elle ne soit pas réinitialisée et remplacée par une valeur 0 chaque fois que la fonction EditNumberField() effectue une recomposition. Vous résoudrez ce problème dans la section suivante.

6. Utiliser la fonction "remember" pour enregistrer l'état

Les méthodes de composables peuvent être appelées plusieurs fois en raison de la recomposition. Le composable réinitialise son état lors de la recomposition s'il n'est pas enregistré.

Les fonctions modulables peuvent stocker un objet lors des recompositions à l'aide de la fonction remember. Une valeur calculée par la fonction remember est stockée dans la composition lors de la composition initiale, et la valeur stockée est renvoyée lors de la recomposition. Les fonctions remember et mutableStateOf sont généralement utilisées conjointement dans des fonctions modulables pour que l'état et ses mises à jour soient correctement reflétés dans l'interface utilisateur.

Utilisez la fonction remember dans la fonction EditNumberField() :

  1. Dans la fonction EditNumberField(), initialisez la variable amountInput avec le délégué de propriété Kotlin remember de by en encadrant l'appel de la fonction mutableStateOf() avec remember.
  2. Dans la fonction mutableStateOf(), transmettez une chaîne vide au lieu d'une chaîne "0" statique :
var amountInput by remember { mutableStateOf("") }

La chaîne vide correspond désormais à la valeur par défaut initiale de la variable amountInput. by est une délégation de propriété de Kotlin. Les fonctions "getter" et "setter" par défaut de la propriété amountInput sont déléguées aux fonctions "getter" et "setter" de la classe remember, respectivement.

  1. Importez ces fonctions :
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

L'ajout des importations "getter" et "setter" du délégué vous permet de lire et de définir amountInput sans faire référence à la propriété value de MutableState.

La fonction EditNumberField() mise à jour devrait se présenter comme suit :

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       modifier = modifier
   )
}
  1. Exécutez l'application et saisissez du texte dans la zone à cet effet. Le texte que vous avez saisi devrait maintenant s'afficher.

f60dddc9dcf03edf.png

7. Changement d'état et recomposition à l'œuvre

Dans cette section, vous définirez un point d'arrêt et déboguerez la fonction modulable EditNumberField() pour voir comment fonctionnent la composition et la recomposition initiales.

Définissez un point d'arrêt et déboguez l'application sur un émulateur ou un appareil :

  1. Dans la fonction EditNumberField(), à côté du paramètre nommé onValueChange, définissez un point d'arrêt de ligne.
  2. Dans le menu de navigation, cliquez sur Déboguer l'application. L'application se lance sur l'émulateur ou l'appareil. L'exécution de votre application s'interrompt pour la première fois lorsque l'élément TextField est créé.

e2e1541f22e39281.png

  1. Dans le volet Debug (Débogage), cliquez sur 7bdc150b4ddfdab3.png Resume Program (Reprendre le programme). La zone de texte est créée.
  2. Sur l'émulateur ou l'appareil, saisissez une lettre dans la zone de texte. L'exécution de votre application s'interrompt de nouveau lorsqu'elle atteint le point d'arrêt que vous avez défini.

Lorsque vous saisissez le texte, le rappel onValueChange est appelé. Dans le lambda it, vous avez la nouvelle valeur que vous avez saisie au clavier.

Une fois que la valeur "it" est attribuée à amountInput, Compose déclenche la recomposition avec les nouvelles informations, étant donné que la valeur observable a changé.

987b5951f9f33262.png

  1. Dans le volet Debug (Débogage), cliquez sur 7bdc150b4ddfdab3.png Resume Program (Reprendre le programme). Le texte saisi dans l'émulateur ou sur l'appareil s'affiche à côté de la ligne avec le point d'arrêt, comme illustré sur cette image :

7e7a3c1a4a64e987.png

Il s'agit de l'état du champ de texte.

  1. Cliquez sur 7bdc150b4ddfdab3.png Resume Program (Reprendre le programme). La valeur saisie s'affiche sur l'émulateur ou l'appareil.

8. Modifier l'apparence

Dans la section précédente, vous avez rendu le champ de texte fonctionnel. Dans cette section, vous améliorerez l'interface utilisateur.

Ajouter un libellé à la zone de texte

Chaque zone de texte doit comporter un libellé indiquant aux utilisateurs les informations qu'ils peuvent saisir. Dans la première partie de l'exemple d'image suivant, le libellé se trouve au milieu d'un champ de texte et est aligné avec la ligne d'entrée. Dans la deuxième partie de l'exemple d'image suivant, le libellé se place plus haut dans la zone de texte lorsque l'utilisateur clique dans la zone pour saisir du texte. Pour en savoir plus sur l'anatomie des champs de texte, consultez Anatomie.

9e802ed30b7612b0.png

Modifiez la fonction EditNumberField() pour ajouter un libellé au champ de texte :

  1. Dans la fonction composable TextField() de la fonction EditNumberField(), ajoutez un ensemble de paramètres label à une expression lambda vide :
TextField(
//...
   label = { }
)
  1. Dans l'expression lambda, appelez la fonction Text() qui accepte une ressource stringResource(R.string.bill_amount) :
label = { Text(stringResource(R.string.bill_amount)) },
  1. Dans la fonction composable TextField(), ajoutez un paramètre nommé singleLine défini sur la valeur true :
TextField(
  // ...
   singleLine = true,
)

La zone de texte est alors condensée en une seule ligne à faire défiler horizontalement, à partir de plusieurs lignes.

  1. Ajoutez le paramètre keyboardOptions défini sur KeyboardOptions() :
import androidx.compose.foundation.text.KeyboardOptions

TextField(
  // ...
   keyboardOptions = KeyboardOptions(),
)

Android permet de configurer le clavier à l'écran afin de permettre la saisie de chiffres, d'adresses e-mail, d'URL et de mots de passe, pour ne citer que quelques exemples. Pour en savoir plus sur les autres types de claviers, consultez KeyboardType.

  1. Définissez le clavier numérique comme type de clavier pour saisir des chiffres. Transmettez à la fonction KeyboardOptions un paramètre keyboardType défini sur KeyboardType.Number :
import androidx.compose.ui.text.input.KeyboardType

TextField(
  // ...
   keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)

Une fois terminée, la fonction EditNumberField() devrait ressembler à ceci :

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
    var amountInput by remember { mutableStateOf("") }
    TextField(
        value = amountInput,
        onValueChange = { amountInput = it },
        singleLine = true,
        label = { Text(stringResource(R.string.bill_amount)) },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        modifier = modifier
    )
}
  1. Exécutez l'application.

Vous pouvez voir les modifications apportées au clavier dans cette capture d'écran :

bbd4c90747fb8d28.png

9. Afficher le montant du pourboire

Dans cette section, vous implémenterez la fonctionnalité principale de l'application, qui consiste à calculer et à afficher le montant du pourboire.

Dans le fichier MainActivity.kt, une fonction private (privée) calculateTip() vous est fournie dans le code de démarrage. Vous l'utiliserez pour calculer le montant du pourboire comme suit :

private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
    val tip = tipPercent / 100 * amount
    return NumberFormat.getCurrencyInstance().format(tip)
}

Dans la méthode ci-dessus, vous utilisez NumberFormat pour appliquer un format monétaire au pourboire.

Votre application peut maintenant calculer le pourboire, mais vous devez encore le mettre en forme et l'afficher avec la classe adaptée.

Utiliser la fonction calculateTip()

Le texte saisi par l'utilisateur dans le composable du champ de texte est renvoyé à la fonction de rappel onValueChange en tant que String, même si l'utilisateur a saisi un nombre. Pour résoudre ce problème, vous devez convertir la valeur amountInput, qui correspond au montant saisi par l'utilisateur.

  1. Dans la fonction composable EditNumberField(), créez une variable appelée amount après la définition de amountInput. Appelez la fonction toDoubleOrNull sur la variable amountInput pour la convertir de String en Double :
val amount = amountInput.toDoubleOrNull()

La fonction toDoubleOrNull() est une fonction Kotlin prédéfinie qui analyse une chaîne en tant que nombre Double et renvoie le résultat, ou null si la chaîne ne correspond pas à une représentation valide d'un nombre.

  1. À la fin de l'instruction, ajoutez un opérateur Elvis ?: qui renverra une valeur 0.0 lorsque la valeur de amountInput sera nulle :
val amount = amountInput.toDoubleOrNull() ?: 0.0
  1. Après la variable amount, créez une autre variable val appelée tip. Initialisez-la avec l'élément calculateTip(), en transmettant le paramètre amount.
val tip = calculateTip(amount)

La fonction EditNumberField() devrait se présenter comme suit :

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       label = { Text(stringResource(R.string.bill_amount)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
   )
}

Afficher le montant calculé du pourboire

Vous avez écrit la fonction permettant de calculer le montant du pourboire. L'étape suivante consiste à afficher le montant calculé :

  1. Dans la fonction TipTimeLayout(), à la fin du bloc Column(), un composable de texte affiche $0.00. Vous devez modifier cette valeur pour qu'elle corresponde au montant calculé du pourboire.
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // ...
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        // ...
    }
}

Vous devez accéder à la variable amountInput dans la fonction TipTimeLayout() pour calculer et afficher le montant du pourboire, mais la variable amountInput correspond à l'état du champ de texte défini dans la fonction composable EditNumberField(). Vous ne pouvez pas encore l'appeler depuis la fonction TipTimeLayout(). Cette image illustre la structure du code :

4d8b69d49a90683a.png

Cette structure ne vous permet pas d'afficher le montant du pourboire dans le nouveau composable Text, car le composable Text doit accéder à la variable amount calculée à partir de la variable amountInput. Vous devez exposer la variable amount à la fonction TipTimeLayout(). Cette image illustre la structure de code souhaitée, qui rend le composable EditNumberField() sans état :

38bd92a2346a910b.png

Ce système est appelé hissage d'état. Dans la section suivante, vous allez hisser l'état d'un composable afin de le rendre sans état.

10. Hissage d'état

Dans cette section, vous allez apprendre à choisir où définir l'état de sorte à pouvoir réutiliser et partager vos composables.

Dans une fonction modulable, vous pouvez définir des variables qui contiennent un état à afficher dans l'interface utilisateur. Par exemple, vous avez défini la variable amountInput comme état dans le composable EditNumberField().

Lorsque l'application devient plus complexe et que d'autres composables ont besoin d'accéder à l'état au sein de ce composable EditNumberField(), vous devez envisager de hisser ou extraire l'état de la fonction modulable EditNumberField().

Comprendre les composables avec ou sans état

Vous devez hisser l'état dans les cas suivants :

  • Vous devez partager l'état avec plusieurs fonctions modulables.
  • Vous devez créer un composable sans état qui pourra être réutilisé dans votre application.

Lorsque vous extrayez l'état d'une fonction composable, la fonction composable obtenue est appelée sans état. Autrement dit, les fonctions composables peuvent être rendues sans état si l'on extrait leur état.

Un composable sans état est un composable qui n'a pas d'état. En d'autres termes, il ne peut détenir, définir, ni modifier aucun nouvel état. En revanche, un composable avec état possède un état qui peut changer au fil du temps.

Le hissage d'état est un modèle qui consiste à faire remonter un état vers son appelant pour obtenir un composant sans état.

Lorsqu'il est appliqué à des composables, cela implique souvent d'introduire deux paramètres dans le composable :

  • Un paramètre value: T, qui correspond à la valeur actuelle à afficher.
  • Un paramètre onValueChange: (T) -> Unit lambda de rappel, qui est déclenché lorsque la valeur change afin que l'état puisse être mis à jour ailleurs, par exemple lorsqu'un utilisateur saisit du texte dans la zone de texte.

Hissez l'état dans la fonction EditNumberField() :

  1. Mettez à jour la définition de la fonction EditNumberField() pour hisser l'état en ajoutant les paramètres value et onValueChange :
@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
//...

Le paramètre value est de type String, et le paramètre onValueChange est de type (String) -> Unit. Il s'agit donc d'une fonction qui accepte une valeur String et n'a aucune valeur renvoyée. Le paramètre onValueChange est utilisé comme rappel onValueChange transmis au composable TextField.

  1. Dans la fonction EditNumberField(), mettez à jour la fonction composable TextField() pour qu'elle utilise les paramètres transmis :
TextField(
   value = value,
   onValueChange = onValueChange,
   // Rest of the code
)
  1. Hissez l'état, déplacez l'état mémorisé de la fonction EditNumberField() vers la fonction TipTimeLayout() :
@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   Column(
       //...
   ) {
       //...
   }
}
  1. Vous avez hissé l'état vers TipTimeLayout(). Transmettez-le maintenant à EditNumberField(). Dans la fonction TipTimeLayout(), mettez à jour l'appel de fonction EditNumberField() pour utiliser l'état hissé :
EditNumberField(
   value = amountInput,
   onValueChange = { amountInput = it },
   modifier = Modifier
       .padding(bottom = 32.dp)
       .fillMaxWidth()
)

De ce fait, EditNumberField devient sans état. Vous avez hissé l'état de l'interface utilisateur vers son ancêtre, TipTimeLayout(). TipTimeLayout() est désormais le propriétaire de l'état amountInput.

Formatage basé sur le positionnement

Le formatage basé sur le positionnement permet d'afficher le contenu dynamique sous forme de chaînes. Par exemple, imaginez que vous souhaitiez que la zone de texte Tip amount (Montant du pourboire) affiche une valeur xx.xx qui peut correspondre à n'importe quel montant calculé et mis en forme dans votre fonction. Pour y parvenir, dans le fichier strings.xml, vous devez définir la ressource de chaîne avec un argument d'espace réservé, comme dans cet extrait de code :

// No need to copy.

// In the res/values/strings.xml file
<string name="tip_amount">Tip Amount: %s</string>

Dans le code Compose, vous pouvez avoir plusieurs arguments réservés, quel que soit leur type. Un espace réservé de chaîne (string) est %s.

Vous pouvez voir que dans le cas du composable de texte dans TipTimeLayout(), vous transmettez le pourboire mis en forme à la fonction stringResource() en tant qu'argument.

// No need to copy
Text(
   text = stringResource(R.string.tip_amount, "$0.00"),
   style = MaterialTheme.typography.displaySmall
)
  1. Dans la fonction TipTimeLayout(), utilisez la propriété tip pour afficher le montant du pourboire. Mettez à jour le paramètre text du composable Text pour utiliser la variable tip comme paramètre.
Text(
     text = stringResource(R.string.tip_amount, tip),
     // ...

Une fois terminées, les fonctions TipTimeLayout() et EditNumberField() devraient ressembler à ceci :

@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }
   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   Column(
       modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
       horizontalAlignment = Alignment.CenterHorizontally,
       verticalArrangement = Arrangement.Center
   ) {
       Text(
           text = stringResource(R.string.calculate_tip),
           modifier = Modifier
               .padding(bottom = 16.dp, top = 40.dp)
               .align(alignment = Alignment.Start)
       )
       EditNumberField(
           value = amountInput,
           onValueChange = { amountInput = it },
           modifier = Modifier
               .padding(bottom = 32.dp)
               .fillMaxWidth()
       )
       Text(
           text = stringResource(R.string.tip_amount, tip),
           style = MaterialTheme.typography.displaySmall
       )
       Spacer(modifier = Modifier.height(150.dp))
   }
}

@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   TextField(
       value = value,
       onValueChange = onValueChange,
       singleLine = true,
       label = { Text(stringResource(R.string.bill_amount)) },
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
       modifier = modifier
   )
}

Pour résumer, vous avez hissé l'état amountInput de EditNumberField() vers le composable TipTimeLayout(). Pour que la zone de texte fonctionne comme précédemment, vous devez transmettre deux arguments à la fonction modulable EditNumberField() : la valeur amountInput et le rappel lambda qui met à jour la valeur amountInput à partir de l'entrée utilisateur. Ces modifications vous permettent de calculer le pourboire à partir de la propriété amountInput dans TipTimeLayout() et de l'afficher pour les utilisateurs.

  1. Exécutez l'application sur l'émulateur ou l'appareil, puis saisissez une valeur dans la zone de texte Bill amount (Montant de la facture). Comme illustré sur cette image, le montant du pourboire, qui correspond à 15 % du montant de la facture, s'affiche :

b6bd5374911410ac.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-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout state

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 souhaitez voir le code de solution, affichez-le sur GitHub.

12. Conclusion

Félicitations ! Vous avez terminé cet atelier de programmation et savez maintenant comment utiliser l'état dans une application Compose.

Résumé

  • Dans une application, l'état correspond à toute valeur susceptible de changer au fil du temps.
  • Le champ Composition est une description de l'interface utilisateur créée par Compose lorsqu'il exécute des composables. Les applications Compose appellent des fonctions modulables pour convertir les données en UI.
  • La composition initiale est une création de l'UI par Compose lorsque des fonctions modulables sont exécutées pour la première fois.
  • La recomposition désigne le processus qui consiste à exécuter à nouveau les mêmes composables pour actualiser l'arborescence lorsque leurs données changent.
  • Le hissage d'état est un modèle qui consiste à faire remonter un état vers son appelant pour obtenir un composant sans état.

En savoir plus