Utilisation des API liées aux Effets de Bord et à l'État dans Android Compose

Cet article explore en profondeur les API liées aux effets de bord et à la gestion d'état dans Android Compose. La maîtrise de ces concepts est fondamentale pour créer des applications Compose robustes et comportant correctement. Examinons attentivement leur mise en œuvre.

Effets de Bord

Dans l'écosystème Compose, les effets de bord désignent les opérations qui se produisent en dehors des fonctions Composables. Étant donné que ces fonctions doivent être idempotentes (exécutées plusieurs fois avec le même résultat) et sans effets secondaires, nous avons besoin d'API spécifiques pour exécuter ces opérations en toute sécurité.

1. LaunchedEffect

LaunchedEffect permet de lancer une coroutine dans le cycle de vie d'un Composable. Lorsque le Composable entre dans la composition pour la première fois, la coroutine démarre, et lorsque le Composable quitte la composition, la coroutine est annulée. Si la clé de LaunchedEffect change, la coroutine existante est annulée et une nouvelle est démarrée.

import androidx.compose.runtime.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

@Composable
fun GestionnaireNotification(donnees: String) {
    val etatSnackBar = remember { SnackbarHostState() }

    LaunchedEffect(donnees) { // La clé est les données
        etatSnackBar.showSnackbar("Informations modifiées: $donnees")
        delay(3000) // Simule une opération longue
        println("SnackBar affiché pour $donnees")
    }

    // ... Éléments UI, par exemple SnackbarHost
}

Cas d'utilisation :

  • Exécution d'opérations asynchroniques uniques, comme les requêtes réseau, les opérations de base de données, etc.
  • Déclenchement d'opérations spécifiques lors de certains changements d'état.
  • Opérations liées au cycle de vie, comme démarrer une animation lorsque le Composable est affiché et l'arrêter lorsqu'il est masqué.

Points clés :

  • Nécessite un ou plusieurs paramètres key. Lorsque ces valeurs de clé changent, la coroutine est redémarrée. Si vous ne souhaitez pas qu'elle soit redémarrée, utilisez Unit comme clé.
  • Les fonctions suspend peuvent être utilisées à l'intérieur de la coroutine.
  • Lorsque le Composable quitte la composition, la coroutine est automatiquement annulée, évitant ainsi les fuites de mémoire.

2. rememberCoroutineScope

rememberCoroutineScope permet d'obtenir un CoroutineScope lié au cycle de vie du Composable. Vous pouvez démarrer plusieurs coroutines dans ce scope, et lorsque le Composable quitte la composition, toutes ces coroutines sont annulées.

import androidx.compose.runtime.*
import kotlinx.coroutines.launch

@Composable
fun EcranPrincipal() {
    val scopeCoroutines = rememberCoroutineScope()
    val etatScaffold = rememberScaffoldState()

    Scaffold(scaffoldState = etatScaffold) {
        Button(onClick = {
            scopeCoroutines.launch {
                etatScaffold.snackbarHostState.showSnackbar("Bouton activé!")
            }
        }) {
            Text("Appuyez-moi")
        }
    }
}

Cas d'utilisation :

  • Démarrer plusieurs coroutines connexes en réponse à des événements utilisateur ou autres événements à l'intérieur d'un Composable.
  • Gérer le cycle de vie d'un groupe d'opérations asynchrones connexes de manière unifiée.

Points clés :

  • Le CoroutineScope retourné annule automatiquement toutes ses sous-coroutines lorsque le Composable quitte la composition.
  • Idéal pour des scénarios nécessitant une gestion plus fine des coroutines dans le scope d'un Composable.

3. rememberUpdatedState

rememberUpdatedState permet de référencer l'état le plus récent d'un Composable dans un effet de bord. Lorsqu'un Composable est recomposé, l'état capturé dans l'effet de bord peut être obsolète. rememberUpdatedState retourne un objet State qui se met à jour lorsque le Composable est recomposé.

import androidx.compose.runtime.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

@Composable
fun OperationDelai(expiration: () -> Unit) {
    rappelExpiration = rememberUpdatedState(newValue = expiration)

    LaunchedEffect(Unit) {
        delay(5000)
        rappelExpiration.value() // Appelle la version la plus récente de expiration
    }

    Text("L'opération expirera dans 5 secondes")
}

