Variables d'état avancées et State Hoisting¶
28.1 Hisser l'état (state hoisting)¶
Lorsqu'une fonction modulable déclare une variable d'état et qu'elle appelle une autre fonction (modulable ou non) qui doit modifier cette variable d'état, il faut utiliser un mécanisme qui s'appelle hissage d'état (en anglais : state hoisting) pour y parvenir.
Il s'agit de passer en paramètre une expression lambda qui permet de modifier la variable d'état.
Cette technique est nécessaire puisqu'en Kotlin, les paramètres sont immuables. Il n'est donc pas possible de passer une variable d'état en paramètre à une fonction qui doit modifier cet état.
Pour éviter d'avoir à hisser l'état trop de fois, le propriétaire d'état (la fonction modulable dans laquelle la variable d'état est déclarée) devra être le plus petit ancêtre commun des fonctions modulables qui doivent lire ou écrire dans cette variable.
Le nom du paramètre de l'expression lamba sera habituellement du genre onXXXChange où XXX représente le nom de la variable d'état.
Dans le traitement de l'expression lambda, on assignera le mot-clé it à la variable d'état.
@Composable
fun MainScreen() {
var maVariableDEtat : String by remember { mutableStateOf("") }
...
Button(
onClick = {
faireQuelqueChose( onMaVariableDEtatChange = { maVariableDEtat = it })
}
) {
Text(text = "Appliquer")
}
}
Dans la déclaration de la fonction, il faut préciser les types de l'expression lambda :
Après le nom du paramètre suivi de deux points (:), on précisera entre parenthèses le type du paramètre que¶
l'expression lambda reçoit. Il s'agit du type de la variable d'état.
À la suite de la flèche, on précisera le type de la valeur de retour de l'expression lambda, soit Unit.¶
À l'intérieur de la fonction, on appellera l'expression lambda au moment approprié, en lui passant en paramètre la valeur qui doit être assignée à la variable d'état.
fun faireQuelqueChose( onMaVariableDEtatChange : ( String ) -> Unit) {
...
val nouvelleValeur = ...
onMaVariableDEtatChange ( nouvelleValeur ) // assigne la nouvelle valeur à la variable d'état
}
Case de saisie simple¶
Un scénario souvent rencontré pour le hissage d'état consiste à assigner à une variable d'état la valeur d'une case de saisie.
Dans le cas le plus simple, on n'a pas à déclarer la fonction qui reçoit l'expression lambda. C'est la fonction modulable TextField qui le fait pour nous. Il suffit d'appeler la fonction en lui passant l'expression lambda appropriée.
TextField(
value = titre,
onValueChange = { titre = it } ,
label = { Text("Titre") }
)
Fonction qui affiche une case de saisie¶
Dans ce second exemple, on travaille avec une fonction modulable qui affiche un texte suivi d'une case de saisie.
Il est ici nécessaire de passer en paramètre la variable d'état (pour afficher sa valeur dans la case) en plus de l'expression lambda (pour modifier la valeur selon ce qui est saisi).
@Composable
fun MainScreen() {
var nom by remember { mutableStateOf("") }
SaisieNom(nom = nom, onNomChange = { nom = it } )
}
@Composable
fun SaisieNom(nom: String, onNomChange: (String) -> Unit ) {
Column {
Text(
text = "Valeur de la variable d'état: $nom",
)
OutlinedTextField(
value = nom,
onValueChange = onNomChange ,
label = { Text("Saisir une valeur :") }
)
}
}
Hisser l'état deux fois¶
Parfois, l'état doit être hissé vers une fonction qui, elle aussi, doit hisser l'état vers une autre fonction.
À ce moment, l'expression lambda qui a été fournie lors du premier hissage n'aura pas à être redéfinie lors du second hissage.
@Composable
fun MainScreen() {
// La varialbe d'état doit être déclarée ici car elle est utilisée dans la barre de titre.
var points: Int by remember { mutableStateOf(0) }
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
CenterAlignedTopAppBar(
title = {
Text(text = "Mon application à $points points")
}
)
},
content = { innerPadding ->
// Il faut hisser l'état pour que les points puissent être modifiés dans le contenu.
MainContent(
modifier = Modifier.padding(innerPadding),
points,
onPointsChange = { points = it }
)
}
)
}
@Composable
fun MainContent(
modifier: Modifier = Modifier,
points: Int,
onPointsChange: (Int) -> Unit
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
...
Button(
onClick = {
// On hisse l'état une seconde fois.
// Cette fois, pas besoin de fournir l'expression lambda puisqu'elle a déjà été passée
// en paramètre à cette fonction.
Jouer(points, onPointsChange )
}
) {
Text(text = "Jouer")
}
}
}
fun Jouer(points: Int, onPointsChange: (Int) -> Unit ) {
...
onPointsChange(points + 1)
}
Pour plus d'information¶
-
« État et Jetpack Compose - Hisser un état » - Android Developers
-
« Unlock the Power of State Hoisting in Jetpack Compose » - Medium 574f742c4721
* « Où hisser l'état? » - Android Developers¶
- Exercice 3
79.1 derivedStateOf()¶
Afin d'améliorer les performances de votre application Android, il est possible d'utiliser derivedStateOf() afiin d'éviter les recompositions superflues.
Prenons l'exemple d'une application qui se termine après 10 itérations. Il n'est pas souhaitable qu'à chaque fois que l'itération est incrémentée, l'application soit recomposée.
Pour que la recomposition n'ait lieu qu'après les 10 itérations, la variable pourra être déclarée avec derivedStateOf() comme suit :
val afficherFinPartie by remember {
derivedStateOf { iterations > 10 }
}
if (afficherFinPartie) {
...
}
Pour plus d'information¶
-
« Jetpack Compose: remember, mutableStateOf, derivedStateOf and rememberSaveable explained » - Medium remember-mutablestateof-derivedstateof-and-remembersaveable-explained-270dbaa61b8
79.2 mutableListOf comme variable d'état¶
Selon la documentation officielle de Android :
Attention : Si vous utilisez des objets modifiables tels que ArrayList ou mutableListOf() en tant qu'état dans Compose, les utilisateurs verront des données incorrectes ou obsolètes dans votre¶
application.
Le problème, c'est qu'un objet créé avec mutableListOf() n'est pas observable alors il ne forcera pas le rafraîchissement de l'écran quand il est modifié.
Par exemple, avec ce code, rien n'apparaîtra à l'écran quand le bouton est cliqué.
@Composable
fun MainScreen() {
val heures : MutableState<MutableList<String>> = remember {
mutableStateOf( mutableListOf() )
}
Column {
Button(
onClick = {
heures .value.add(LocalDateTime.now().toString()) // la modification de la liste ne rafraîchit pas l'écran
}
) {
Text(text = "Ajouter")
}
AfficherItems(heures.value)
}
}
@Composable
private fun AfficherItems(items: List<String>) {
Column {
for (item in items) {
Text(text = item)
}
}
}
Il faut donc utiliser une astuce pour forcer le rafraîchissement de l'écran si une variable d'état a été créée avec mutableListOf().
Cette astuce consiste à copier les valeurs de la liste dans une liste temporaire, à effectuer les modifications désirées dans cette liste temporaire puis à réassigner la liste originale.
Cette réassignation crée une nouvelle copie de la liste et cette fois, Compose détectera le changement et rafraîchira l'écran.
Button(
onClick = {
val tempo = heures.value.toMutableList()
tempo.add(LocalDateTime.now().toString())
heures.value = tempo // la réaffectation de la liste cause le rafraîchissement
}
) {
Text(text = "Ajouter")
}
Autre syntaxe équivalente que vous rencontrerez souvent en Kotlin :
val tempo = heures.value.toMutableList().apply { add(LocalDateTime.now().toString()) }
heures.value = tempo // la réaffectation de la liste cause le rafraîchissement
ou mieux :
Version avec la syntaxe « by remember »¶
Dans l'exemple précédent, la variable d'état n'a pas été créée à l'aide du mot-clé by, ce qui exige l'utilisation de .value pour accéder à sa valeur.
Le code aurait également pu être écrit comme suit.
Remarquez :
l'ajout du by¶
l'ajout des deux import¶
le retrait du MutableState<> lors de la déclaration de la variable d'état¶
le retrait du .value lors de son utilisation¶
cette fois, il faut déclarer la variable avec var puisqu'elle sera réassignée (la syntaxe sans le by réassignait¶
heures.value).
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
...
@Composable
fun MainScreen() {
var heures: MutableList<String> by remember {
mutableStateOf(mutableListOf())
}
Column {
Button(
onClick = {
val tempo = heures.toMutableList().apply { add(LocalDateTime.now().toString()) }
heures = tempo // la réaffectation de la liste cause le rafraîchissement
}
) {
Text(text = "Ajouter")
}
AfficherItems(heures)
}
}
@Composable
private fun AfficherItems(items: List<String>) {
Column {
for (item in items) {
Text(text = item)
}
}
}
Version avec la syntaxe « var (heures, setHeures) »¶
Pour compléter l'étude de ce concept, je vous présente une autre version qui utilise une syntaxe différente.
En effet, selon la documentation officielle de Android :
Il existe trois façons de déclarer un objet MutableState dans un composable :¶
val mutableState = remember¶
var value by remember¶
val (value, setValue) = remember { mutableStateOf(default) }
Cette fois, le code déclare directement un modificateur (setter) et s'en sert pour effectuer une réaffectation de la liste, ce qui force le rafraîchissement de l'écran.
@Composable
fun MainScreen() {
val (heures, setHeures) = remember {
mutableStateOf(mutableListOf<String>())
}
Column {
Button(
onClick = {
setHeures(heures.toMutableList().apply { add(LocalDateTime.now().toString()) })
}
) {
Text(text = "Ajouter")
}
AfficherItems(heures)
}
}
@Composable
private fun AfficherItems(items: List<String>) {
Column {
for (item in items) {
Text(text = item)
}
}
}
Source :
1. * « État et Jetpack Compose » - Android Developers¶
2. * « État et Jetpack Compose » - Android Developers¶
Pour plus d'information¶
* « There are three ways to declare a MutableState object in a composable » - Reddit¶
- Optimiser l'application