2. Asynchronisme en pratique

Nous allons aborder les principes de base de l’asynchronisme en programmation et voir comment les mettre en pratique dans des applications Flutter. Nous allons utiliser une API pour récupérer des images et des textes, et voir comment utiliser les widgets FutureBuilder et StreamBuilder pour afficher les résultats de manière asynchrone.

Afin de mettre en œuvre ce qui a été vu au cours précédent, nous allons utiliser l'API Random Duck. Cette API nous permettra de récupérer une image de canard ainsi qu’un texte. Pour cela, nous allons utiliser FutureBuilder et StreamBuilder pour voir comment utiliser ces deux widgets pour un même rendu visuel.

Api Random Duck

Dans ce cours, nous allons utiliser l’URL suivante : https://random-d.uk/api/quack. Cet appel nous renvoie une réponse sous le format JSON de ce type :

{
   "message":"Powered by random-d.uk",
   "url":"https://random-d.uk/api/168.jpg"
}

Commençons l’app

Commençons par créer la base de l’application.

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: DuckPage(),
    );
  }
}

class DuckPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Qwack !')),
      body: Image.network('https://random-d.uk/api/168.jpg'),
    );
  }
}

Image

Le widget Image de Flutter est utilisé pour afficher une image sur l’interface utilisateur, qu’elle soit une images locales, des images en ligne et même une images sous forme de sprites.

Ici nous avons besoin d’afficher une image d’après une URL. Pour cela il existe deux solutions Image.network et NetworkImage.

Image.network est un widget et donc permet notamment de donner plus de paramètres à l’image qu’il affiche comme on peut le voir dans son constructeur :

  Image.network(
    String src, {
    super.key,
    double scale = 1.0,
    this.frameBuilder,
    this.loadingBuilder,
    this.errorBuilder,
    this.semanticLabel,
    this.excludeFromSemantics = false,
    this.width,
    this.height,
    this.color,
    this.opacity,
    this.colorBlendMode,
    this.fit,
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.centerSlice,
    this.matchTextDirection = false,
    this.gaplessPlayback = false,
    this.filterQuality = FilterQuality.low,
    this.isAntiAlias = false,
    Map<String, String>? headers,
    int? cacheWidth,
    int? cacheHeight,
  }

Tandis que NetworkImage est beaucoup plus limité car c’est un objet :

    const factory NetworkImage(String url, { double scale, Map<String, String>? headers }) = network_image.NetworkImage;

NetworkImage est une classe qui permet de charger une image à partir d’une URL, tandis que Image.network est un constructeur du widget Image qui permet de charger une image à partir d’une URL et de personnaliser le rendu du widget.

Pour notre utilisation, Image.network sera parfait car on a besoin d’afficher directement un widget comme on le souhaite.

Bouton

Afin de rendre l’application interactive, nous allons ajouter un bouton qui permettra de lancer un nouvel appel à l’API afin d’afficher une nouvelle image de canard à chaque fois que l’utilisateur appuie dessus. Pour cela, nous allons ajouter un FloatingActionButton.

class DuckPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Qwack !')),
      body: Image.network('https://random-d.uk/api/168.jpg'),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Appel à l'API et mise à jour de l'image
        },
        child: Icon(Icons.refresh),
      ),
    );
  }
}

Dans le callback onPressed on y ajoutera la fonction qui permettera de mettre à jour l’image. Si aucun callback n’est donné à un bouton (null) il sera considéré comme désactivé, pensez y si vous voulez limiter son utilisation.

FutureBuilder

FutureBuilder est un widget Flutter qui permet de construire des widgets en fonction du résultat d’une tâche asynchrone. Il est souvent utilisé pour afficher des données qui nécessitent un temps de traitement supplémentaire, telles que les requêtes réseau, les accès à la base de données, ou toute autre tâche dont on sait qu’on aura pas le résultat immédiatement.

Nous allons maintenant implementer la méthode qui permet d’appeler l’API :

