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 :
{
"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 :
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 :
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é :
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.
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 :
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 :
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 :
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
:
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
:
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 :
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 :
- Recréez un projet similaire avec une autre API, comme Dog API.
- Rendre l'image clickable pour remplacer le bouton.
- 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.
- Ajoutez des bordures arrondies à l'image.