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écuter des fonctions de suspension dans le champ d'application d'une fonction modulable
Pour appeler des fonctions de suspension en toute sécurité depuis un composable, 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, l'affichage d'un Snackbar
dans un Scaffold
est effectué à l'aide de la fonction SnackbarHostState.showSnackbar
, qui est une fonction de suspension.
@Composable
fun MyScreen(
state: UiState<List<Movie>>,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
// If the UI state contains an error, show snackbar
if (state.hasError) {
// `LaunchedEffect` will cancel and re-launch if
// `scaffoldState.snackbarHostState` changes
LaunchedEffect(scaffoldState.snackbarHostState) {
// Show snackbar using a coroutine, when the coroutine is cancelled the
// snackbar will automatically dismiss. This coroutine will cancel whenever
// `state.hasError` is false, and only start when `state.hasError` is true
// (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
scaffoldState.snackbarHostState.showSnackbar(
message = "Error message",
actionLabel = "Retry message"
)
}
}
Scaffold(scaffoldState = scaffoldState) {
/* ... */
}
}
Dans le code ci-dessus, une coroutine est déclenchée si l'état contient une erreur et est annulée dans le cas contraire. Comme le site d'appel LaunchedEffect
se trouve dans une instruction if, lorsque cette instruction est fausse, si LaunchedEffect
était dans la composition, il est supprimé et la coroutine est annulée.
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(scaffoldState: ScaffoldState = rememberScaffoldState()) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
Column {
/* ... */
Button(
onClick = {
// Create a new coroutine in the event handler to show a snackbar
scope.launch {
scaffoldState.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
, car il est appelé à chaque recomposition réussie.
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 rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* ... */
}
// 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 : convertir 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
): 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 : convertir un ou plusieurs objets d'état en un autre état
Utilisez derivedStateOf
lorsqu'un certain état est calculé ou dérivé d'autres objets d'état. L'utilisation de cette fonction garantit que le calcul n'a lieu que lorsque l'un des états utilisés est modifié.
L'exemple suivant montre une liste À faire de base dont les tâches avec des mots clés à priorité élevée définis par l'utilisateur apparaissent en premier :
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
val todoTasks = remember { mutableStateListOf<String>() }
// Calculate high priority tasks only when the todoTasks or highPriorityKeywords
// change, not on every recomposition
val highPriorityTasks by remember(highPriorityKeywords) {
derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
}
Box(Modifier.fillMaxSize()) {
LazyColumn {
items(highPriorityTasks) { /* ... */ }
items(todoTasks) { /* ... */ }
}
/* Rest of the UI where users can add elements to the list */
}
}
Dans le code ci-dessus, derivedStateOf
garantit que chaque fois que todoTasks
change, le calcul de highPriorityTasks
est effectué et l'interface utilisateur est mise à jour en conséquence. Si highPriorityKeywords
change, le bloc remember
est exécuté et un objet d'état dérivé est créé et mémorisé à la place de l'ancien. Le filtrage pour calculer highPriorityTasks
pouvant être coûteux, il ne doit être exécuté que lorsque l'une des listes change et non à chaque recomposition.
De plus, une mise à jour de l'état produite par derivedStateOf
n'entraîne pas la recomposition du composable où il est déclaré. Compose ne recompose que les composables dont l'état renvoyé est lu, à l'intérieur de LazyColumn
dans l'exemple.
Le code suppose également que highPriorityKeywords
change beaucoup moins souvent que todoTasks
. Si ce n'était pas le cas, le code pourrait utiliser remember(todoTasks, highPriorityKeywords)
au lieu de derivedStateOf
.
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.