Aller au contenu principal

Le site sera en maintenance le 01/03/2026

Aller au contenu principal

Programmation asynchrone en Dart : Future, Stream et async/await expliqués

Dans cet article, je t’explique en profondeur la programmation asynchrone en Dart : quand utiliser `Future` ou `Stream`, comment gérer les erreurs, et quels patterns avancés adopter.

M
Mpia
15/02/2026 32 min
80 vues
1 comme.
#Dart#Async#Future#Stream
Programmation asynchrone en Dart : Future, Stream et async/await expliqués

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) :

  1. La file d'attente : Les tâches attendent leur tour

  2. Le serveur : Traite une tâche à la fois

  3. La cuisine : Les opérations asynchrones se font "ailleurs"

  4. 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 Future qui retournera un String

  • Future.delayed simule 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 :

  1. Le mot-clé async indique que la fonction contient du code asynchrone

  2. await met en pause l'exécution de la fonction jusqu'à ce que le Future soit complété

  3. Une fois complété, le résultat est assigné à nom

  4. 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 :

  1. Séparation des responsabilités : Le modèle Utilisateur et le service ApiService sont séparés

  2. Gestion d'erreurs : On vérifie le statut HTTP et on catch les erreurs réseau

  3. async/await : Code lisible et séquentiel

  4. 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 :

  1. Stream infini : Le while(true) crée un stream qui ne s'arrête jamais

  2. StreamSubscription : Permet de contrôler l'écoute (pause, resume, cancel)

  3. Gestion d'état : On stocke la dernière température

  4. 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/await pour un code lisible

  • Future.wait pour 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 for ou listen()

  • 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 !

Commentaires (1)

Laisser un commentaire