FutureBuilder<String?>(
                future: getNewDuck(),
                builder: (context, snapshot) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return const CircularProgressIndicator();
                  }

                  if (snapshot.data == null) {
                    return const Text('No data');
                  }

                  return Image.network(
                    snapshot.data as String,
                    height: MediaQuery.of(context).size.height * 0.3,
                    fit: BoxFit.fitHeight,
                  );
                }),
                
/// Et en dehors de la méthode Build
Future<String?> getNewDuck() async {
    final response =
        await http.get(Uri.parse('https://random-d.uk/api/random'));
    if (response.statusCode == 200) {
      final json = jsonDecode(response.body);
      return json['url'];
    }
    return null;
  }

Vous remarquerez ici que le type retourné est nullable, ici la logique mise en place est : si l’appel est un succès avec le code 200, on renvoi l’URL de l’image, sinon on renvoi null. Cette logique à été mise en place afin de coller avec la partie du code dans le builder du widget FutureBuilder qui indique qu’on accepte que l’objet retourné soit de type null.

StreamBuilder

Nous allons maintenant voir ce que la même fonctionnalité et rendu visuel donnera avec un StreamBuilder. Ce cas est particulierement utilse à connaître car les librairies de management d’état fonctionnent des fois avec des Streams.

Côté UI, on va simplement changer FutureBuilder en StreamBuilder et son attrobut future devient stream.

StreamBuilder<String?>(
  stream: getNewDuckStream(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }

    if (snapshot.data == null) {
      return const Text('No data');
    }

    return Image.network(
      snapshot.data as String,
      height: MediaQuery.of(context).size.height * 0.3,
      fit: BoxFit.fitHeight,
    );
  },
)

La fonction getNewDuck cepandant changera. On va crééer une fonction getNewDuckStream afin de guarder de côté celle avec renvoit un future.

Stream<String?> getNewDuckStream() async* {
    final response =
        await http.get(Uri.parse('https://random-d.uk/api/random'));
    if (response.statusCode == 200) {
      final json = jsonDecode(response.body);
      yield json['url'];
    } else {
      yield null;
    }
}

async* est une syntaxe pour les générateurs asynchrones dans Dart. Les générateurs asynchrones permettent de produire des valeurs sur une période de temps plus longue. Chaque appel à la fonction yield permet de renvoyer une valeur immédiate, tout en conservant l’état de la fonction pour la prochaine itération.

Lorsqu’une fonction est déclarée avec async*, elle peut utiliser await pour gérer des tâches asynchrones et yield pour produire des valeurs dans un Stream. La fonction retourne un objet Stream qui peut être utilisé pour s’abonner aux valeurs produites par la fonction.

Stream<String?> getNewDuckStreamEverySecond() async* {
    while (true) {
      final response =
      await http.get(Uri.parse('https://random-d.uk/api/random'));
      if (response.statusCode == 200) {
        final json = jsonDecode(response.body);
        yield json['url'] as String;
      } else {
        yield null;
      }
      await Future.delayed(Duration(seconds: 1));
    }
  }

Dans cette version du code, getNewDuckStreamEverySecond renvoi une URL de canard toutes les secondes.

En résumé, async* et yield sont des moyens puissants de produire et de consommer des valeurs asynchrones dans Dart. Ils peuvent être utilisés pour simplifier la gestion de la logique asynchrone dans une application.

Future, Stream, ces connaissances sont indispensables pour développer des applications performantes et réactives. Si vous souhaitez approfondir vos compétences, je vous encourage à essayer de réaliser les défis ci-dessous proposés pour mettre en pratique ce que vous avez appris.

Défis

Si vous souhaitez tester vos competences avec ce qu’on a vu dans cette leçon, voici quelques pistes …

  • Refaites un projet similaire avec une autre api par exemple avec DogApi
  • Cherchez comment rendre le widget Image clickable afin que l’action effectuée avec le bouton soit fait directement d’un tap sur l’image.
  • Basé sur l’idée précédente, mettez en place une mise à jour des images de canard de façon automatique (toutes les 5 secondes par exemple), sauf que l’intéraction que vous avez mis en place sur l’image permet de stopper cette mise à jour automatique.
  • Essayez de mettre en place des bords arrondis sur l’image.