Deferred et awaitAll : comment paralléliser vos coroutines

Baptiste Carlier
3 min readFeb 5, 2023

--

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
- LinkedIn

--

--