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, ainsi qu'une couche d'interopérabilité avec LiveData. Cet article explique comment utiliser efficacement les coroutines avec des composants tenant compte du cycle de vie.

Ajouter des dépendances KTX

Les champs d'application de coroutine intégrés décrits dans cet article sont contenus dans les extensions KTX de chaque composant correspondant. Veillez à ajouter les dépendances appropriées lorsque vous utilisez ces champs d'application.

  • Pour ViewModelScope, utilisez androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 ou une version ultérieure.
  • Pour LifecycleScope, utilisez androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 ou une version ultérieure.
  • Pour liveData, utilisez androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 ou une version ultérieure.

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

Les composants tenant compte du cycle de vie définissent les champs d'application intégrés suivants que vous pouvez utiliser dans votre application.

ViewModelScope

Un élément ViewModelScope est défini pour chaque élément 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 du ViewModel, comme indiqué dans l'exemple suivant :

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

LifecycleScope

Un élément LifecycleScope est défini pour chaque objet Lifecycle. Toute coroutine lancée dans ce champ d'application est annulée lorsque l'élément Lifecycle est détruit. Vous pouvez accéder à l'élément CoroutineScope de Lifecycle via les propriétés lifecycle.coroutineScope ou lifecycleOwner.lifecycleScope.

L'exemple ci-dessous montre comment utiliser lifecycleOwner.lifecycleScope pour créer du texte précalculé de manière asynchrone :

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

Coroutines redémarrables tenant compte du cycle de vie

Même si lifecycleScope fournit un moyen approprié d'annuler automatiquement les opérations de longue durée lorsque Lifecycle est défini sur DESTROYED, vous pourriez, dans d'autres situations, avoir besoin de lancer l'exécution d'un bloc de code lorsque Lifecycle se trouve dans un certain état, et annuler l'opération lorsqu'il se trouve dans un autre état. Par exemple, vous pouvez collecter un flux lorsque Lifecycle est défini sur STARTED et l'annuler lorsqu'il est défini sur STOPPED. Cette approche ne traite les émissions de flux que lorsque l'interface utilisateur est visible à l'écran, ce qui permet d'économiser des ressources et d'éviter les plantages de l'application.

Lifecycle et LifecycleOwner fournissent alors l'API de suspension repeatOnLifecycle qui effectue exactement cette opération. L'exemple suivant contient un bloc de code qui s'exécute chaque fois que l'élément Lifecycle associé est au moins à l'état STARTED et s'annule lorsque Lifecycle est à l'état STOPPED :

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

Collecte de flux tenant compte du cycle de vie

Si vous ne devez effectuer une collecte tenant compte du cycle de vie que sur un seul flux, vous pouvez utiliser la méthode Flow.flowWithLifecycle() pour simplifier votre code :

viewLifecycleOwner.lifecycleScope.launch {
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect {
            // Process the value.
        }
}

En revanche, si vous devez effectuer une collecte tenant compte du cycle de vie sur plusieurs flux en parallèle, vous devez collecter chaque flux dans des coroutines différentes. Il est alors plus efficace d'utiliser directement repeatOnLifecycle() :

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // Because collect is a suspend function, if you want to
        // collect multiple flows in parallel, you need to do so in
        // different coroutines.
        launch {
            flow1.collect { /* Process the value. */ }
        }

        launch {
            flow2.collect { /* Process the value. */ }
        }
    }
}

Suspendre les coroutines tenant compte du cycle de vie

Bien que CoroutineScope fournisse un moyen approprié d'annuler automatiquement les opérations de longue durée, vous pourriez, dans d'autres situations, avoir besoin de suspendre l'exécution d'un bloc de code, à moins que l'élément Lifecycle présente un état spécifique. Par exemple, pour exécuter un élément FragmentTransaction, attendez que Lifecycle soit au moins à l'état STARTED. Lifecycle fournit alors des méthodes supplémentaires : lifecycle.whenCreated, lifecycle.whenStarted et lifecycle.whenResumed. Toute coroutine exécutée dans ces blocs est suspendue si Lifecycle n'est pas au moins à l'état minimal souhaité.

