Flutter : Focus sur l’async et les isolates

Mono-Thread et Asynchrone

Dart est un langage mono thread qui offre la possibilité de faire de l’asynchrone. Pour comprendre comment tout ça marche on va jeter un oeil du côté des Future, des isolates et de l’event loop.

Quand vous aurez une application lancée, elle tournera dans ce qu’on appel un isolate. Un isolate est un espace qui possède sa propre mémoire et une boucle d’évènement qui sont exécutées par ordre d’arrivée. (FIFO)

Imaginons que dans cet isolate, on cherche à effectuer une tâche, par exemple une requête à un webservice, une écriture sur une base de donnée ou un parsing de json.
Ce genre d’action n’est pas immédiate, il y aura toujours une latence avant que celle-ci soit terminée vu son ampleure.
Pour l’exemple du webservice, le serveur appelé peut mettre quelques centaines de millisecondes à répondre, il faut donc prendre en compte le fait qu’on n’aura pas une réponse immédiate.
Pour prendre en compte cette problématique en Dart, on va faire de l’asynchrone à l’aide des mot-clés Future et Async/Await.

Future


C’est un type de variable qui est en attente d’une valeur. Elle sera soit la valeur retournée par notre webservice, soit une erreur, soit terminée.
Un Future est le résultat d’une opération asynchrone qui retourne 2 types de réponses, une valeur ou une erreur.
Dans les deux cas la future passera par le stade complete.

Dans potichien, lorsque l’on clique sur le bouton pour avoir une nouvelle image. L’évent loop récupère cette action et lance l’appel pour récupèrer l’url d’une nouvelle image auprès du webservice.
Pendant que cet appel est fait, l’event loop est disponible pour effectuer d’autres tâches, elle n’est pas bloquée.
Lorsque le webservice retourne sa donnée ou son erreur, le client http repasse dans la loop pour effectuer la mise à jour de la variable de type Future qui était en attente d’une valeur.

Parmi les librairies présentes dans Dart, plusieurs vous renvoient un Future quand vous y faites appel, on a donc le client http, les Shared Preferences, etc.

Pour simuler un appel webservice (ou une opération longue nécessitant de l’asynchrone) vous pouvez faire le code suivant :

final mockedWebserviceCall = Future.delayed(
    Duration(seconds: 10), 
    () =>'retour du webservice');

Lorsque votre appel est fini, le fonction then permet d’effectuer une autre fonction qui retournerai un Future :

final myChainedCalls = _callWebService()
    .then((value) => _saveItemsInLocalDataBase(value));

Dans le cas ou votre appel auprès de votre webservice vous renvoi une erreur, vous pouvez la gérer avec catchError :

final myChainedCalls = _callWebService()
    .then((value) => _saveItemsInLocalDataBase(value))
    .catchError((err) { print(err)) }
    .whenComplete((){ print('completed') });

Async Await

On a vu que pour effectuer une action à la réception du résultat d’une requête on pouvait utiliser la fonction then.
Il est possible d’utiliser une autre façon de faire, l’async await, cela permet d’avoir une meilleur lisibilité notamment quand il faut effectuer plusieurs opérations asynchrones d’affilé. Par exemple de cette façon :

Future getUserAdresse() async {
    final id = await _getUserId();
    final adresse = await _getUserAdresse(id);
    return address;
}

On place await devant chaque fonction qui retourne un Future.
Cette fonction est le même processus que les enchaînements d’appels avec l’API Future et then. Avec async/Await la fonction est cependant plus lisible.

Concernant la gestion des erreurs avec Async/Await on peut mettre en place un try catch.

Future getUserAdresse() async {
try {
    final id = await _getUserId();
    final adresse = await _getUserAdresse(id);
    return address;
    } on HttpException catch (err) {
        //Gestion cas erreur
    } finally {
        //Requête terminée
    }
}

Il est possible de faire ce type d’opération tout en restant dans le même isolate cependant on risque de rapidement attendre les limites de cet isolate qui a déjà tout une application à gérer.

Isolate

Pour limiter la surcharge du main isolate avec des opérations lourdes, on peut créer d’autres isolates.
Ce nouvel isolate aura sa propre mémoire et sa propre boucle d’évènements.
Attention pour lancer un nouvel isolate il vous faudra le faire depuis une fonction de haut niveau ou une fonction statique !

Il y a plusieurs façons de créer un isolate :
Si vous souhaitez créer et gérer entièrement à la main votre isolate ou bien tout simplement utiliser la fonction compute() qui prend en premier argument la fonction que vous voulez faire fonctionner dans cet isolate et en deuxième argument les paramètre de cette fonction.

Par exemple lors d’un parsing de json :

Future> fetchPhotos(http.Client client) async {
   final response =
       await client.get('https://jsonplaceholder.typicode.com/photos');
   return compute(parsePhotos, response.body);
 }

On va donc créer une méthode parsePhotos de haut niveau (qui ne sera donc pas dans une classe) et qui prendra en argument le body de la réponse de l’appel webservice.

List parsePhotos(String responseBody) {
   final parsed = jsonDecode(responseBody).cast>();
   return parsed.map((json) => Photo.fromJson(json)).toList();
 }

Un isolate peut retourner différents types de données, les built-in types (String, bool, int …) mais aussi une liste d’un type comme pour le résultat du parsing. Attentions aux objets que vous voulez retourner tous ne passerons pas, par exemple les Future. Limitez la complexité de l’objet à retourner ou tout devrait bien se passer 🙂