15. Asynchronisme 2/2

Lundi 15/1/24
916 Mots
5 Minutes

2. Asynchronisme : la pratique

// Rajouter un paragraphe sur la librairie http

Dart version 3.3.7

Afin de mettre en pratique ce que nous avons vu précédemment, nous allons utiliser l'API Random Duck. Cette API va nous permettre de récupérer une image de canard ainsi qu'un texte. Nous allons utiliser FutureBuilder et StreamBuilder pour voir comment utiliser ces deux widgets pour un même rendu visuel.

API Random Duck

Nous allons utiliser l'URL suivante : https://random-d.uk/api/quack. Cet appel nous renvoie une réponse au format JSON de ce type :

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

Création de l'application

Commençons par créer la base de l'application :

dart
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'),
    );
  }
}

Afficher une image

Le widget Image de Flutter est utilisé pour afficher des images, qu'elles soient locales, en ligne ou même sous forme de sprites. Ici, nous avons besoin d'afficher une image d'après une URL. Il existe deux solutions : Image.network et NetworkImage.

Image.network est un widget qui permet de spécifier de nombreux paramètres pour personnaliser le rendu de l'image, comme le montre son constructeur :

dart
 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,
  }

De son côté, NetworkImage est un objet plus limité :

dart
  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 personnaliser le rendu de l'image et d'ajuster son comportement.

Pour notre utilisation, Image.network est parfait car nous avons besoin d'afficher directement un widget personnalisé.

Ajouter un bouton

Pour 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.

dart
body: Column(
        children: [
          Image.network('https://random-d.uk/api/168.jpg'),
          FilledButton(onPressed: (){}, child: Text('Qwack !'))
        ],
      ),

Dans le callback onPressed, nous ajouterons la fonction qui mettra à jour l'image. Si aucun callback n'est fourni, le bouton sera désactivé. Pensez à cela si vous voulez limiter l’utilisation du bouton dans certains cas.

FutureBuilder

FutureBuilder est un widget Flutter qui permet de construire dynamiquement des widgets en fonction du résultat d'une tâche asynchrone. Il est souvent utilisé pour des tâches nécessitant un temps de traitement supplémentaire, telles que les requêtes réseau ou l'accès à une base de données.

Voici un exemple avec notre appel à l'API :

dart
FutureBuilder<http.Response>(
  future: http.get('https://random-d.uk/api/168.jpg'),
  builder: (BuildContext context, AsyncSnapshot<http.Response> snapshot) {
    /// Gestion des différents états : chargement, erreur, résultat
  },
)

Le builder est rappelé à chaque changement d'état du Future : au début (chargement), puis à la fin (succès ou échec). Voici comment gérer ces différents cas :

dart
FutureBuilder<http.Response>(
  future: http.get('https://random-d.uk/api/168.jpg'),
  builder: (BuildContext context, AsyncSnapshot<http.Response> snapshot) {
    /// L'appel à l'API est en cours
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }
    
    /// Si l'appel échoue
    if (snapshot.data == null) {
      return const Text('Erreur');
    }
    
    /// Si l'appel est un succès
    return const Text(snapshot.data!.body);
  },
)

Il est souvent préférable de séparer la logique d'appel à l'API de la construction de l'interface. Voici comment simplifier l'exemple précédent :

dart
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!,
      height: MediaQuery.of(context).size.height * 0.3,
      fit: BoxFit.fitHeight,
    );
  },
)

/// 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;
}

Ici, le type retourné est nullable. En cas de succès, l'URL de l'image est renvoyée, sinon null. Cela correspond à la logique gérée dans le FutureBuilder.

StreamBuilder

Contrairement au FutureBuilder qui gère des tâches asynchrones uniques, StreamBuilder est utilisé pour des flux de données continus. Cela le rend particulièrement utile dans des cas où les données sont mises à jour en temps réel.

Voici un exemple avec le StreamBuilder, qui fonctionne de manière similaire au FutureBuilder mais avec un stream à la place du future :

dart
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!,
      height: MediaQuery.of(context).size.height * 0.3,
      fit: BoxFit.fitHeight,
    );
  },
)

Voici la fonction getNewDuckStream qui renvoie un Stream :

dart
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;
  }
}

Dart propose la syntaxe async* pour créer des générateurs asynchrones, permettant d’émettre des valeurs dans un Stream au fil du temps, sans que la fonction ne se termine.

Voici une version améliorée qui renvoie une nouvelle URL de canard toutes les secondes :

dart
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'];
    } else {
      yield null;
    }
    await Future.delayed(Duration(seconds: 1));
  }
}

Défis

Pour tester vos compétences, voici quelques défis à relever :

  1. Recréez un projet similaire avec une autre API, comme Dog API.
  2. Rendre l'image clickable pour remplacer le bouton.
  3. Mettez en place une mise à jour automatique des images toutes les 5 secondes, avec une interaction sur l'image pour arrêter la mise à jour.
  4. Ajoutez des bordures arrondies à l'image.