Salut ! Aujourd'hui, on va plonger dans l'un des aspects les plus puissants de Dart : la programmation asynchrone. Si tu développes des apps mobiles avec Flutter, tu vas constamment faire des appels API, lire des fichiers, ou attendre des interactions utilisateur. Tout ça, c'est asynchrone !
La bonne nouvelle ? Dart rend l'asynchrone vraiment élégant avec async/await, Future et Stream. Mais pour bien les utiliser, il faut comprendre comment ils fonctionnent sous le capot.
Dans cet article, je vais t'expliquer tout ça avec des exemples concrets et des cas d'usage réels. À la fin, tu sauras exactement quand utiliser Future, quand utiliser Stream, et comment éviter les pièges classiques.
Prêt à maîtriser l'asynchrone ? Let's go !
C'est quoi la programmation asynchrone ?
Avant de plonger dans le code, comprends bien le concept. Imagine que tu commandes un café :
Approche synchrone (bloquante)
Tu arrives au comptoir, tu commandes, et tu attends là sans rien faire jusqu'à ce que ton café soit prêt. Personne d'autre ne peut commander pendant ce temps. C'est bloquant.
Approche asynchrone (non-bloquante)
Tu commandes, on te donne un numéro, et tu peux aller t'asseoir, consulter ton téléphone, parler avec des amis. Quand ton café est prêt, on t'appelle. Pendant ce temps, d'autres personnes peuvent commander. C'est non-bloquant.
En programmation, c'est pareil. Les opérations asynchrones permettent à ton app de rester réactive pendant qu'elle attend des données du réseau, lit un fichier, ou exécute une opération longue.
Le modèle de concurrence de Dart : Event Loop
Dart utilise un modèle à un seul thread avec une event loop (boucle d'événements). C'est crucial à comprendre.
Comment ça marche ?
Imagine Dart comme un serveur de restaurant avec un seul serveur (le thread principal) :
-
La file d'attente : Les tâches attendent leur tour
-
Le serveur : Traite une tâche à la fois
-
La cuisine : Les opérations asynchrones se font "ailleurs"
-
Le callback : Quand c'est prêt, ça revient dans la file
Concrètement :
void main() {
print('1. Début du programme');
Future.delayed(Duration(seconds: 2), () {
print('3. Après 2 secondes');
});
print('2. Fin du main (mais pas du programme)');
}
// Output:
// 1. Début du programme
// 2. Fin du main (mais pas du programme)
// 3. Après 2 secondes
Pourquoi cet ordre ? Parce que Future.delayed ne bloque pas. Le code continue, et quand les 2 secondes sont écoulées, le callback est ajouté à la file d'attente et exécuté.
Future : une valeur qui arrivera... dans le futur
Un Future représente une opération qui va se terminer dans le futur avec soit une valeur, soit une erreur. C'est comme une promesse (Promise en JavaScript).
Créer un Future simple
Imaginons qu'on simule un appel API qui prend 2 secondes :
Future<String> recupererUtilisateur() {
return Future.delayed(
Duration(seconds: 2),
() => 'Alice',
);
}
Qu'est-ce qui se passe ici ?
-
On crée un
Futurequi retournera unString -
Future.delayedsimule un délai (comme un appel réseau) -
Après 2 secondes, il retourne la chaîne
'Alice'
Consommer un Future avec then()
La méthode classique pour récupérer le résultat :
void exemple1() {
print('Début de la requête');
recupererUtilisateur().then((nom) {
print('Utilisateur récupéré: $nom');
});
print('Requête lancée (mais pas terminée)');
}
// Output:
// Début de la requête
// Requête lancée (mais pas terminée)
// Utilisateur récupéré: Alice (après 2 secondes)
Le problème avec then() ? Si tu enchaînes plusieurs opérations, ça devient vite le "callback hell" :
// Difficile à lire 😫
recupererUtilisateur().then((nom) {
return recupererPosts(nom);
}).then((posts) {
return traiterPosts(posts);
}).then((resultat) {
print(resultat);
}).catchError((erreur) {
print('Erreur: $erreur');
});
async/await : la syntaxe moderne
async/await rend le code asynchrone aussi lisible que du code synchrone :
Future<void> exemple2() async {
print('Début de la requête');
String nom = await recupererUtilisateur();
print('Utilisateur récupéré: $nom');
print('Fin');
}
C'est beaucoup plus clair ! Voici ce qui se passe :
-
Le mot-clé
asyncindique que la fonction contient du code asynchrone -
awaitmet en pause l'exécution de la fonction jusqu'à ce que le Future soit complété -
Une fois complété, le résultat est assigné à
nom -
Le code continue normalement
Important : await ne bloque pas toute l'app, seulement cette fonction. L'event loop continue de tourner et d'autres opérations peuvent s'exécuter.
Gestion des erreurs
Avec then(), on utilise catchError(). Avec async/await, on utilise try/catch :
Future<void> exempleAvecErreurs() async {
try {
String nom = await recupererUtilisateur();
print('Utilisateur: $nom');
List posts = await recupererPosts(nom);
print('Posts récupérés: ${posts.length}');
} catch (e) {
print('Erreur survenue: $e');
} finally {
print('Nettoyage (toujours exécuté)');
}
}
Le bloc finally est super utile pour fermer des connexions, libérer des ressources, ou cacher un loader dans une UI.
Exemple concret : Appel API
Créons un exemple réaliste d'appel API avec gestion complète :
import 'dart:convert';
import 'package:http/http.dart' as http;
// Modèle de données
class Utilisateur {
final int id;
final String nom;
final String email;
Utilisateur({
required this.id,
required this.nom,
required this.email,
});
factory Utilisateur.fromJson(Map<String, dynamic> json) {
return Utilisateur(
id: json['id'],
nom: json['name'],
email: json['email'],
);
}
}
// Service API
class ApiService {
static const String baseUrl = 'https://jsonplaceholder.typicode.com';
// Récupérer un utilisateur
Future<Utilisateur> recupererUtilisateur(int id) async {
try {
// Faire l'appel HTTP
final response = await http.get(
Uri.parse('$baseUrl/users/$id'),
);
// Vérifier le statut
if (response.statusCode == 200) {
// Décoder le JSON
final json = jsonDecode(response.body);
return Utilisateur.fromJson(json);
} else {
throw Exception('Erreur ${response.statusCode}');
}
} catch (e) {
// Gérer les erreurs réseau
throw Exception('Impossible de récupérer l\'utilisateur: $e');
}
}
// Récupérer plusieurs utilisateurs
Future<List<Utilisateur>> recupererTousLesUtilisateurs() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/users'),
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body);
// Convertir chaque élément JSON en Utilisateur
return jsonList.map((json) => Utilisateur.fromJson(json)).toList();
} else {
throw Exception('Erreur ${response.statusCode}');
}
} catch (e) {
throw Exception('Impossible de récupérer les utilisateurs: $e');
}
}
}
// Utilisation
void main() async {
final api = ApiService();
try {
print('Récupération d\'un utilisateur...');
final utilisateur = await api.recupererUtilisateur(1);
print('✓ Utilisateur: ${utilisateur.nom} (${utilisateur.email})');
print('\nRécupération de tous les utilisateurs...');
final utilisateurs = await api.recupererTousLesUtilisateurs();
print('✓ ${utilisateurs.length} utilisateurs récupérés');
} catch (e) {
print('✗ Erreur: $e');
}
}
Dans cet exemple, remarque plusieurs bonnes pratiques :
-
Séparation des responsabilités : Le modèle
Utilisateuret le serviceApiServicesont séparés -
Gestion d'erreurs : On vérifie le statut HTTP et on catch les erreurs réseau
-
async/await : Code lisible et séquentiel
-
Type safety :
Future<Utilisateur>garantit qu'on retourne bien un utilisateur
Opérations parallèles avec Future.wait
Si tu dois faire plusieurs appels API indépendants, tu peux les exécuter en parallèle pour gagner du temps :
Future<void> chargerDonneesSequentiellement() async {
print('Chargement séquentiel...');
final debut = DateTime.now();
// Chaque await attend que l'opération précédente soit terminée
final user = await recupererUtilisateur(1);
final posts = await recupererPosts(1);
final comments = await recupererComments(1);
final duree = DateTime.now().difference(debut);
print('Terminé en ${duree.inMilliseconds}ms');
// Si chaque appel prend 1s, total = ~3s
}
Future<void> chargerDonneesParallele() async {
print('Chargement parallèle...');
final debut = DateTime.now();
// Future.wait lance toutes les opérations en même temps
final resultats = await Future.wait([
recupererUtilisateur(1),
recupererPosts(1),
recupererComments(1),
]);
final user = resultats[0];
final posts = resultats[1];
final comments = resultats[2];
final duree = DateTime.now().difference(debut);
print('Terminé en ${duree.inMilliseconds}ms');
// Si chaque appel prend 1s, total = ~1s (3x plus rapide!)
}
La différence est énorme ! Mais attention : utilise Future.wait seulement quand les opérations sont indépendantes. Si recupererPosts() a besoin du résultat de recupererUtilisateur(), tu dois les faire séquentiellement.
Gérer les échecs avec Future.wait
Par défaut, si une des opérations échoue, tout échoue. Pour gérer ça finement, utilise eagerError: false :
Future<void> chargerAvecGestionEchecs() async {
final futures = [
recupererUtilisateur(1),
recupererUtilisateur(999), // N'existe pas
recupererUtilisateur(2),
];
try {
final resultats = await Future.wait(
futures,
eagerError: false, // Continue même si certaines échouent
);
print('Résultats: $resultats');
} catch (e) {
print('Au moins une erreur: $e');
}
}
Ou encore mieux, utilise Future.wait avec gestion individuelle :
Future<void> chargerAvecGestionFine() async {
final futures = [
recupererUtilisateur(1).catchError((e) => null),
recupererUtilisateur(999).catchError((e) => null),
recupererUtilisateur(2).catchError((e) => null),
];
final resultats = await Future.wait(futures);
final utilisateursValides = resultats
.where((user) => user != null)
.toList();
print('${utilisateursValides.length} utilisateurs récupérés');
}
Stream : un flux continu de données
Si Future est une promesse d'une valeur, Stream est un flux de plusieurs valeurs dans le temps. C'est comme une rivière : les données coulent continuellement.
Quand utiliser Stream ?
Utilise Stream quand tu as :
-
Des événements répétés : clics utilisateur, mouvements de souris
-
Des données en temps réel : WebSocket, Firebase Realtime Database
-
Des flux de données : lecture de fichier ligne par ligne
-
Des mises à jour progressives : progression d'upload/download
Créer un Stream simple
Imaginons un compteur qui émet une valeur chaque seconde :
Stream<int> compteur(int max) async* {
for (int i = 1; i <= max; i++) {
// Simuler un délai
await Future.delayed(Duration(seconds: 1));
// yield émet une valeur dans le stream
yield i;
}
}
Le mot-clé async* indique qu'on crée un Stream générateur. yield émet chaque valeur une par une.
Écouter un Stream
Il y a deux façons principales d'écouter un Stream :
1. Avec await for (dans une fonction async)
Future<void> ecouterCompteur() async {
print('Début du compteur');
// await for attend chaque valeur
await for (var valeur in compteur(5)) {
print('Reçu: $valeur');
}
print('Compteur terminé');
}
// Output (avec 1s entre chaque):
// Début du compteur
// Reçu: 1
// Reçu: 2
// Reçu: 3
// Reçu: 4
// Reçu: 5
// Compteur terminé
2. Avec listen() (sans async)
void ecouterCompteurAvecListen() {
print('Début du compteur');
final subscription = compteur(5).listen(
(valeur) {
print('Reçu: $valeur');
},
onError: (erreur) {
print('Erreur: $erreur');
},
onDone: () {
print('Compteur terminé');
},
);
// Optionnel : annuler l'écoute
// subscription.cancel();
}
La différence ? await for est bloquant (attend chaque valeur), listen() est non-bloquant (définit des callbacks).
Exemple concret : Monitoring en temps réel
Créons un exemple de monitoring de température qui émet des valeurs régulièrement :
// Simuler un capteur de température
Stream<double> capteurTemperature() async* {
// Générateur de nombres aléatoires
final random = Random();
double temperatureBase = 20.0;
while (true) {
// Attendre 2 secondes entre chaque lecture
await Future.delayed(Duration(seconds: 2));
// Varier la température de -2 à +2 degrés
final variation = (random.nextDouble() - 0.5) * 4;
temperatureBase += variation;
// Garder entre 15 et 30 degrés
temperatureBase = temperatureBase.clamp(15.0, 30.0);
yield temperatureBase;
}
}
// Monitorer la température
class MoniteurTemperature {
StreamSubscription<double>? _subscription;
double? _derniereTemperature;
void demarrer() {
print('🌡️ Démarrage du monitoring...');
_subscription = capteurTemperature().listen(
(temperature) {
_derniereTemperature = temperature;
_analyserTemperature(temperature);
},
onError: (erreur) {
print('❌ Erreur capteur: $erreur');
},
);
}
void _analyserTemperature(double temp) {
print('📊 Température actuelle: ${temp.toStringAsFixed(1)}°C');
if (temp < 18) {
print('⚠️ Alerte: Température trop basse!');
} else if (temp > 26) {
print('⚠️ Alerte: Température trop haute!');
} else {
print('✅ Température normale');
}
}
void arreter() {
_subscription?.cancel();
print('🛑 Monitoring arrêté');
}
double? get temperatureActuelle => _derniereTemperature;
}
// Utilisation
void main() async {
final moniteur = MoniteurTemperature();
// Démarrer le monitoring
moniteur.demarrer();
// Laisser tourner 10 secondes
await Future.delayed(Duration(seconds: 10));
// Arrêter
moniteur.arreter();
print('Dernière température: ${moniteur.temperatureActuelle}°C');
}
Ce code montre plusieurs concepts importants :
-
Stream infini : Le
while(true)crée un stream qui ne s'arrête jamais -
StreamSubscription : Permet de contrôler l'écoute (pause, resume, cancel)
-
Gestion d'état : On stocke la dernière température
-
Analyse en temps réel : Chaque valeur déclenche une analyse
Transformer les Streams
Dart offre des opérateurs puissants pour transformer les streams :
Stream<int> nombres() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(Duration(milliseconds: 500));
yield i;
}
}
Future<void> exempleTransformations() async {
// map : transformer chaque valeur
await for (var carre in nombres().map((n) => n * n)) {
print('Carré: $carre');
}
// where : filtrer les valeurs
await for (var pair in nombres().where((n) => n % 2 == 0)) {
print('Pair: $pair');
}
// take : prendre seulement les N premiers
await for (var n in nombres().take(3)) {
print('Premier: $n');
}
// skip : sauter les N premiers
await for (var n in nombres().skip(5)) {
print('Après 5: $n');
}
// distinct : éliminer les doublons
final avecDoublons = Stream.fromIterable([1, 2, 2, 3, 3, 3, 4]);
await for (var unique in avecDoublons.distinct()) {
print('Unique: $unique');
}
}
Opérateurs de combinaison
// Combiner plusieurs streams
Stream<String> streamA() async* {
yield 'A1';
await Future.delayed(Duration(seconds: 1));
yield 'A2';
}
Stream<String> streamB() async* {
await Future.delayed(Duration(milliseconds: 500));
yield 'B1';
await Future.delayed(Duration(seconds: 1));
yield 'B2';
}
Future<void> combinerStreams() async {
// Fusionner deux streams
final combine = StreamGroup.merge([streamA(), streamB()]);
await for (var valeur in combine) {
print('Reçu: $valeur');
}
// Output: B1, A1, B2, A2 (ordre d'arrivée)
}
StreamController : Créer des Streams personnalisés
Pour des cas plus complexes, utilise StreamController :
class GestionnaireMessages {
// Créer un controller
final _controller = StreamController<String>();
// Exposer le stream (lecture seule)
Stream<String> get messages => _controller.stream;
// Ajouter un message
void envoyerMessage(String message) {
_controller.add(message);
}
// Envoyer une erreur
void envoyerErreur(String erreur) {
_controller.addError(erreur);
}
// Fermer le stream
void fermer() {
_controller.close();
}
}
// Utilisation
void main() async {
final gestionnaire = GestionnaireMessages();
// Écouter les messages
gestionnaire.messages.listen(
(message) => print('📨 Message: $message'),
onError: (erreur) => print('❌ Erreur: $erreur'),
onDone: () => print('✅ Stream fermé'),
);
// Envoyer des messages
gestionnaire.envoyerMessage('Bonjour');
await Future.delayed(Duration(seconds: 1));
gestionnaire.envoyerMessage('Comment ça va ?');
await Future.delayed(Duration(seconds: 1));
gestionnaire.envoyerErreur('Connection perdue');
await Future.delayed(Duration(seconds: 1));
gestionnaire.envoyerMessage('Reconnecté');
// Fermer
gestionnaire.fermer();
}
Broadcast Stream
Par défaut, un Stream ne peut avoir qu'un seul listener. Pour avoir plusieurs listeners, utilise un broadcast stream :
final controller = StreamController<int>.broadcast();
// Plusieurs listeners
controller.stream.listen((n) => print('Listener 1: $n'));
controller.stream.listen((n) => print('Listener 2: $n'));
controller.add(42);
// Output:
// Listener 1: 42
// Listener 2: 42
Future vs Stream : Tableau comparatif
| Critère | Future | Stream |
|---|---|---|
| Nombre de valeurs | Une seule | Plusieurs (0 à ∞) |
| Quand utiliser | Appel API, lecture fichier unique | Événements, temps réel, flux continu |
| Complétion | Se complète une fois | Peut être fermé ou infini |
| Exemple d'usage | http.get(), File.readAsString() |
WebSocket, capteurs, événements UI |
Bonnes pratiques
1. Toujours gérer les erreurs
// ❌ Mauvais
Future<void> mauvais() async {
await apiCall(); // Peut crasher l'app
}
// ✅ Bon
Future<void> bon() async {
try {
await apiCall();
} catch (e) {
print('Erreur gérée: $e');
}
}
2. Nettoyer les ressources
StreamSubscription? subscription;
void demarrer() {
subscription = monStream.listen((data) {
// Traiter les données
});
}
void arreter() {
subscription?.cancel(); // Important !
subscription = null;
}
3. Éviter le callback hell
// ❌ Difficile à lire
recupererUser().then((user) {
return recupererPosts(user.id);
}).then((posts) {
return traiterPosts(posts);
}).then((result) {
print(result);
});
// ✅ Plus clair
Future<void> bon() async {
final user = await recupererUser();
final posts = await recupererPosts(user.id);
final result = await traiterPosts(posts);
print(result);
}
4. Utiliser timeout pour éviter les blocages
Future<String> appelAvecTimeout() async {
try {
return await apiCall().timeout(
Duration(seconds: 10),
onTimeout: () => throw TimeoutException('Trop lent'),
);
} catch (e) {
print('Erreur: $e');
rethrow;
}
}
Conclusion
La programmation asynchrone en Dart est puissante et élégante. Voici les points clés à retenir :
Future :
-
Représente une opération qui retournera une valeur
-
Parfait pour les appels API, lectures de fichiers uniques
-
Utilise
async/awaitpour un code lisible -
Future.waitpour paralléliser les opérations indépendantes
Stream :
-
Représente un flux de plusieurs valeurs dans le temps
-
Idéal pour les événements, le temps réel, les données continues
-
Utilise
await foroulisten() -
N'oublie pas de
cancel()les subscriptions
Bonnes pratiques :
-
Toujours gérer les erreurs avec
try/catch -
Nettoyer les ressources (cancel subscriptions)
-
Utiliser des timeouts pour éviter les blocages
-
Préférer
async/awaitàthen()
Maintenant tu as toutes les clés pour gérer l'asynchrone comme un pro en Dart !
Ressources pour aller plus loin :
Articles connexes :
Tu as des questions sur l'asynchrone en Dart ? Partage-les dans les commentaires !

Laisser un commentaire