Un effet secondaire est un changement d'état de l'application qui se produit en dehors du champ d'application d'une fonction modulable. En raison du cycle de vie et des propriétés des composables (les recompositions imprévisibles, l'exécution de recompositions de composables dans différents ordres ou les recompositions pouvant être supprimées), les composables devraient idéalement ne pas avoir d'effets secondaires.
Cependant, des effets secondaires sont parfois nécessaires, par exemple pour déclencher un événement ponctuel, comme afficher un snackbar ou accéder à un autre écran en fonction d'une certaine condition d'état. Ces actions doivent être appelées à partir d'un environnement contrôlé qui prend en compte le cycle de vie du composable. Sur cette page, vous découvrirez les différentes API d'effets secondaires proposées par Jetpack Compose.
Cas d'utilisation d'état et d'effet
Comme indiqué dans la documentation Raisonnement dans Compose, les composables ne devraient pas avoir d'effets secondaires. Lorsque vous devez modifier l'état de l'application (comme décrit dans la documentation Gestion de l'état), vous devez utiliser les API Effect pour exécuter ces effets secondaires de manière prévisible.
Les différentes possibilités offertes par les effets dans Compose peuvent facilement entraîner une utilisation excessive. Assurez-vous que votre travail est lié à l'interface utilisateur et qu'il ne brise pas le flux de données unidirectionnel, comme expliqué dans la documentation Gestion de l'état.
LaunchedEffect
: exécute des fonctions de suspension dans le champ d'application d'un composable.
Pour effectuer des tâches tout au long de la durée de vie d'un composable et pouvoir appeler des fonctions de suspension, utilisez le composable LaunchedEffect
. Lorsque LaunchedEffect
entre dans la composition, il lance une coroutine avec le bloc de code transmis en tant que paramètre. La coroutine est annulée si LaunchedEffect
quitte la composition. Si LaunchedEffect
est recomposé avec des clés différentes (voir la section Redémarrage des effets ci-dessous), la coroutine existante est annulée et la nouvelle fonction de suspension est lancée dans une nouvelle coroutine.
Par exemple, voici une animation qui fait clignoter la valeur alpha avec un délai configurable:
// Allow the pulse rate to be configured, so it can be sped up if the user is running // out of time var pulseRateMs by remember { mutableStateOf(3000L) } val alpha = remember { Animatable(1f) } LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes while (isActive) { delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user alpha.animateTo(0f) alpha.animateTo(1f) } }
Dans le code ci-dessus, l'animation utilise la fonction de suspension delay
pour attendre la durée définie. Ensuite, il anime séquentiellement l'alpha à zéro, puis à nouveau à l'aide de animateTo
.
Cette opération sera répétée pendant toute la durée de vie du composable.
rememberCoroutineScope
: obtenir un champ d'application compatible avec la composition pour lancer une coroutine en dehors d'un composable
Comme LaunchedEffect
est une fonction modulable, il ne peut être utilisé que dans d'autres fonctions modulables. Pour lancer une coroutine en dehors d'un composable, mais avec une portée limitée de sorte qu'elle soit automatiquement annulée une fois qu'elle a quitté la composition, utilisez rememberCoroutineScope
.
Utilisez également rememberCoroutineScope
chaque fois que vous devez contrôler manuellement le cycle de vie d'une ou plusieurs coroutines, par exemple pour annuler une animation lors d'un événement utilisateur.
rememberCoroutineScope
est une fonction modulable qui renvoie un CoroutineScope
lié au point de la composition où il est appelé. Le champ d'application est annulé lorsque l'appel quitte la composition.
Comme dans l'exemple précédent, vous pouvez utiliser ce code pour afficher un Snackbar
lorsque l'utilisateur appuie sur un Button
:
@Composable fun MoviesScreen(snackbarHostState: SnackbarHostState) { // Creates a CoroutineScope bound to the MoviesScreen's lifecycle val scope = rememberCoroutineScope() Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { contentPadding -> Column(Modifier.padding(contentPadding)) { Button( onClick = { // Create a new coroutine in the event handler to show a snackbar scope.launch { snackbarHostState.showSnackbar("Something happened!") } } ) { Text("Press me") } } } }
rememberUpdatedState
: référencer une valeur dans un effet qui ne devrait pas redémarrer si la valeur change
LaunchedEffect
redémarre lorsque l'un des paramètres clés est modifié. Toutefois, dans certaines situations, vous souhaiterez peut-être capturer une valeur dans votre effet que vous ne souhaitez pas redémarrer si elle change. Pour ce faire, vous devez utiliser rememberUpdatedState
pour créer une référence à cette valeur, qui peut être capturée et mise à jour. Cette approche est utile pour les effets qui contiennent des opérations de longue durée dont la recréation et le redémarrage peuvent être coûteux ou interdits.
Par exemple, imaginons que votre application dispose d'un LandingScreen
qui disparaît au bout d'un certain temps. Même si LandingScreen
est recomposé, l'effet qui attend un certain temps et indique que le temps s'est écoulé ne doit pas être redémarré :
@Composable fun LandingScreen(onTimeout: () -> Unit) { // This will always refer to the latest onTimeout function that // LandingScreen was recomposed with val currentOnTimeout by rememberUpdatedState(onTimeout) // Create an effect that matches the lifecycle of LandingScreen. // If LandingScreen recomposes, the delay shouldn't start again. LaunchedEffect(true) { delay(SplashWaitTimeMillis) currentOnTimeout() } /* Landing screen content */ }
Pour créer un effet correspondant au cycle de vie du site d'appel, une constante qui ne change jamais, comme Unit
ou true
est transmise en tant que paramètre. Dans le code ci-dessus, LaunchedEffect(true)
est utilisé. Pour que le lambda onTimeout
contienne toujours la dernière valeur utilisée pour recomposer LandingScreen
, onTimeout
doit être encapsulé avec la fonction rememberUpdatedState
.
Le State
renvoyé, currentOnTimeout
dans le code, doit être utilisé dans l'effet.
DisposableEffect
: effets nécessitant un nettoyage
Pour les effets secondaires qui doivent être nettoyés après le changement de clés ou si le composable quitte la composition, utilisez DisposableEffect
.
Si les clés DisposableEffect
changent, le composable doit supprimer (procéder au nettoyage de) l'effet actuel et le réinitialiser en appelant à nouveau l'effet.
Par exemple, vous pouvez envoyer des événements d'analyse basés sur des événements Lifecycle
à l'aide d'un LifecycleObserver
.
Pour écouter ces événements dans Compose, utilisez un DisposableEffect
afin d'enregistrer et d'annuler l'enregistrement de l'observateur si nécessaire.
@Composable fun HomeScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, // Send the 'started' analytics event onStop: () -> Unit // Send the 'stopped' analytics event ) { // Safely update the current lambdas when a new one is provided val currentOnStart by rememberUpdatedState(onStart) val currentOnStop by rememberUpdatedState(onStop) // If `lifecycleOwner` changes, dispose and reset the effect DisposableEffect(lifecycleOwner) { // Create an observer that triggers our remembered callbacks // for sending analytics events val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } // Add the observer to the lifecycle lifecycleOwner.lifecycle.addObserver(observer) // When the effect leaves the Composition, remove the observer onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } /* Home screen content */ }
Dans le code ci-dessus, l'effet ajoute le observer
au lifecycleOwner
. Si lifecycleOwner
change, l'effet est supprimé et redémarré avec le nouveau lifecycleOwner
.
Un DisposableEffect
doit inclure une clause onDispose
comme instruction finale dans son bloc de code. Sinon, l'IDE affiche une erreur de temps de compilation.
SideEffect
: publier l'état de Compose dans du code non-Compose
Pour partager l'état de Compose avec des objets non gérés par Compose, utilisez le composable SideEffect
. L'utilisation d'un SideEffect
garantit que l'effet s'exécute après chaque recomposition réussie. En revanche, il est incorrect d'effectuer un effet avant qu'une recomposition réussie ne soit garantie, ce qui est le cas lorsque vous écrivez l'effet directement dans un composable.
Par exemple, votre bibliothèque d'analyse peut vous permettre de segmenter votre base d'utilisateurs en associant des métadonnées personnalisées ("propriétés utilisateur" dans cet exemple) à tous les événements d'analyse suivants. Pour communiquer le type d'utilisateur de l'utilisateur actuel à votre bibliothèque d'analyse, utilisez SideEffect
afin de mettre à jour sa valeur.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
produceState
: convertit un état autre que Compose en un état Compose.
produceState
lance une coroutine limitée à la composition qui peut transférer des valeurs dans un State
renvoyé. Utilisez-le pour convertir l'état autre que Compose en un état Compose, par exemple en intégrant un état externe basé sur un abonnement, tel que Flow
, LiveData
ou RxJava
dans la composition.
Le producteur est lancé lorsque produceState
entre dans la composition. Il est annulé lorsqu'il quitte la composition. Le State
renvoyé est combiné. Définir la même valeur ne déclenche pas de recomposition.
Bien que produceState
crée une coroutine, il peut également être utilisé pour observer des sources de données sans suspension. Pour supprimer l'abonnement à cette source, utilisez la fonction awaitDispose
.
L'exemple suivant montre comment utiliser produceState
pour charger une image à partir du réseau. La fonction modulable loadNetworkImage
renvoie un State
utilisable dans d'autres composables.
@Composable fun loadNetworkImage( url: String, imageRepository: ImageRepository = ImageRepository() ): State<Result<Image>> { // Creates a State<T> with Result.Loading as initial value // If either `url` or `imageRepository` changes, the running producer // will cancel and will be re-launched with the new inputs. return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) { // In a coroutine, can make suspend calls val image = imageRepository.load(url) // Update State with either an Error or Success result. // This will trigger a recomposition where this State is read value = if (image == null) { Result.Error } else { Result.Success(image) } } }
derivedStateOf
: convertit un ou plusieurs objets d'état en un autre état.
Dans Compose, une recomposition a lieu chaque fois qu'un objet d'état observé ou que l'entrée d'un composable change. Un objet d'état ou une entrée peuvent changer plus souvent que l'interface utilisateur n'a besoin d'être mise à jour, conduisant à une recomposition inutile.
Vous devez utiliser la fonction derivedStateOf
lorsque les entrées d'un composable changent plus souvent qu'il n'est nécessaire de recomposer. Cela se produit souvent lorsque quelque chose change fréquemment, comme une position de défilement, mais que le composable ne doit réagir qu'une fois qu'il a franchi un certain seuil. derivedStateOf
crée un nouvel objet d'état Compose que vous pouvez observer et qui ne se met à jour qu'en fonction de vos besoins. Ainsi, il agit de la même manière que l'opérateur de flux Kotlin distinctUntilChanged()
.
Utilisation correcte
L'extrait de code suivant montre un cas d'utilisation approprié de derivedStateOf
:
@Composable // When the messages parameter changes, the MessageList // composable recomposes. derivedStateOf does not // affect this recomposition. fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
Dans cet extrait de code, firstVisibleItemIndex
change dès que le premier élément visible change. En avançant dans le code, la valeur devient 0
, 1
, 2
, 3
, 4
, 5
, etc.
Cependant, la recomposition ne doit avoir lieu que si la valeur est supérieure à 0
.
Ce décalage dans la fréquence de mise à jour montre qu'il s'agit d'un bon cas d'utilisation pour derivedStateOf
.
Utilisation incorrecte
Une erreur courante est d'assumer que lorsque vous combinez deux objets d'état Compose vous devez utiliser derivedStateOf
car vous "modifiez l'état". Cependant, ceci ne représente qu'un surcoût et n'est pas nécessaire, comme le montre l'extrait de code suivant :
// DO NOT USE. Incorrect usage of derivedStateOf. var firstName by remember { mutableStateOf("") } var lastName by remember { mutableStateOf("") } val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!! val fullNameCorrect = "$firstName $lastName" // This is correct
Dans cet extrait, fullName
doit se mettre autant à jour que firstName
et lastName
. Ainsi, nous ne sommes pas en présence d'une recomposition excessive. Utiliser derivedStateOf
n'est donc pas nécessaire.
snapshotFlow
: convertir l'état de Compose en flux
Utilisez snapshotFlow
pour convertir des objets State<T>
en flux froid. snapshotFlow
exécute son bloc lorsqu'il est collecté et émet le résultat des objets State
qui y sont lus. Lorsque l'un des objets State
lus à l'intérieur du bloc snapshotFlow
est modifié, le flux émet la nouvelle valeur pour son collecteur si la nouvelle valeur n'est pas égale à à la valeur précédemment émise (ce comportement est semblable à celui de Flow.distinctUntilChanged
).
L'exemple suivant illustre un effet secondaire enregistré lorsque l'utilisateur dépasse le premier élément en faisant défiler une liste à des fins d'analyse :
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
Dans le code ci-dessus, listState.firstVisibleItemIndex
est converti en flux pouvant bénéficier de la puissance des opérateurs de flux.
Redémarrage des effets
Certains effets dans Compose, comme LaunchedEffect
, produceState
ou DisposableEffect
, utilisent un nombre variable d'arguments ou de clés, qui permettent d'annuler l'effet en cours d'exécution et d'en démarrer un autre avec les nouvelles clés.
En général, ces API se présentent sous la forme suivante :
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
La subtilité de ce comportement peut provoquer des problèmes si les paramètres utilisés pour redémarrer l'effet ne sont pas corrects :
- Redémarrer les effets moins souvent que nécessaire peut provoquer des bugs dans votre application.
- Redémarrer les effets plus souvent que nécessaire peut s'avérer inefficace.
En règle générale, les variables modifiables et immuables utilisées dans le bloc d'effet du code doivent être ajoutées en tant que paramètres au composable d'effet. Outre ces variables, vous pouvez ajouter d'autres paramètres pour forcer le redémarrage de l'effet. Si la modification d'une variable ne doit pas entraîner le redémarrage de l'effet, la variable doit être encapsulée dans rememberUpdatedState
. Si la variable ne change jamais, car elle est encapsulée dans un remember
sans clé, vous n'avez pas besoin de transmettre la variable en tant que clé à l'effet.
Dans le code DisposableEffect
présenté ci-dessus, l'effet prend comme paramètre le lifecycleOwner
utilisé dans son bloc, car toute modification doit entraîner le redémarrage de l'effet.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
currentOnStart
et currentOnStop
ne sont pas nécessaires en tant que clés DisposableEffect
, car leur valeur ne change jamais dans la composition en raison de l'utilisation de rememberUpdatedState
. Si vous ne transmettez pas lifecycleOwner
en tant que paramètre et qu'il change, HomeScreen
se recompose, mais DisposableEffect
n'est pas supprimé ni redémarré. Cela provoque des problèmes, car le mauvais lifecycleOwner
est utilisé à partir de ce moment-là.
Constantes comme clés
Vous pouvez utiliser une constante telle que true
comme clé d'effet pour la faire suivre le cycle de vie du site d'appel. Il existe des cas d'utilisation valides, tels que l'exemple de LaunchedEffect
présenté ci-dessus. Cependant, avant de commencer, réfléchissez bien et vérifiez que c'est bien ce dont vous avez besoin.
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé.
- États et Jetpack Compose
- Kotlin pour Jetpack Compose
- Utiliser les vues dans Compose