// Composable parent
@Composable
fun ComposableParent() {
    var compteur by remember { mutableStateOf(0) }

    OperationDelai(expiration = {
        println("Expiration! Compteur: $compteur")
        compteur++
    })

    Button(onClick = { compteur++ }) {
        Text("Incrémenter le compteur")
    }
}

Cas d'utilisation :

  • Lorsqu'un effet de bord doit référencer un état susceptible de changer pendant le cycle de vie d'un Composable, comme une fonction de rappel.
  • S'assurer que la fermeture capturée dans l'effet de bord utilise la valeur d'état la plus récente.

Points clés :

  • Retourne un objet State, accessible via .value pour obtenir la valeur la plus récente.
  • Évite l'utilisation de variables de fermeture obsolètes dans les effets de bord.

4. DisposableEffect

DisposableEffect permet d'exécuter des opérations spécifiques lorsqu'un Composable entre et quitte la composition. Il fournit un bloc onDispose dans lequel des opérations de nettoyage peuvent être exécutées, comme l'annulation d'abonnements, la libération de ressources, etc.

import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver

@Composable
fun ObservateurCycleVie(demarrage: () -> Unit, arret: () -> Unit) {
    val proprietaireCycleVie = LocalLifecycleOwner.current

    DisposableEffect(proprietaireCycleVie) {
        val observateur = LifecycleEventObserver { _, evenement ->
            when (evenement) {
                Lifecycle.Event.ON_START -> demarrage()
                Lifecycle.Event.ON_STOP -> arret()
                else -> Unit
            }
        }
        proprietaireCycleVie.lifecycle.addObserver(observateur)

        // Bloc onDispose, exécuté lorsque le Composable quitte la composition
        onDispose {
            proprietaireCycleVie.lifecycle.removeObserver(observateur)
        }
    }

    Text("Surveillance des événements du cycle de vie")
}

// Exemple d'utilisation
@Composable
fun EcranAvecCycleVie() {
    ObservateurCycleVie(
        demarrage = { println("Composable démarré") },
        arret = { println("Composable arrêté") }
    )
}

Cas d'utilisation :

  • S'abonner et se désabonner de ressources externes, comme les événements du cycle de vie, les capteurs, les récepteurs de broadcast, etc.
  • Exécuter des opérations de nettoyage lorsque le Composable n'est plus affiché.

Points clés :

  • Nécessite un ou plusieurs paramètres key. Lorsque ces valeurs de clé changent, le bloc onDispose est exécuté en premier pour le nettoyage, puis le nouvel effet est exécuté.
  • Le bloc onDispose est crucial pour garantir le bon libération des ressources.

5. produceState

produceState permet de générer de manière asynchrone un State en dehors du système d'état de Compose. Il reçoit une valeur initiale et un bloc de coroutine qui peut mettre à jour le MutableState retourné. Lorsque le Composable quitte la composition, la coroutine de production est annulée.

import androidx.compose.runtime.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

@Composable
fun GenerateurNombreAleatoire(): State<Int> {
    return produceState(initialValue = 0) {
        while (true) {
            delay(1000)
            val nombreAleatoire = (0..100).random()
            value = nombreAleatoire // Met à jour la valeur de State
        }
    }
}

@Composable
fun AfficherNombreAleatoire() {
    val etatNombreAleatoire = GenerateurNombreAleatoire()
    Text("Nombre aléatoire: ${etatNombreAleatoire.value}")
}

// Utilisation de Flow pour générer State
@Composable
fun ProducteurFluxNombre(fluxNombre: Flow<Int>): State<Int> {
    return produceState(initialValue = 0, fluxNombre) {
        fluxNombre.collect { value = it }
    }
}

Cas d'utilisation :

  • Convertir des sources de données externes basées sur des rappels ou d'autres modèles asynchrones en State de Compose.
  • Générer un State à partir d'un Flow ou d'autres flux de données asynchrones.

Points clés :

  • Retourne un objet State directement utilisable dans un Composable.
  • La durée de vie de la coroutine de production est liée à celle du Composable.

6. remember (Association avec les effets de bord)

Bien que remember ne soit pas une API spécifiquement conçue pour les effets de bord, il est souvent utilisé en conjonction avec eux. remember permet de conserver l'état entre les recompositions d'un Composable. Ceci est particulièrement utile pour créer des objets liés aux effets de bord qui ne doivent être initialisés qu'une seule fois.

