Les coroutines dans Kotlin : Concepts et manipulation
Kotlin a annoncé l’intégration des coroutines à partir de la version 1.1, à l’état expérimental.
Qu’est-ce qu’une coroutine ?
Kotlin a annoncé l’intégration des coroutines à partir de la version 1.1. Elle dispose cependant d’un statut “expérimental”. La librairie standard kotlin.coroutines.experimental. Il existe également une librairie kotlinx.coroutines qui fournit en plus une collection de wrapper et de helpers pour manipuler les coroutines.
Pour démarrer, je vous propose de découvrir les principaux concepts des coroutines Kotlin.
Coroutine
Les coroutines ouvrent une nouvelle voie pour écrire du code asynchrone et non bloquant. Une coroutine est une instance de calcul suspendable. Cela ressemble conceptuellement à un thread dans le sens où elle exécute un bloc de code et possède un cycle de vie similaire. Elle est créée et démarrée. Cependant, une coroutine n’est pas liée à un thread en particulier. Elle peut suspendre son exécution dans un thread et continuer dans un autre. Par ailleurs, elle peut s’achever comme une future ou une promise avec un résultat ou une exception.
Suspending function
Une suspending function est une fonction ordinaire marquée par le modifier suspend. Elle peut suspendre l’exécution d’un bloc de code sans bloquer l’exécution du thread courant. On ne peut invoquer une suspending function que depuis une autre suspending function ou depuis des suspending lambdas. Lorsque la suspending function est invoquée, on obtient un point de suspension. Un point de suspension est un point durant l’exécution d’une coroutine où l’exécution de la coroutine peut être suspendue.
Suspending lambda
Une suspending lambda est un bloc de code qui peut être exécuté au sein d’une coroutine. Elle est quasiment identique à une lambda ordinaire hormis son type fonctionnel qui est marqué du modifier suspend. De la même façon qu’une lambda est la forme syntaxique courte d’une fonction locale anonyme, une suspending lambda est la forme syntaxique courte d’une fonction suspendable anonyme. Elle peut suspendre l’exécution de code sans bloquer le thread courant en invoquant des suspending function. Par exemple, un bloc de code entouré d’accolades après la coroutine builder launch, future ou buildSequence est une suspending lambda.
Suspending function type
Une suspending function type est un type de fonction pour les suspending functions et lambdas. Par exemple, suspend () -> Int est une type de suspending function sans arguments qui retourne un Int. Une suspending function déclarée comme suspend fun foo(): Int sera conforme au type de fonction au-dessus.
Coroutine builder
Une coroutine builder est une fonction qui va prendre en argument une suspending lambda, créer une coroutine et dans certains cas donner accès à un résultat.
Les coroutine builder launch() et future() par exemple sont définies dans une bibliothèque. La bibliothèque standard fournit des coroutine builder primitifs utilisées pour définir tous les autres coroutine builder.
Sous le capot des coroutines
Je vous propose de plonger un peu plus dans le fonctionnement des coroutines pour mieux appréhender leur fonctionnement.
Prenons l’exemple d’une suspending function pour expliquer comment le compiler exécutera la fonction en arrière-plan.
//Kotlin
suspend fun createPost(token: Token, item: Item): Post {…}
Voici une fonction ordinaire à laquelle on applique le modifier suspend pour la transformer en suspending function.
À la compilation, Kotlin interprétera notre fonction en ajoutant implicitement un callback Continuation.
//Java/JVM
Object createPost(Token token, Item item, Continuation<Post> cont): Post {...}
Continuation n’est en fait qu’une interface générique de callback. Le callback ici est implicite. Visuellement, on n’a pas l’impression de gérer de l’asynchrone.
interface Continuation<in T> {
val context: CoroutineContext
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
Comme n’importe quelle classe de callback, Continuation aura 2 méthodes. resume pour gérer les succès et resumeWithException pour gérer les erreurs.
L’interface dispose également d’une classe immutable CoroutineContext.
Coroutine context
Coroutine context est une collection d’objets définis par l’utilisateur qui pourront être attachés à une coroutine. Par exemple, des objets responsables de la politique de threading, les logs, les aspects sécurité et transaction de l’exécution de la coroutine, l’identité et le nom de la coroutine.
Une coroutine context n’est qu’une collection de variables thread local. La coroutine context est immutable.
La bibliothèque standard ne contient aucune implémentation concrète de context mais définit des interfaces et des classes abstraites.
Conceptuellement, coroutine context n’est qu’une collection d’éléments indexés dans laquelle chaque élément possède une clé unique.
Interface minimale CoroutineContex définie dans la bibliothèque standard :
interface CoroutineContext {
operator fun <E : Element> get(key: Key<E>): E?
fun <R> fold(initial: R, operation: (R, Element) -> R): R
operator fun plus(context: CoroutineContext): CoroutineContext
fun minusKey(key: Key<*>): CoroutineContext
interface Element : CoroutineContext {
val key: Key<*>
}
interface Key<E : Element>
}
Un élément de la coroutine context est un context lui-même. C’est en réalité un context singleton avec cet élément uniquement. On pourra ainsi créer des contexts composite définis par potentiellement plusieurs bibliothèques et les combiner avec +.
Par exemple, une bibliothèque définit un élément auth contenant des informations d’autorisation de l’utilisateur et une autre bibliothèque définit un objet CommonPool comportant des informations d’exécution de context. On pourrait tout à fait appeler la coroutine builder launch() et combiner les contexts.
launch(auth + CommonPool) {
...
}
Suspending function et state machine
Une coroutine se doit d’être efficiente, donc de créer aussi peu de classes et d’objets que possible. Kotlin a lancé ses coroutines en implémentant des state machines.
Le compiler ne crée qu’une seule classe par suspending lambda même si celle-ci possède plusieurs points de suspension dans son body.
Une suspending function est compilée en une state machine dans laquelle les states correspondent aux points de suspension.
Imaginons une suspending function postItem qui ira chercher un token via un appel api et fera un autre appel http pour créer le post sur l’api.
Pour ne pas bloquer le thread UI, on utilisera donc des suspending function pour ces deux appels api.
Une méthode processPost permettra d’afficher le post nouvellement créé.
suspend postItem(item: Item) {
val token = requestToken() //suspending function
val post = createPost(token, item) //suspending function
processPost(post)
}
Kotlin assignera un label avant chaque point de suspension et le mettra à jour après exécution de chacun.
suspend postItem(item: Item) {
// LABEL 0
val token = requestToken()
// LABEL 1
val post = createPost(token, item)
//LABEL 2
processPost(post)
}
Conceptuellement, on obtient un switch.
Implicitement, une instance de Continuation est passée à notre suspending function postItem.
suspend postItem(item: Item, cont: Continuation) {
val sm = object: CoroutineImpl {}
switch (sm.label) {
case 0:
sm.item = item
sm.label = 1
requestToken(sm)
case 1:
sm.item = item
sm.token = result as Token
createPost(token, item, sm)
case 2:
sm.item = item
sm.post = result as Post
processPost(post)
}
}
Le label est donc à 0 à l’initialisation. La state machine ira stocker notre paramètre item et passera la state machine à chaque suspending function présente dans postItem.
result est un objet qui stockera le résultat de requestToken puis createPost sans créer d’autre objet intermédiaire.
Une fois tous les points de suspension passés, tous les autres appels sont exécutés directement dans le même case label.
Exemple d’implémentation d’autocompletion d’adresse
Android via Places.GeoDataApi permet de fournir l’autocompletion d’adresse selon une query grâce à l’appel à getAutocompletePredictions
Cette api fournit également de nombreuses informations sur un lieu grâce à getPlaceById
Admettons que je souhaite à l’autocomplétion retourner une adresse ainsi que sa latitude et longitude.
getAutocompletePredictions nous mettra à disposition des instances d’AutocompletePrediction. Ceux-ci nous permettent de récupérer une adresse texte ainsi qu’un placeId dont nous aurons besoin pour récupérer les coordonnées géographiques.
Pour éviter de bloquer le thread UI, Android a implémenté des callback pour obtenir les résultats de cette api.
En utilisant les coroutines, voici ce qu’on peut imaginer :
launch {
val predictionBuffer = Places.GeoDataApi.getAutocompletePredictions(googleApiClient, query, bounds, null)
predictionBuffer.setResultCallback {
it.forEach { prediction ->
val placeBuffer = Places.GeoDataApi.getPlaceById(googleApiClient, prediction.placeId)
val places = placeBuffer.setResultCallback { place ->
val address = AddressEventLV( prediction.getPrimaryText(null).toString(), prediction.getSecondaryText(null).toString(),
place.get(0).latLng
)
place.release()
addressList.add(address)
}
}
it.release()
}
}
Comme on peut le voir, le code n’est pas évident à lire. Nous avons besoin des prédictions qui fournissent le placeId pour faire l’appel à l’api getPlaceById. Nous ne sommes pas encore dans le callback hell mais ce n’est tout de même pas intuitif à lire.
Grâce aux extensions de classe Kotlin et aux types primitifs des coroutines de la librairie standard, nous pouvons utiliser la continuation passing style pour obtenir un style de code plus direct et plus aisé à lire.
private suspend fun PendingResult<AutocompletePredictionBuffer>.awaitPredictions(): AutocompletePredictionBuffer = suspendCoroutine {
cont-> setResultCallback { predictions -> cont.resume(predictions) }
}
private suspend fun PendingResult<PlaceBuffer>.awaitPlace(): PlaceBuffer = suspendCoroutine {
cont-> setResultCallback { places -> cont.resume(places) }
}
Nous allons étendre la classe PendingResult pour lui ajouter la méthode awaitPredictions qui sera une suspending function.
suspendCoroutine permet de récupérer l’instance Continuation courante dans la suspending function et suspend l’exécution de la coroutine courante.
Le code reste asynchrone mais il permet de wrapper des callback facilement.
Au final, nous obtenons un style de code plus direct.
L’exécution de la coroutine est suspendue à l’appel de notre méthode awaitPredictions. L’instance de Continuation nous passera les résultats de AutocompletePredictionBuffer.
On itère sur chaque AutocompletePrediction et faisons appel à awaitPlace sur PlaceBuffer.
Au final, on peuple chaque objet Address avec le texte autocomplété ainsi que les coordonnées géographiques.
launch {
val predictionBuffer = Places.GeoDataApi.getAutocompletePredictions(googleApiClient, query, bounds, null)
val predictions = predictionBuffer.awaitPredictions()
predictions.forEach {
val placeBuffer = Places.GeoDataApi.getPlaceById(googleApiClient, it.placeId)
val places = placeBuffer.awaitPlace()
val address = Address(
it.getPrimaryText(null).toString(),
it.getSecondaryText(null).toString(),
places.get(0).latLng
)
addressList.add(address)
places.release()
}
predictions.release()
}
Conclusion
Kotlin a souhaité donner de la flexibilité dans l’utilisation des coroutines afin de supporter de nombreuses API asynchrones.
Pour cette raison, le compiler est uniquement responsable du support des suspending function, des suspending lambdas ainsi que les suspending function type correspondantes. Il n’existe que quelques primitifs dans la bibliothèque standard. Le reste est implémenté dans les bibliothèques d’application.
Cet article reste une introduction. Il existe encore de nombreuses possibilités grâce aux Channel par exemple qui ne sont pas traités ici.
J’espère néanmoins vous avoir donné envie d’utiliser les coroutines Kotlin.
Vous pouvez retrouver d’autres article de Julien sur notre blog :
- Créer un CV en réalité augmentée avec ARCore
- La création d’un portail AR avec ARCore et Sceneform
- P.O.C. : association de la réalité augmentée et du
machine learning sur Android
De plus, nous publions régulièrement des articles sur des sujets de développement produit web et mobile, data et analytics, sécurité, cloud, hyperautomatisation et digital workplace.
Suivez-nous pour être notifié des prochains articles et réaliser votre veille professionnelle.
Retrouvez aussi nos publications et notre actualité via notre newsletter, ainsi que nos différents réseaux sociaux : LinkedIn, Twitter, Youtube, Twitch et Instagram
Vous souhaitez en savoir plus ? Consultez notre site web et nos offres d’emploi.