Utiliser des coroutines Kotlin avec des composants tenant compte du cycle de vie

Les coroutines Kotlin fournissent une API qui vous permet d'écrire du code asynchrone. Les coroutines Kotlin vous permettent de définir un élément CoroutineScope, qui vous aide à gérer le moment où vos coroutines doivent s'exécuter. Chaque opération asynchrone s'exécute dans un champ d'application particulier.

Les composants tenant compte des cycles de vie offrent une compatibilité de premier ordre pour les coroutines des champs d'application logiques de votre application. Ce document explique comment utiliser efficacement les coroutines avec des composants tenant compte du cycle de vie.

Ajouter des dépendances

Les champs d'application de coroutine intégrés décrits dans cet article sont contenus dans l'API Lifecycle. Veillez à ajouter les dépendances appropriées lorsque vous utilisez ces champs d'application.

  • Pour les utilitaires ViewModel dans Compose, utilisez implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version").
  • Pour les utilitaires Lifecycle dans Compose, utilisez implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version").

Champs d'application des coroutines tenant compte du cycle de vie

Compose et les bibliothèques Lifecycle fournissent les champs d'application intégrés suivants que vous pouvez utiliser dans votre application.

ViewModelScope

Un ViewModelScope est défini pour chaque ViewModel de votre appli. Toute coroutine lancée dans ce champ d'application est automatiquement annulée si l'élément ViewModel est effacé. Les coroutines sont utiles ici si des tâches ne doivent être effectuées que si l'élément ViewModel est actif. Par exemple, si vous calculez des données pour une mise en page, vous devez limiter la tâche à l'élément ViewModel. Si l'élément ViewModel est effacé, la tâche est automatiquement annulée pour éviter de consommer des ressources.

Vous pouvez accéder à l'élément CoroutineScope d'un élément ViewModel via la propriété viewModelScope de l'élément ViewModel, comme illustré dans l'exemple suivant :

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

Pour des cas d'utilisation plus avancés, vous pouvez transmettre un élément CoroutineScope personnalisé directement au constructeur du ViewModel pour remplacer l'élément viewModelScope par défaut. Cette approche offre plus de contrôle et de flexibilité, en particulier pour les cas suivants :

  • Tests : elle vous permet d'injecter un élément TestScope, ce qui facilite le contrôle du temps et la vérification du comportement des coroutines dans les tests unitaires.

  • Configuration personnalisée : vous pouvez configurer le champ d'application avec un élément CoroutineDispatcher spécifique (comme Dispatchers.Default pour les calculs lourds) ou un élément CoroutineExceptionHandler personnalisé avant même que le ViewModel ne commence son travail.

Champs d'application liés à la composition

Les effets secondaires tels que les animations, les appels réseau ou les minuteurs doivent être limités au cycle de vie du composable. Ainsi, lorsqu'un composable quitte l'écran (quitte la composition), toutes les coroutines en cours d'exécution sont automatiquement annulées pour éviter les fuites de mémoire.

Compose fournit l'API LaunchedEffect pour gérer la définition du champ d'application de la composition de manière déclarative.

LaunchedEffect crée un CoroutineScope qui vous permet d'exécuter des fonctions de suspension. Le champ d'application est lié au cycle de vie de la composition du composable, et non au cycle de vie de l'activité hôte.

  • Entrée : la coroutine démarre lorsque le composable entre dans la composition.
  • Sortie : la coroutine est annulée lorsque le composable quitte la composition.
  • Relancement : si une clé transmise à LaunchedEffect change, la coroutine existante est annulée et une nouvelle est lancée.

L'exemple suivant montre comment utiliser LaunchedEffect pour créer une animation de pulsation. La coroutine est liée à la présence du composable dans la composition et réagit aux modifications de configuration :

// 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 { mutableLongStateOf(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)
    }
}

Pour en savoir plus sur LaunchedEffect, consultez Effets secondaires dans Compose.

Collecte de flux tenant compte du cycle de vie

Pour collecter des flux en toute sécurité dans Jetpack Compose, utilisez l' collectAsStateWithLifecycle API. Cette fonction unique convertit un élément Flow en objet State Compose et gère automatiquement l'abonnement au cycle de vie pour vous. Par défaut, la collecte commence lorsque le cycle de vie est STARTED et s'arrête lorsqu'il est STOPPED. Pour remplacer ce comportement par défaut, transmettez le paramètre minActiveState avec la méthode de cycle de vie souhaitée, comme Lifecycle.State.RESUMED.

L'exemple suivant montre comment collecter un élément StateFlow de ViewModel dans un composable :

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

Collecte parallèle de plusieurs flux

Dans Compose, vous pouvez collecter plusieurs flux en parallèle en déclarant plusieurs variables d'état. Étant donné que collectAsStateWithLifecycle gère son propre champ d'application sous-jacent, la collecte parallèle est gérée automatiquement :

@Composable
fun DashboardScreen(viewModel: DashboardViewModel = viewModel()) {
    // Both flows are collected safely in parallel and will emit updates when either changes, the composables will recompose
    val userData by viewModel.userFlow.collectAsStateWithLifecycle()
    val feedData by viewModel.feedFlow.collectAsStateWithLifecycle()

    // ...
}

Calculer des valeurs de manière asynchrone à l'aide de flux

Lorsque vous devez calculer des valeurs de manière asynchrone, utilisez StateFlow avec l'opérateur stateIn.

L'extrait de code suivant utilise un élément Flow standard converti en élément StateFlow. Le WhileSubscribed(5000) paramètre maintient l'abonnement actif pendant cinq secondes après la disparition de l'UI pour gérer les modifications de configuration.

val uiState: StateFlow<Result> = flow {
    emit(repository.fetchData())
}
.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5_000),
    initialValue = Result.Loading
)

Utilisez collectAsStateWithLifecycle pour convertir les valeurs collectées en élément Compose State, afin que votre UI puisse se mettre à jour de manière réactive chaque fois que les données changent.

Pour en savoir plus sur l'état, consultez États et Jetpack Compose.

Ressources supplémentaires

Afficher le contenu

Exemples