import androidx.compose.runtime.*
import androidx.compose.material.SnackbarHostState

@Composable
fun MonComposableAvecSnackBar() {
    val etatSnackBar = remember { SnackbarHostState() } // Créé une seule fois SnackbarHostState

    // ... Utilisation de etatSnackBar pour les effets de bord (par exemple LaunchedEffect)
}

Cas d'utilisation :

  • Créer des gestionnaires d'effets de bord ou des détenteurs d'état qui ne doivent être initialisés qu'une seule fois.

Points clés :

  • La valeur retournée par remember reste inchangée pendant tout le cycle de vie du Composable, sauf si la clé change (si une clé est fournie).

Gestion d'État

Dans Compose, l'état (State) fait référence aux données qui peuvent changer avec le temps. L'idée centrale de Compose est de mettre à jour l'interface utilisateur en fonction des changements d'état.

1. remember et mutableStateOf

remember est une fonction Composable qui permet de conserver l'état entre les recompositions. mutableStateOf est une fonction d'usine qui crée un objet MutableState observable. Lorsque la propriété value de MutableState change, Compose planifie la recomposition de tout Composable qui lit cet État.

import androidx.compose.runtime.*
import androidx.compose.material.Button
import androidx.compose.material.Text

@Composable
fun ApplicationCompteur() {
    var compteur by remember { mutableStateOf(0) } // Utilisation du délégation de propriété pour simplifier la lecture/écriture de State

    Button(onClick = { compteur++ }) {
        Text("Incrémenter: $compteur")
    }
}

Cas d'utilisation :

  • Stocker des états internes simples à modifier et observer à l'intérieur d'un Composable.

Points clés :

  • remember assure que l'état n'est pas perdu entre les recompositions.
  • mutableStateOf crée un conteneur d'état observable.
  • La délégation de propriété by peut être utilisée pour une lecture/écriture plus concise de la propriété value de MutableState.

2. rememberSaveable

rememberSaveable fonctionne comme remember, mais il peut également restaurer l'état après la recréation d'une Activity ou d'un processus. Il convient aux données qui doivent être conservées à travers les changements de configuration (comme la rotation de l'écran) ou l'arrêt du processus.

import androidx.compose.runtime.*
import androidx.compose.material.Text
import androidx.compose.material.TextField

@Composable
fun SaisieNom() {
    var nom by rememberSaveable { mutableStateOf("") }

    TextField(
        value = nom,
        onValueChange = { nom = it },
        label = { Text("Entrez votre nom") }
    )
    Text("Bonjour, $nom!")
}

Cas d'utilisation :

  • Stocker des saisies utilisateur, des états d'interface utilisateur, etc., qui doivent être conservés après les changements de configuration.

Points clés :

  • Utilise internement un Bundle pour sauvegarder et restaurer l'état.
  • Seuls les types de données qui peuvent être stockés dans un Bundle peuvent être correctement sauvegardés par rememberSaveable. Pour des types de données plus complexes, un objet Saver personnalisé doit être fourni.

3. Interfaces State et MutableState

State est une interface en lecture seule qui représente une valeur d'état observable. Elle n'a qu'une propriété value. MutableState hérite de State et ajoute une propriété value en écriture. Lorsque cette propriété est modifiée, elle notifie les observateurs (le runtime Compose).

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.material.Button

@Composable
fun MonComposableAvecEtat() {
    val monEtat: MutableState<String> = remember { mutableStateOf("Valeur Initiale") }
    val etatLectureSeule: State<String> = monEtat

    Button(onClick = { monEtat.value = "Valeur Mise à Jour" }) {
        Text("Mettre à jour l'état")
    }
    Text("État actuel: ${etatLectureSeule.value}")
}

Cas d'utilisation :

  • Comme types pour exposer l'état dans des détenteurs d'état ou des ViewModels.
  • Gérer l'état à l'intérieur d'un Composable.

Points clés :

  • State fournit un accès en lecture seule, garantissant que l'état ne peut être modifié que par des moyens spécifiques.
  • MutableState fournit un accès en lecture/écriture et déclenche la recomposition de l'interface utilisateur.

4. derivedStateOf

