Deferred et awaitAll : comment paralléliser vos coroutines
L’utilisation des coroutines est un standard pour gérer les tâches concurrentielles sur Android. Elles simplifient grandement le code exécuté de manière asynchrone et sa complexité.
Cela dit, les développeurs n’ont pas toujours connaissance des notions plus poussées permettant d’accélérer le traitement. Et c’est normal. Voici pourquoi.
En Clean Architecture, on a facilement ce type de découpe lorsqu’on a un écran “simple” :
- 1 écran simple utilise 1 ViewModel
- 1 ViewModel utilise 1 UseCase
- 1 UseCase utilise 1 Repository
- 1 Repository utilise 1 DataSource
Grosso modo, il n’est pas toujours nécessaire d’avoir du parallélisme. 👌
Par contre, le principe d’un Usecase est aussi de pouvoir être réutilisables, notamment pour des écrans plus chargés comme des dashboards. On peut donc mettre en place un UseCase d’agrégation utilisant d’autres UseCases :
Dans ce cas là, s’il n’y a pas d’adhérences entre ces UseCases, le parallélisme devient pertinent. Let’s go! ✨
async/await
Le pattern async/await
de la librairie kotlinx.coroutines.core
permet de ne pas attendre la fin de la réponse et de continuer le traitement.
Dans l’exemple ci-dessous, les 3 UseCases s’exécutent en parallèle. Ils sont lancés dans la lambda async
qui retourne un Deferred
typé par le retour du UseCase présent à l’intérieur.
coroutineScope.launch {
// Not blocking calls
val alphaDeferred: Deferred<Long> = async { usecaseAlpha() }
val betaDeferred: Deferred<Long> = async { usecaseBeta() }
val gammaDeferred: Deferred<Long> = async { usecaseGamma() }
// Doing other stuff
delay(1000)
// Waiting for the end
val alpha: Long = alphaDeferred.await()
val beta: Long = betaDeferred.await()
val gamma: Long = gammaDeferred.await()
}
Il est possible de continuer à faire un traitement tant que la fin d’un Deferred
n’est implicitement appelé avec await()
(ou join()
).
awaitAll
Au lieu d’appeler await()
sur chaque Deferred
, la méthode awaitAll()
est aussi disponible sur une collection de Deferred
. Pour cela, rien de plus simple que de lancer les async
directement dans une liste :
coroutineScope.launch {
// Not blocking calls
val deferreds: Deferred<Long> = listOf(
async { usecaseAlpha() },
async { usecaseBeta() },
async { usecaseGamma() }
)
// Doing other stuff
delay(1000)
val complet = deferreds.awaitAll()
// Waiting for the end
val alpha: Long = complet[0]
val beta: Long = complet[1]
val gamma: Long = complet[2]
}
Pratique dans le cas où tous les retours sont du même type. Un peu moins si ce n’est pas le cas.
Au cas échant, il faut caster manuellement les items de la liste. Et même si les appels se font en parallèle, parfois l’un avant l’autre, les items de la liste finale ici nommée complet
sont dans le même ordre que la liste initiale deferreds
.
💡 Ces outils sont simples mais pas assez souvent utilisés. Si vous êtes développeurs/développeuse, pensez y quand vous enchainerez des méthodes suspend
un peu automatique. Et partagez ce type d’information à vos collègues. ;)
- Twitter : @bapness
- YouTube : @baptistemobiledev