Coroutines Kotlin vous permettent d'écrire un code asynchrone clair et simplifié la réactivité de votre application tout en gérant les tâches de longue durée, comme les appels réseau. ou des opérations de disque.
Cet article fournit une présentation détaillée des coroutines sur Android. Si vous ne maîtrisez pas les coroutines, assurez-vous de lire l'article Coroutines Kotlin sur Android avant de lire celui-ci.
Gérer les tâches de longue durée
Les coroutines s'appuient sur des fonctions standards en ajoutant deux opérations pour gérer les tâches de longue durée. En plus de invoke
(ou call
) et return
, les coroutines ajoutent suspend
et resume
:
suspend
interrompt l'exécution de la coroutine actuelle, ce qui enregistre toutes les variables locales.resume
reprend l'exécution d'une coroutine suspendue de là où elle a été interrompue.
Vous ne pouvez appeler des fonctions suspend
qu'à partir d'autres fonctions suspend
ou en utilisant un constructeur de coroutines tel que launch
pour démarrer une nouvelle coroutine.
L'exemple suivant illustre une implémentation simple de coroutine pour une tâche de longue durée hypothétique :
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
Dans cet exemple, get()
s'exécute toujours sur le thread principal, mais il suspend la coroutine avant de lancer la requête réseau. Une fois la requête réseau terminée, get
reprend la coroutine suspendue au lieu d'utiliser un rappel pour avertir le thread principal.
Kotlin utilise un bloc de pile pour gérer la fonction qui s'exécute avec toutes les variables locales. Lorsque vous suspendez une coroutine, le bloc de pile actuel est copié et enregistré pour plus tard. Lors de la reprise, le bloc de pile est recopié à partir de l'endroit où il a été enregistré, et la fonction reprend son exécution. Même si le code peut ressembler à une requête de blocage séquentiel ordinaire, la coroutine garantit que la requête réseau évite de bloquer le thread principal.
Utiliser des coroutines pour la sécurité principale
Les coroutines Kotlin utilisent des coordinateurs afin de déterminer les threads utilisés pour leur exécution. Pour exécuter du code en dehors du thread principal, vous pouvez demander aux coroutines Kotlin d'effectuer des tâches sur le coordinateur par défaut ou IO. Dans Kotlin, toutes les coroutines doivent s'exécuter dans un coordinateur, même lorsqu'elles s'exécutent sur le thread principal. Les coroutines peuvent se suspendre elles-mêmes, et le coordinateur se charge de les réactiver.
Pour spécifier où les coroutines doivent s'exécuter, Kotlin fournit trois coordinateurs que vous pouvez utiliser :
- Dispatchers.Main : permet d'exécuter une coroutine sur le thread Android principal. Ce coordinateur ne doit être utilisé que pour interagir avec l'interface utilisateur et pour effectuer des opérations rapides. Par exemple, vous pouvez appeler des fonctions
suspend
, exécuter des opérations de la structure d'IU Android et mettre à jour des objetsLiveData
. - Dispatchers.IO : ce coordinateur est optimisé pour effectuer des opérations d'E/S sur le disque ou le réseau en dehors du thread principal. Exemples : utilisation du composant Room, lecture ou écriture dans des fichiers et exécution d'opérations réseau.
- Dispatchers.Default : ce coordinateur est optimisé pour effectuer des tâches nécessitant une utilisation intensive des processeurs en dehors du thread principal. Parmi les cas d'utilisation typiques, on peut citer des tâches comme trier une liste ou effectuer une analyse JSON.
Pour reprendre l'exemple précédent, vous pouvez utiliser les coordinateurs pour redéfinir la fonction get
. Dans le corps de get
, appelez withContext(Dispatchers.IO)
pour créer un bloc qui s'exécute sur le pool de threads d'E/S. Le code que vous placez dans ce bloc s'exécute toujours via le coordinateur IO
. Comme withContext
est une fonction de suspension, la fonction get
l'est également.
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
Les coroutines vous permettent de coordonner les threads avec précision. Étant donné que withContext()
vous permet de contrôler le pool de threads de n'importe quelle ligne de code sans introduire de rappels, vous pouvez l'appliquer à de très petites fonctions, telles que la lecture d'une base de données ou l'exécution d'une requête réseau. Une bonne pratique consiste à utiliser withContext()
pour vous assurer que chaque fonction est sécurisée, ce qui signifie que vous pouvez l'appeler à partir du thread principal. De cette façon, l'appelant n'a jamais besoin de déterminer quel thread doit être utilisé pour exécuter la fonction.
Dans l'exemple précédent, fetchDocs()
s'exécute sur le thread principal. Cependant, il peut appeler get
de façon sécurisée, qui effectue une requête réseau en arrière-plan.
Étant donné que les coroutines prennent en charge les fonctions suspend
et resume
, la coroutine sur le thread principal est reprise avec le résultat get
dès que le bloc withContext
est terminé.
Performances de withContext()
withContext()
n'entraîne pas de frais supplémentaires par rapport à une solution équivalente basée sur le rappel
la mise en œuvre. De plus, dans certaines situations, il est possible d'optimiser les appels withContext()
au-delà d'une implémentation équivalente basée sur le rappel. Par exemple, si une fonction effectue dix appels vers un réseau, vous pouvez demander à Kotlin de changer de thread une seule fois en utilisant un withContext()
externe. Ainsi, même si la bibliothèque réseau utilise withContext()
plusieurs fois, elle reste sur le même coordinateur sans avoir à changer de thread. En outre, Kotlin optimise le passage entre Dispatchers.Default
et Dispatchers.IO
afin d'éviter les commutations de threads dans la mesure du possible.
Démarrer une coroutine
Deux options s'offrent à vous pour démarrer des coroutines :
launch
démarre une nouvelle coroutine et ne renvoie pas le résultat à l'appelant. Toutes les opérations de type "fire and forget" peuvent être lancées aveclaunch
.async
démarre une nouvelle coroutine et vous permet de renvoyer un résultat avec une instruction "suspend" appeléeawait
.
En règle générale, vous devez exécuter l'action launch
pour une nouvelle coroutine à partir d'une fonction standard, car une telle fonction ne peut pas appeler await
. Utilisez async
uniquement à l'intérieur d'une autre coroutine ou à l'intérieur d'une fonction de suspension et en effectuant une décomposition en parallèle.
Décomposition en parallèle
Toutes les coroutines démarrées dans une fonction suspend
doivent être arrêtées lorsqu'elle est renvoyée. Vous devez donc sans doute vous assurer que ces coroutines se terminent avant d'être renvoyée. Avec la simultanéité structurée en Kotlin, vous pouvez définir une coroutineScope
qui démarre une ou plusieurs coroutines. Ensuite, en utilisant await()
(pour une seule coroutine) ou awaitAll()
(pour plusieurs), vous pouvez garantir que celles-ci se terminent avant d'être renvoyées à partir de la fonction.
Définissons, par exemple, une valeur coroutineScope
qui récupère deux documents de manière asynchrone. En appelant await()
sur chaque référence différée, nous garantissons que les deux opérations async
se terminent avant de renvoyer une valeur :
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
Vous pouvez également utiliser awaitAll()
sur des collections, comme illustré dans l'exemple suivant :
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
Même si fetchTwoDocs()
lance de nouvelles coroutines avec async
, la fonction utilise awaitAll()
pour attendre que les coroutines lancées se terminent avant de renvoyer une valeur. Notez cependant que même si nous n'avions pas appelé awaitAll()
, le constructeur coroutineScope
ne reprend la coroutine qui a appelé fetchTwoDocs
qu'une fois toutes les nouvelles coroutines terminées.
De plus, coroutineScope
intercepte toutes les exceptions que les coroutines génèrent et les renvoie vers l'appelant.
Pour en savoir plus sur la décomposition en parallèle, consultez la section Composer des fonctions de suspension.
Concepts de coroutines
CoroutineScope
Un CoroutineScope
effectue le suivi de toute coroutine qu'il crée à l'aide de launch
ou async
. Vous pouvez annuler les opérations en cours (c'est-à-dire les coroutines en cours d'exécution) à tout moment en appelant scope.cancel()
. Sous Android, certaines bibliothèques KTX fournissent leur propre CoroutineScope
pour certaines classes du cycle de vie. Par exemple, ViewModel
a un viewModelScope
, et Lifecycle
a un lifecycleScope
.
Cependant, contrairement à un coordinateur, un CoroutineScope
n'exécute pas les coroutines.
viewModelScope
est également utilisé dans les exemples de la section Exécuter des threads d'arrière-plan sur Android avec les coroutines.
Cependant, si vous avez besoin de créer votre propre CoroutineScope
pour contrôler le cycle de vie des coroutines dans une couche particulière de votre application, vous pouvez en créer un comme suit :
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
Un champ d'application annulé ne peut pas créer plus de coroutines. Par conséquent, vous ne devez appeler scope.cancel()
que lorsque la classe qui contrôle son cycle de vie est détruite. Lorsque vous utilisez viewModelScope
, la classe ViewModel
annule automatiquement le champ d'application dans la méthode onCleared()
du ViewModel.
Job
Un Job
est un handle vers une coroutine. Chaque coroutine que vous créez avec launch
ou async
renvoie une instance de Job
qui identifie de manière unique la coroutine et gère son cycle de vie. Vous pouvez également transmettre un Job
à un CoroutineScope
pour mieux gérer son cycle de vie, comme illustré dans l'exemple suivant :
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
CoroutineContext
Un CoroutineContext
définit le comportement d'une coroutine à l'aide de l'ensemble d'éléments suivant :
Job
: contrôle le cycle de vie de la coroutine.CoroutineDispatcher
: distribue des tâches au thread approprié.CoroutineName
: indique le nom de la coroutine, utile pour le débogage.CoroutineExceptionHandler
: gère les exceptions non interceptées.
Pour les nouvelles coroutines créées dans un champ d'application, une nouvelle instance de Job
est attribuée à la nouvelle coroutine, et les autres éléments du CoroutineContext
sont hérités du champ d'application qui les contient. Vous pouvez remplacer les éléments hérités en transmettant un nouveau CoroutineContext
à la fonction launch
ou async
. Notez que transmettre Job
à launch
ou async
n'a aucun effet, car une nouvelle instance de Job
est toujours attribuée à une nouvelle coroutine.
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
Ressources supplémentaires sur les coroutines
Pour plus de ressources sur les coroutines, consultez les liens suivants :