derivedStateOf permet de calcluer un nouvel État à partir d'un ou plusieurs États existants. Lorsque les États d'origine changent, l'État créé par derivedStateOf est recalculé automatiquement. Cela aide à éviter des calculs inutiles lors des recompositions.

import androidx.compose.runtime.*
import androidx.compose.material.Text
import androidx.compose.material.Button

@Composable
fun ListeUtilisateurs(utilisateurs: List<String>) {
    val nombreUtilisateurs by remember { derivedStateOf { utilisateurs.size } }
    Text("Nombre d'utilisateurs: $nombreUtilisateurs")

    // ... Affiche la liste des utilisateurs
}

// Composable parent
@Composable
fun ListeUtilisateursParent() {
    var listeUtilisateurs by remember { mutableStateOf(listOf("Alice", "Bob")) }
    Button(onClick = { listeUtilisateurs = listeUtilisateurs + "Charlie" }) {
        Text("Ajouter un utilisateur")
    }
    ListeUtilisateurs(utilisateurs = listeUtilisateurs)
}

Cas d'utilisation :

  • Dériver un nouvel état d'interface utilisateur à partir d'autres états.
  • Optimiser les performances en évitant des recompositions inutiles lorsque l'état d'origine n'affecte pas l'état dérivé.

Points clés :

  • Seule lorsque le résultat du calcul de l'état dérivé change, les Composables qui en dépendent sont recomposés.

5. snapshotFlow

snapshotFlow convertit un État de Compose en un Flow de Kotlin. Cela vous permet d'utiliser les puissants opérateurs Flow pour traiter les changements d'état de Compose.

import androidx.compose.runtime.*
import kotlinx.coroutines.flow.*

@Composable
fun BarreRecherche() {
    var requete by remember { mutableStateOf("") }
    val resultatsRecherche = remember {
        snapshotFlow { requete }
            .debounce(300)
            .mapLatest { effectuerRecherche(it) } // Supposons que effectuerRecherche est une fonction suspendue
            .collectAsState(initial = emptyList())
    }

    TextField(
        value = requete,
        onValueChange = { requete = it },
        label = { Text("Rechercher") }
    )

    // Affiche les resultatsRecherche
}

suspend fun effectuerRecherche(requete: String): List<String> {
    delay(500) // Simule un délai de recherche
    return listOf("Résultat 1 pour $requete", "Résultat 2 pour $requete")
}

Cas d'utilisation :

  • Intégrer l'état de Compose dans des flux de données basés sur Flow pour un traitement asynchrone plus complexe.
  • Utiliser les opérateurs Flow (comme debounce, map, filter) pour traiter les changements d'état.

Points clés :

  • Retourne un Flow qui capture un instantané de l'état de Compose à chaque exécution.
  • collectAsState() doit être utilisé pour convertir le Flow en un State de Compose, afin de pouvoir l'utiliser dans l'interface utilisateur.

Conclusion

La compréhension des effets de bord et de la gestion d'état est essentielle pour construire des applications Android Compose complexes et réactives.

  • Effets de bord : Ils permettent d'exécuter en toute sécurité des opérations en dehors du scope d'un Composable tout au long de son cycle de vie. Compose propose diverses API pour gérer différents types d'effets secondaires, comme les opérations uniques (LaunchedEffect), les coroutines liées au cycle de vie (rememberCoroutineScope), la référence à l'état le plus récent (rememberUpdatedState), le nettoyage des ressources (DisposableEffect) et la production d'état asynchrone (produceState).
  • État : C'est les données qui pilotent les mises à jour de l'interface utilisateur. Compose offre plusieurs API pour gérer des états de différents cycles de vie et complexités, incluant l'état interne simple (remember et mutableStateOf), l'état persistant (rememberSaveable), les interfaces d'état en lecture seule et en écriture (State et MutableState), l'état dérivé (derivedStateOf) et le mécanisme de conversion d'État en Flow (snapshotFlow).

Une utilisation judicieuse de ces API vous permettra de créer des applications Compose à la fois performantes et faciles à maintanir. Rappelez-vous que les fonctions Composables elles-mêmes devraient être sans effets secondaires, toutes les interactions avec le monde extérieur doivent être traitées via des effets de bord, tandis que les mises à jour de l'interface utilisateur doivent être pilotées par les changements d'état.

Étiquettes: Android Compose Effets de Bord Gestion d'état Jetpack Compose coroutines

Publié le 13 juin à 17h34