Aujourd’hui il existe plusieurs façons de faire de la programmation asynchrone. En Javascript par le biais de Promises, en Java par les Future, en C# et typescript par les async/await, ou encore via l’utilisation de librairies de programmation réactive : RxJava, RxJs, RxSwift, RxKotlin etc…

J’ai pris l’habitude de développer l’ensemble de mes nouveaux projets en suivant les principes de la programmation réactive :

  1. Android – RxJava & RxKotlin
  2. Angular 5 – RxJs
  3. Spring boot – Webflux ( Reactor )
  4. iOS – RxSwift

Je n’avais jusqu’ici jamais pris le temps de m’intéresser aux coroutines, alors que ce concept commence à se démocratiser. Dernièrement j’ai découvert que plusieurs languages disposaient déjà de l’async-await comme Typescript ou encore Dart.

Dernièrement j’ai assisté aux Android Makers 2018, parmi l’ensemble des conférences il y avait celle de Erik Hellman “Better asynchronous programming with Coroutines”. Suite à cela, j’ai souhaité approfondir et vous en expliquer les concepts rapidement.

Tout d’abord back to basis, qu’est-ce qu’une coroutine ?

Dans un programme, une coroutine est une unité de traitement qui s’apparente à une routine, à ceci près que, lorsque la sortie d’une routine met fin à la routine, la sortie de la coroutine peut être le résultat d’une suspension de son traitement jusqu’à ce qu’il lui soit signalé de reprendre son cours. La suspension de la coroutine et sa reprise peuvent s’accompagner d’une transmission de données.

Les coroutines permettent de réaliser des traitements basés sur des algorithmes coopératifs comme les itérateurs, les générateurs, des canaux de communication, etc.

source  : https://fr.wikipedia.org/wiki/Coroutine

C’est pas plus clair ? Pas grâve ça va le devenir et nous allons retenir pour l’instant les points suivants :

  1. unité de traitement
  2. suspension de traitement
  3. transmission de données

Depuis la version 1.1, les coroutines sont disponibles sur Kotlin en version expérimentale. L’implémentation par défaut met à disposition un ensemble de classes abstraites et d’interfaces mais aucune implémentation. Afin de faciliter l’usage des modules complémentaires sont disponibles : kotlinx.coroutines. Ils intègrent un ensemble d’implémentation prêts à l’emploi.

Les coroutines sont une approche différente dans l’écriture de code asynchrone ; contrairement à de la programmation réactive qui manipule un flux, les coroutines favorisent un type séquentiel de programmation asynchrone. Elles peuvent donc exécuter plusieurs traitements en parallèle et le code reste quant à lui séquentiel et donc plus facile à lire.

Comment ça fonctionne ?

Afin d’utiliser les coroutines, un opérateur “suspend”  a été créé dans le langage afin de mettre en suspension une méthode ou une lambda. Après leurs exécutions dans un CoroutineScope, elles peuvent être arrêtées à tout moment . Exemple de suspend méthode et lambda :

// fonction suspendue
suspend fun maMethode(): String { 
   // ...
   return value
}

suspend () -> {
   // ...
}

La syntaxe change très peu d’un code kotlin classique, en effet seul l’opérateur “suspend” est ajouté.

Comment créer une coroutine ? Comment utiliser mes fonctions suspendues ?

Pour cela il faut utiliser les coroutines builders, ce sont des fonctions qui prennent en paramètre une lambda suspendue afin d’en créer une coroutine. Il existe une multitude de builder, les plus connus étant les suivants :

  1. async(…)
  2. launch(…)
  3. buildSequence(…)

Note : ses fonctions retournent un objet de type Job ou Deferred que nous expliquerons un peu plus bas.

Exemple d’utilisation de la fonction async :

val asyncTask = async {
    // long traitement
    delay(2, TimeUnit.SECONDS)
    // retourner une valeur
    return 1
}

// paramètres de la fonction async 
public actual fun <T> async(
    context: CoroutineContext = DefaultDispatcher, 
    start: CoroutineStart = CoroutineStart.DEFAULT, 
    parent: Job? = null, 
    block: suspend CoroutineScope.() -> T 
): Deferred<T>

Constatons que la fonction async prend un paramètre context de type CoroutineContext à quoi cela sert-il ?

CoroutineContext –  Lancer une coroutine dans un thread particulier

