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
, utilisezandroidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
ou une version ultérieure. - Pour
LifecycleScope
, utilisezandroidx.lifecycle:lifecycle-runtime-ktx:2.4.0
ou une version ultérieure. - Pour
liveData
, utilisezandroidx.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 :
- Améliorer les performances des applications avec des coroutines Kotlin
- Présentation des coroutines
- Exécuter des threads dans CoroutineWorker
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
- Coroutines sur Android : modèles d'application
- Coroutines faciles dans Android : viewModelScope
- Tester deux émissions LiveData consécutives dans des coroutines
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Présentation de LiveData
- Gérer les cycles de vie à l'aide de composants tenant compte des cycles de vie
- Charger et afficher des données paginées