L'exemple ci-dessous contient un bloc de code qui ne s'exécute que lorsque l'élément Lifecycle associé est au moins à l'état STARTED :

class MyFragment: Fragment {
    init { // Notice that we can safely launch in the constructor of the Fragment.
        lifecycleScope.launch {
            whenStarted {
                // The block inside will run only when Lifecycle is at least STARTED.
                // It will start executing when fragment is started and
                // can call other suspend methods.
                loadingView.visibility = View.VISIBLE
                val canAccess = withContext(Dispatchers.IO) {
                    checkUserAccess()
                }

                // When checkUserAccess returns, the next line is automatically
                // suspended if the Lifecycle is not *at least* STARTED.
                // We could safely run fragment transactions because we know the
                // code won't run unless the lifecycle is at least STARTED.
                loadingView.visibility = View.GONE
                if (canAccess == false) {
                    findNavController().popBackStack()
                } else {
                    showContent()
                }
            }

            // This line runs only after the whenStarted block above has completed.

        }
    }
}

Si Lifecycle est détruit alors qu'une coroutine est active via l'une des méthodes when, la coroutine est automatiquement annulée. Dans l'exemple ci-dessous, le bloc finally s'exécute une fois que l'état Lifecycle est DESTROYED :

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // Call some suspend functions.
            } finally {
                // This line might execute after Lifecycle is DESTROYED.
                if (lifecycle.state >= STARTED) {
                    // Here, since we've checked, it is safe to run any
                    // Fragment transactions.
                }
            }
        }
    }
}

Utiliser des coroutines avec LiveData

Si vous utilisez LiveData, vous devrez peut-être calculer des valeurs de manière asynchrone. Par exemple, vous pouvez récupérer les préférences d'un utilisateur et les diffuser dans votre interface utilisateur. Dans ce cas, vous pouvez utiliser la fonction liveData du compilateur pour appeler une fonction suspend et diffuser le résultat en tant qu'objet LiveData.

Dans l'exemple ci-dessous, loadUser() est une fonction de suspension déclarée ailleurs. Utilisez la fonction de compilateur liveData pour appeler loadUser() de manière asynchrone, puis utilisez emit() pour émettre le résultat :

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

Le composant liveData sert de primitive de simultanéité structurée entre les coroutines et LiveData. Le bloc de code s'exécute lorsque LiveData devient actif et est automatiquement annulé après un délai configurable lorsque LiveData devient inactif. S'il est annulé avant la fin, il est redémarré si LiveData redevient actif. S'il se termine correctement dans une exécution précédente, il ne redémarre pas. Notez qu'il n'est redémarré que s'il est annulé automatiquement. Si le bloc est annulé pour une autre raison (par exemple, en générant une erreur CancellationException), il n'est pas redémarré.

Vous pouvez également émettre plusieurs valeurs à partir du bloc. Chaque appel emit() suspend l'exécution du bloc jusqu'à ce que la valeur LiveData soit définie sur le thread principal.

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

Vous pouvez également combiner liveData avec Transformations, comme illustré dans l'exemple suivant :

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

Vous pouvez émettre plusieurs valeurs à partir de LiveData en appelant la fonction emitSource() chaque fois que vous souhaitez émettre une nouvelle valeur. Notez que chaque appel de la fonction emit() ou emitSource() supprime la source précédemment ajoutée.

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            // Stop the previous emission to avoid dispatching the updated user
            // as `loading`.
            disposable.dispose()
            // Update the database.
            userDao.insert(user)
            // Re-establish the emission with success type.
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            // Any call to `emit` disposes the previous one automatically so we don't
            // need to dispose it here as we didn't get an updated value.
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

Pour en savoir plus sur les coroutines, consultez les liens suivants :

Ressources supplémentaires

Pour en savoir plus sur l'utilisation de coroutines avec des composants tenant compte du cycle de vie, consultez les ressources supplémentaires suivantes.

Exemples

Blogs