Le contexte d’une coroutine est un dispatcher indiquant dans quel contexte le traitement sera effectué. Si un traitement est bloquant, il sera préférable d’utiliser un contexte asynchrone tandis que pour rafraîchir une interface graphique un contexte avec le handler main looper de l’application Android.

Exemple d’utilisation :

// utiliser la fonction async
val asyncTask = async(context = CommonPool) {
    delay(1, TimeUnit.SECONDS)
    return 1
}

// ou encore
val asyncTask = async(CommonPool) {
    delay(1, TimeUnit.SECONDS)
    return 1
}

Pour les habitués de Rx cela s’apparente au Scheduler et l’utilisation de la méthode subscribeOn/observeOn

Dans les modules installés plusieurs CoroutineContext sont disponibles :

  1. UI
  2. CommonPool

Mais il est possible d’en créer au sein de votre projet de la façon suivante :

val customContext = newFixedThreadPoolContext(10, "MyCustomContext")

fun loadData() {
    launch(UI) {
        val task = async(customContext) {
            // ...
        }
        // ...
    }
}

Quelle est la différence entre Job / Deferred ?

Job est la classe la plus basse, elle indique l’état de son cycle de vie ( actif, annulé, terminé ), des dépendances à des jobs enfants ( imbrication de coroutine ), ainsi que toutes les méthodes pour démarrer, interrompre, et propager une erreur.

Deferred quant à elle hérite de la classe Job de ses fonctionnalités. Elle a la particularité de pouvoir retourner une valeur différée par le biais de la fonction await().

Exemple :

suspend fun loadNames(): List<String> {
    delay(2, TimeUnit.SECONDS)
    return listOf("mehdi", "slimani")
}

fun main() {
    val job: Job = launch(UI) {
        val task: Deferred<List<String>> = async(CommonPool) {
            loadNames().map { "Name : $it" }
        }
            
        val names = task.await()
        refreshNames(names)
    }
    // annuler au besoin
    // job.cancel()
}

Les actors

Un acteur est une fonction permettant d’englober une coroutine. Un système de mailbox permet de communiquer avec celui-ci par le biais de messages. Un message est traité ou non selon la méthode d’envoi et l’ensemble fonctionne de façon ordonné.

Parmi les méthodes d’envoi on retrouve :

  • offer(…) – envoi en asynchrone ; le message sera ignoré si la coroutine est occupée
  • send(…) – envoi en asynchrone ; le message ne sera jamais ignoré même si la coroutine est occupée

Note : La classe SendChannel<T> retournée par la fonction actor<T> est typée par le type du message. En effet si l’actor est typé par String alors le message envoyé de type String

val actor: Actor<String> {
    channel.map(CommonPool) {
        // traitement paramétré par $it 
    }.consumeEach {
        // résultat du traitement dans $it
    }
}

// envoyer un message
actor.send("test")
actor.offer("test")

Afin de traiter le message, un channel est mis à disposition dans l’acteur depuis un ActorScope ( lambda suspendue). Le channel contient de multiples fonctions de transformation et permet de traiter votre message. Les différentes fonctions ressemblent à ce que vous avez l’habitude de trouver sur de la programmation réactive et en voici un exemple :

  • map
  • groupBy
  • distinct
  • toList
  • toSet
  • maxWith
  • minWith
  • reduce
  • zip
  • consume
  • etc…

Exemple d’utilisation :

private val job = Job()

suspend fun loadNames(): List<String> {
    delay(2, TimeUnit.SECONDS)
    return listOf("mehdi", "slimani")
}

val actorLoadNames = actor<Nothing>(context = UI, parent = job) {
    channel.map(CommonPool) {
        loadNames()
    }.consumeEach { 
        refreshUI(it)
    }
}

fun refreshUI(names: List<String>) {
    // ...
}

Si vous êtes intéressés par la programmation orientée acteurs je vous conseille de lire cet article de OCTO Technology.

Vous souhaitez vous lancer ?

Il ne reste plus qu’à configurer votre projet

Depuis la DSL kotlin, activez les coroutines et ajoutez les dépendances kotlinx.coroutines

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'


// activer les coroutines dans le plugin kotlin
kotlin {
    experimental {
        coroutines "enable"
    }
}

android {
    // ...
}

dependencies {
    
    // coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.5"

    // ... 
}

Attention les coroutines sont encore en version expérimentale

Maintenant il n’y a plus qu’à ! Si vous souhaitez voir du code plus concret voici un projet d’exemple sur notre github et n’hésitez pas à laisser des commentaires au besoin.