Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Flutter

WebSockets et Communication en Temps Réel avec Flutter

Un cours complet pour maîtriser WebSockets et Communication en Temps Réel avec Flutter.

0 chapitres 0 QCM Accès gratuit
  1. Objectif

  2. API WebSockets

    1. Qu’est-ce qu’un WebSocket ?
      • Un WebSocket est un protocole qui permet une communication bidirectionnelle et persistante (en temps réel) entre un client, comme un navigateur web, et un serveur, via une seule connexion TCP.
      • Contrairement au protocole HTTP classique, WebSocket autorise le serveur à envoyer des données au client à tout moment, sans nécessiter une requête préalable du client, ce qui est idéal pour les applications interactives comme la messagerie instantanée ou les jeux en ligne.
      • Le protocole WebSocket est un protocole réseau basé sur le protocole TCP. Celui-ci définit la façon dont les données sont échangées entre les réseaux. En raison de sa fiabilité et de son efficacité, il est utilisé par presque tous les clients. TCP établit une connexion entre deux points finaux de communication qu’on appelle des sockets. Ainsi, une communication bidirectionnelle s’établit entre les données.
      • Dans le cas d’une connexion bidirectionnelle comme avec le protocole WebSocket (parfois écrit web socket), les données circulent simultanément dans les deux sens. L’avantage : le chargement des données est beaucoup plus rapide. Le WebSocket est spécialement conçu pour permettre d’établir une communication directe entre une application Web et un serveur WebSocket. Concrètement, cela signifie que : vous cliquez sur un site Web et celui-ci s’affiche en « temps réel ».
      • Un WebSocket est un protocole qui établit une connexion persistante et bidirectionnelle (full-duplex) entre un client (navigateur) et un serveur, permettant un échange de données en temps réel et efficace sans avoir besoin de rouvrir une connexion pour chaque requête, contrairement à HTTP. Cette communication instantanée et continue est idéale pour des applications interactives comme les chats, jeux en ligne, ou tableaux de bord, où le serveur peut pousser des données vers le client spontanément, sans attente.
    2. Cas d’utilisation
      1. Applications de chat en temps réel

        • Les WebSockets sont indispensables pour les applications de messagerie instantanée.
        • Elles permettent d’envoyer et de recevoir des messages immédiatement, sans rafraîchir la page ni effectuer de requêtes répétées. Dès qu’un utilisateur écrit un message, celui-ci est transmis instantanément à tous les participants connectés.
      2. Tableaux de bord avec mises à jour en direct

        • Dans les dashboards (statistiques, monitoring, finance, IoT…), les WebSockets permettent d’actualiser les données automatiquement.
        • Les graphiques, indicateurs ou tableaux se mettent à jour dès que de nouvelles informations arrivent, sans intervention de l’utilisateur. Cela rend l’affichage dynamique et toujours synchronisé avec les données réelles.
      3. Jeux multi-joueurs

        • Les jeux en ligne nécessitent une communication très rapide entre les joueurs et le serveur.
        • Les WebSockets permettent d’envoyer en continu les actions des participants (mouvements, scores, positions…) avec une latence minimale. Grâce à ce canal bidirectionnel, tous les joueurs voient l’état du jeu en temps réel.
      4. Notifications push

        • Les WebSockets permettent d’envoyer des alertes ou notifications immédiatement aux utilisateurs, qu'il s'agisse d'un message, d'un événement système, d'une alerte sécurité ou d'un rappel.
        • Le serveur n’a pas besoin d’attendre qu’une application demande des données : il envoie directement l’information.
      5. Collaboration en temps réel (éditeurs de texte, tableaux blancs…)

        • Pour les applications collaboratives, où plusieurs utilisateurs modifient simultanément un même document (comme Google Docs ou un whiteboard interactif), les WebSockets permettent de synchroniser les actions de chacun instantanément.
        • Lorsqu’un utilisateur ajoute du texte, dessine ou modifie un élément, tous les autres voient la mise à jour immédiatement.
    3. Avantages des WebSockets
      • Communication bidirectionnelle : le serveur peut envoyer des données sans requête préalable du client.
      • Faible latence : pas de surcharge de connexion pour chaque message.
      • Efficace : moins de bande passante utilisée comparé aux requêtes HTTP répétées.
    4. Protocole WebSocket vs HTTP
      • HTTP (Request-Response)
      
      // Modèle traditionnel
      client -> GET /data -> serveur
      client <- Response <- serveur
      // Nouvelle requête nécessaire pour obtenir des updates
        
    5. WebSocket (Full-Duplex)
    6. 
      // Connexion initiale
      client -> HTTP Upgrade -> serveur
      client <- HTTP 101 Switching Protocols <- serveur
      
      // Communication continue
      client <-> Messages bidirectionnels <-> serveur
        
    7. Comparaison détaillée
    8. AspectHTTPWebSocket
      Modèle de communicationRequest-ResponseFull-Duplex
      LatenceÉlevée (nouvelle connexion par requête)Faible (connexion persistante)
      OverheadHeaders HTTP pour chaque requêteHeaders minimaux après handshake
      Usage optimalDonnées statiques, API RESTDonnées temps réel, streaming
      Support serveurUniverselNécessite un serveur WebSocket
  3. Comment fonctionne le WebSocket ?

    1. Cas d'une connexion HTTP
      • Comment fonctionne la consultation d’un site Web sans WebSocket ? Sur Internet, la transmission de site Web se fait en général par le biais d’une connexion HTTP. Le protocole est utilisé pour transmettre les données et permet d’afficher le site Web dans le navigateur. Dans ce contexte, votre client envoie à chaque action (par exemple chaque clic) une requête au serveur.
      • En HTTP, pour consulter un site Web, le client doit d’abord envoyer une requête au serveur. Celui-ci peut ensuite répondre et transmettre le contenu désiré. Il s’agit d’un simple modèle de requête et de réponse, qui finit par occasionner un délai important entre la requête et la réponse.
    2. Cas d'une connexion WebSocket
      • Une connexion WebSocket est initialisée en utilisant le protocole HTTP : chaque connexion à une WebSocket débute par une requête HTTP qui utilise l'option upgrade dans son en-tête. Cette option permet de préciser que le client souhaite que la connexion utilise un autre protocole, en l'occurrence le protocole WebSocket. Cette requête HTTP s'appelle handshake dans le cas de l'utilisation d'une WebSocket.
      • Le protocole HTTP n'est utilisé que pour établir la connexion d'une WebSocket : une fois la connexion établie le protocole HTTP n'est plus utilisé au profit du protocole WebSocket.
      • C'est toujours le client qui initie une demande de connexion : le serveur ne peut pas initier de connexions mais il est à l'écoute des clients qui le contacte pour créer une connexion.
      • Lorsque le serveur répond, la connexion est établie et le client et le serveur peuvent envoyer et recevoir des messages.
      • L’utilisation d’un WebSocket permet la consultation dynamique en temps réel d’un site Web. Avec le protocole WebSocket, il suffit au client d’établir la connexion avec un serveur Web. La connexion entre le client et le serveur s’établit grâce à la phase de handshake du protocole WebSocket. Dans ce cas, le client envoie toutes les identifications nécessaires à l’échange de données au serveur.
      • Une fois établi, le canal de communication reste semi-ouvert. Le serveur peut s’activer de lui-même et transmettre au client toutes les informations sans que le client ne les demande. Les notifications push des sites Web fonctionnent sur ce principe. Si de nouvelles informations sont disponibles sur le serveur, le serveur les communique au client sans que celui-ci n’ait à émettre de requête.
  4. Exemple de Service WebSocket en Ligne

      Service WebSocket Adresse (URL WebSocket) Type Utilisation principale
      WebSocket Echo Test ws://echo.websocket.events Gratuit Serveur de test qui renvoie le même message. Idéal pour apprendre, tester des connexions WebSocket et vérifier la stabilité du client.
      WebSocket.org Echo wss://echo.websocket.org (souvent désactivé, mais utilisé historiquement) Gratuit Ancien service d’écho WebSocket pour les démonstrations et tests de messages.
      CoinCap WebSocket API wss://ws.coincap.io/prices?assets=bitcoin,ethereum Gratuit Streaming de prix de cryptomonnaies en temps réel. Utilisé pour tableaux de bord financiers, crypto apps, graphiques temps réel.
      Binance WebSocket API wss://stream.binance.com:9443/ws/btcusdt@trade Gratuit (limité) Données de trading crypto (prix, volumes, ordres) en temps réel. Utile pour apps de trading, analyse et bots.
      Polygon.io WebSocket wss://socket.polygon.io/stocks Payant Données boursières en temps réel : actions, forex, crypto. Utilisé dans des plateformes financières professionnelles.
      Pusher WebSocket Channels wss://ws.pusherapp.com/app/APP_KEY Payant (avec version gratuite limitée) Push temps réel, notifications, chats, dashboards. Intégré souvent dans des apps web pour synchronisation live.
      Ably Realtime wss://realtime.ably.io Payant (essai gratuit) Messagerie temps réel, synchronisation de documents, IoT, collaboration multi-utilisateurs.
      Kraken Crypto WebSocket wss://ws.kraken.com Gratuit Données crypto en direct : prix, carnets d’ordres. Idéal pour les applications crypto pro.
      Finnhub WebSocket wss://ws.finnhub.io?token=VOTRE_TOKEN Payant (avec plan gratuit limité) Streaming financier : actions, crypto, forex, actualités en direct.
      MetaWeather (via proxy WebSocket) wss://weather.example-proxy/ws Dépend du service Météo en temps réel via WebSocket lorsqu’un proxy WS est utilisé. Utile pour dashboards météo.
    • Vous pouvez utiliser un service de test en ligne comme celui de websocket.org ou socketsbay.com.
    • URL typique (sécurisée) : wss://echo.websocket.org
    • Protocole : wss: // (WebSocket sécurisé, l'équivalent de https:// pour HTTP). ws:// est l'équivalent non sécurisé.
    • Comment ça fonctionne ?
      • Connexion : Votre client (par exemple, un script JavaScript dans votre navigateur ou un outil de test) établit une connexion à wss://echo.websocket.org.
      • Envoi: Votre client envoie un message, disons "Bonjour le monde !".
      • Réception (Écho) : Le serveur d'écho reçoit le message et le renvoie immédiatement au client, qui reçoit donc "Bonjour le monde !".
      Exemple en flutter
      import 'package:flutter/material.dart';
      import 'package:web_socket_channel/web_socket_channel.dart';
      import 'package:web_socket_channel/io.dart'; // Pour la plupart des plateformes
      
      // --- Démarrage de l'Application ---
      void main() {
        runApp(const MyApp());
      }
      
      class MyApp extends StatelessWidget {
        const MyApp({super.key});
      
        @override
        Widget build(BuildContext context) {
          return const MaterialApp(
            title: 'Flutter WebSocket Echo',
            home: WebSocketEchoScreen(),
          );
        }
      }
      
      // --- Widget Principal de l'Écran ---
      class WebSocketEchoScreen extends StatefulWidget {
        const WebSocketEchoScreen({super.key});
      
        @override
        State<WebSocketEchoScreen> createState() => _WebSocketEchoScreenState();
      }
      
      class _WebSocketEchoScreenState extends State<WebSocketEchoScreen> {
        // L'URL du serveur d'écho que vous avez mentionné
        final String _echoUrl = 'wss://echo.websocket.org';
        
        // Le contrôleur pour le champ de texte
        final TextEditingController _controller = TextEditingController();
        
        // La connexion WebSocket. On la déclare plus tard.
        late IOWebSocketChannel _channel;
      
        // Liste pour stocker les messages reçus et envoyés (pour l'affichage)
        List<String> messages = [];
      
        @override
        void initState() {
          super.initState();
          _connectWebSocket();
        }
      
        // --- 1. Méthode de Connexion ---
        void _connectWebSocket() {
          // Établir la connexion. Le '!' est utilisé car l'URL est connue.
          _channel = IOWebSocketChannel.connect(Uri.parse(_echoUrl));
      
          // Écouter les messages entrants
          _channel.stream.listen(
            (data) {
              // Mettre à jour l'interface utilisateur avec le message reçu
              setState(() {
                messages.add('✅ Reçu: $data');
              });
            },
            // Gestion des erreurs
            onError: (error) {
              setState(() {
                messages.add('❌ Erreur de connexion: $error');
              });
            },
            // Gestion de la fermeture de la connexion
            onDone: () {
              setState(() {
                messages.add('⚠️ Connexion fermée.');
              });
            },
            cancelOnError: true,
          );
      
          setState(() {
            messages.add('? Connexion à $_echoUrl...');
          });
        }
      
        // --- 2. Méthode d'Envoi ---
        void _sendMessage() {
          if (_controller.text.isNotEmpty) {
            final messageToSend = _controller.text;
            
            // Envoi du message via le canal
            _channel.sink.add(messageToSend);
            
            setState(() {
              messages.add('⬆️ Envoyé: $messageToSend');
            });
            
            _controller.clear(); // Effacer le champ après l'envoi
          }
        }
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(
              title: const Text('Flutter WebSocket Echo Test'),
              backgroundColor: Colors.blueAccent,
            ),
            body: Padding(
              padding: const EdgeInsets.all(20.0),
              child: Column(
                children: <Widget>[
                  // Champ de saisie et bouton
                  Row(
                    children: <Widget>[
                      Expanded(
                        child: TextField(
                          controller: _controller,
                          decoration: const InputDecoration(labelText: 'Entrez un message'),
                        ),
                      ),
                      ElevatedButton(
                        onPressed: _sendMessage,
                        child: const Text('Envoyer'),
                      ),
                    ],
                  ),
                  const Divider(height: 30),
                  
                  // Affichage des messages
                  Expanded(
                    child: ListView.builder(
                      itemCount: messages.length,
                      itemBuilder: (context, index) {
                        return ListTile(
                          title: Text(messages[index]),
                        );
                      },
                    ),
                  ),
                ],
              ),
            ),
          );
        }
      
        @override
        void dispose() {
          // --- 3. Fermeture du Canal ---
          _channel.sink.close();
          _controller.dispose();
          super.dispose();
        }
      }
  5. Comment fonctionne WebSocket dans les applications Flutter ?

    • WebSocket fonctionne en établissant une connexion bidirectionnelle et persistante entre un client et un serveur, permettant ainsi aux deux d'envoyer et de recevoir des données à tout moment. Voici comment cela fonctionne :
      1. Commencer
        • Le processus commence par l'envoi, par le client, d'une requête HTTP au serveur demandant la mise à niveau de la connexion vers un protocole WebSocket. Cette requête inclut un en-tête spécifique indiquant que le client souhaite utiliser ce protocole. Le serveur répond en confirmant son accord pour procéder à la mise à niveau.
      2. Mise à niveau vers WebSocket
        • Une fois l'accord du serveur obtenu, la connexion bascule de HTTP à WebSocket. Dès lors, la connexion est persistante, c'est-à-dire qu'elle reste ouverte tant que le client et le serveur ont besoin de communiquer. À partir de ce moment, les données sont transmises via le protocole WebSocket plutôt que HTTP.
      3. Communication bidirectionnelle
        • Une fois la mise à niveau terminée, le client et le serveur peuvent envoyer et recevoir des messages de manière autonome. Ceci contraste avec le modèle requête-réponse traditionnel du protocole HTTP, où le client doit toujours initier la communication. Les WebSockets dans Flutter permettent un flux de données continu et en temps réel dans les deux sens.
      4. Transfert de données
        • Les données transmises via WebSockets sont découpées en trames plus petites, pouvant contenir du texte, des données binaires ou des informations de contrôle. La transmission efficace de ces trames permet de réduire la surcharge par rapport à HTTP.
      5. Persistance de la connexion
        • La connexion WebSocket reste ouverte tant que les deux parties le souhaitent. Cela permet une communication instantanée sans nécessiter d'échanges répétés. La connexion n'est fermée que lorsque le client ou le serveur décide d'y mettre fin.
      6. Fermeture de la connexion
        • Lorsque le client ou le serveur souhaite mettre fin à la connexion, il envoie une trame spéciale pour fermer proprement la connexion WebSocket. Une fois la connexion fermée, la communication s'achève et les ressources utilisées sont libérées.
      7. Exemple
        • Avec le WebSocket, le client commence par envoyer une requête classique, comme dans le cadre du protocole HTTP, mais dans ce cas, le processus de connexion s’effectue via une connexion TCP permanente. La phase de handshake entre le client et le serveur se déroule de la manière suivante :
        • Le client envoie la requête :
            
            GET /chatService HTTP/1.1
            Host: server.example.com
            Upgrade: WebSocket
            Connection: Upgrade
            Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
            Sec-WebSocket-Origin: http://example.com
            Sec-WebSocket-Protocol: chat, superchat
            Sec-WebSocket-Version: 13
        • La réponse du serveur est la suivante :
            
            HTTP/1.1 101 Switching Protocols
            Upgrade: WebSocket
            Connection: Upgrade
            Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
            Sec-WebSocket-Protocol: superchat
  6. Implémentation des WebSockets en Flutter

    • Flutter utilise le package web_socket_channel pour gérer les connexions WebSocket. Nous allons détailler son installation et son utilisation, en commençant par un exemple de base pour aboutir à un gestionnaire réutilisable.
      1. Étape 1 : Installation du Package
        • Commencez par créer votre projet Flutter, puis ajoutez la dépendance dans le fichier pubspec.yaml.
        • Ajouter la Dépendance: Ouvrez le fichier pubspec.yaml de votre projet et ajoutez web_socket_channel sous dependencies:
            # pubspec.yaml
            dependencies:
              flutter:
                sdk: flutter
              # ... autres dépendances
              web_socket_channel: ^3.0.2
        • Installer le Package: Exécutez la commande suivante dans votre terminal pour télécharger et lier la dépendance à votre projet :flutter pub get
            
            # pubspec.yaml
            dependencies:
              web_socket_channel: ^3.0.2
            
      2. Étape 2 : Établissement d'une Connexion de Base
        • Pour un test rapide, vous pouvez utiliser les fonctionnalités de base du package dans n'importe quel fichier Dart, comme main.dart ou un widget d'état.
        • A. Code d'Exemple de Base
          • Ce code montre comment se connecter, écouter, envoyer des messages et se déconnecter.
          • main.dart
            import 'package:web_socket_channel/web_socket_channel.dart';
            import 'package:web_socket_channel/io.dart'; // Pour la connexion sur mobile/desktop
            
            // Connexion à un serveur WebSocket
            final channel = IOWebSocketChannel.connect(
              'ws://echo.websocket.org', // URL du serveur WebSocket de test
            );
            
            void main() {
              // Écoute des messages
              channel.stream.listen(
                (message) {
                  print('Message reçu: $message');
                  // Afficher le message dans l'interface utilisateur
                },
                onError: (error) {
                  print('Erreur WebSocket: $error');
                  // Gérer l'erreur
                },
                onDone: () {
                  print('Connexion WebSocket fermée');
                  // Gérer la fermeture
                },
              );
            
              // Envoi d'un message après une courte attente
              Future.delayed(Duration(seconds: 1), () {
                channel.sink.add('Hello WebSocket!');
              });
            
              // Fermeture de la connexion après 5 secondes
              Future.delayed(Duration(seconds: 5), () {
                channel.sink.close();
              });
            }
      3. Étape 3 : Créer un Gestionnaire de WebSocket Réutilisable (Service)
        • Pour une application plus robuste, il est préférable de créer une classe de service réutilisable pour gérer la connexion, les événements (messages reçus, connexion, déconnexion) et l'état de la connexion.
        • A. Création du Fichier web_socket_service.dart
          • Créez un nouveau fichier lib/web_socket_service.dart et implémentez la classe WebSocketService.
          • web_socket_service.dart
            import 'package:web_socket_channel/web_socket_channel.dart';
            import 'package:web_socket_channel/io.dart';
            import 'package:web_socket_channel/status.dart' as status;
            import 'dart:convert';
            import 'dart:typed_data';
            
            class WebSocketService {
              WebSocketChannel? _channel;
              Function(dynamic)? _onMessage;
              Function()? _onConnected;
              Function()? _onDisconnected;
            
              /// Établit la connexion WebSocket.
              void connect(
                String url, {
                Function(dynamic)? onMessage,
                Function()? onConnected,
                Function()? onDisconnected,
              }) {
                _onMessage = onMessage;
                _onConnected = onConnected;
                _onDisconnected = onDisconnected;
            
                try {
                  // Utilisation de IOWebSocketChannel.connect pour un environnement non-web
                  _channel = IOWebSocketChannel.connect(url);
                  
                  _channel!.stream.listen(
                    _handleMessage,
                    onError: _handleError,
                    onDone: _handleDone,
                  );
                  
                  _onConnected?.call();
                  print('Connexion WebSocket établie à $url');
                } catch (e) {
                  print('Erreur de connexion WebSocket: $e');
                  _onDisconnected?.call();
                }
              }
            
              /// Gère les messages reçus et appelle le callback défini.
              void _handleMessage(dynamic message) {
                print('Message WebSocket reçu: $message');
                _onMessage?.call(message);
              }
            
              /// Gère les erreurs.
              void _handleError(error) {
                print('Erreur WebSocket: $error');
                disconnect(); // Déconnecter en cas d'erreur
              }
            
              /// Gère la fermeture de la connexion.
              void _handleDone() {
                print('Connexion WebSocket fermée');
                _onDisconnected?.call();
                _channel = null; // Réinitialiser le canal
              }
            
              /// Envoie un message texte ou binaire via le canal.
              void send(dynamic message) {
                // Vérifie si le canal est connecté (closeCode est null si ouvert)
                if (_channel != null && _channel!.closeCode == null) {
                  _channel!.sink.add(message);
                } else {
                  print('Impossible d\'envoyer le message: WebSocket non connecté');
                }
              }
            
              /// Envoie des données au format JSON.
              void sendJson(Map data) {
                final jsonString = json.encode(data);
                send(jsonString);
              }
              
              /// Envoie des données binaires (Uint8List).
              void sendBinary(Uint8List data) {
                send(data);
              }
            
              /// Ferme explicitement la connexion WebSocket.
              void disconnect() {
                // Utilisation du statut 'goingAway' pour indiquer que le client se déconnecte.
                _channel?.sink.close(status.goingAway);
                _channel = null;
                print('Déconnexion manuelle.');
              }
            
              /// Vérifie l'état de la connexion.
              bool get isConnected => _channel != null && _channel!.closeCode == null;
            }
      4. Étape 4 : Utilisation du Gestionnaire dans un Widget
        • Vous pouvez maintenant utiliser ce service dans un widget de votre application Flutter pour gérer la logique de connexion.
        • A. Exemple d'Utilisation
            code
            final webSocketService = WebSocketService();
            
            void setupWebSocket() {
              webSocketService.connect(
                'ws://echo.websocket.org',
                onConnected: () {
                  print('Application connectée!');
                  webSocketService.send('Hello from Flutter!');
                  // Exemple JSON
                  webSocketService.sendJson({'action': 'login', 'user': 'utilisateur_flutter'});
                },
                onMessage: (message) {
                  if (message is String) {
                    // Gérer les messages texte, potentiellement JSON
                    try {
                      final data = json.decode(message);
                      print('Données JSON reçues: $data');
                    } catch (e) {
                      print('Message texte reçu: $message');
                    }
                  } else if (message is Uint8List) {
                    // Gérer les messages binaires
                    print('Données binaires reçues (${message.length} octets)');
                  }
                },
                onDisconnected: () {
                  print('Application déconnectée!');
                },
              );
            }
            
            // Pour fermer la connexion lorsque ce n'est plus nécessaire (par exemple, dans dispose())
            void closeWebSocket() {
              webSocketService.disconnect();
            }
  7. Gestion d'état avec les WebSockets

    1. Gestion d'État avec le Pattern BLoC et WebSockets
      • Le pattern BLoC (Business Logic Component) gère la connexion, l'envoi et la réception de messages WebSocket en transformant les événements (actions de l'utilisateur ou du système) en états (données affichées à l'écran).
      • Concept
        • Le pattern BLoC est un modèle architectural (souvent utilisé avec le package flutter_bloc) qui permet de séparer clairement la logique métier de l’interface utilisateur.
        • Il repose sur l'utilisation de Streams, ce qui rend l'application plus prédictible, testable et réutilisable.
      • Définition Détaillée
        • Le BLoC agit comme un intermédiaire entre l'UI et la logique métier.
        • Il reçoit des événements émis par l’utilisateur ou par le système, exécute l’action demandée, puis produit de nouveaux états que l’UI va écouter pour se reconstruire automatiquement.
      • Rôle dans les WebSockets
        • Lors d’une connexion WebSocket :
          • L’UI déclenche un événement comme WebSocketConnect.
          • Le BLoC établit la connexion.
          • Quand le WebSocket reçoit un message, le BLoC le transforme en un nouvel événement interne :
          • WebSocketMessageReceived.
          • Le BLoC émet ensuite un nouvel état (ex : WebSocketMessageReceivedState) que l’UI consomme.
          • L’interface utilisateur n’interagit jamais directement avec le WebSocket : elle passe uniquement par le BLoC.
      • Événements
        • Les événements sont des classes qui représentent les actions possibles dans l’application.
        • Exemples :
          • WebSocketConnect
          • WebSocketSendMessage
          • WebSocketDisconnect
      • États
        • Les états représentent tout ce que l’UI peut afficher selon la situation actuelle.
        • Exemples :
          • WebSocketConnecting
          • WebSocketConnected
          • WebSocketErrorState
        code
        
        // Importations nécessaires
        import 'dart:async'; // Pour les Streams et StreamSubscription
        import 'package:bloc/bloc.dart'; // Package BLoC principal
        import 'package:web_socket_channel/web_socket_channel.dart'; // Pour la gestion des WebSockets
        import 'package:web_socket_channel/io.dart'; // Implémentation IOWebSocketChannel pour les plateformes Dart/Flutter
        
        // --- ÉVÉNEMENTS (Inputs du BLoC) ---
        // Définition de la classe abstraite de base pour tous les événements WebSocket.
        abstract class WebSocketEvent {}
        
        /// Événement pour initier la connexion au WebSocket.
        class WebSocketConnect extends WebSocketEvent {
          final String url; // L'URL du serveur WebSocket (ex: 'ws://echo.websocket.org')
          WebSocketConnect(this.url);
        }
        
        /// Événement pour envoyer un message au serveur.
        class WebSocketSendMessage extends WebSocketEvent {
          final dynamic message; // Le message à envoyer (peut être String, JSON, etc.)
          WebSocketSendMessage(this.message);
        }
        
        /// Événement pour demander la déconnexion.
        class WebSocketDisconnect extends WebSocketEvent {}
        
        /// Événement généré *à l'intérieur* du BLoC pour notifier la réception d'un message.
        class WebSocketMessageReceived extends WebSocketEvent {
          final dynamic message;
          WebSocketMessageReceived(this.message);
        }
        
        /// Événement généré *à l'intérieur* du BLoC en cas d'erreur.
        class WebSocketErrorEvent extends WebSocketEvent {
          final String error;
          WebSocketErrorEvent(this.error);
        }
        
        
        // --- ÉTATS (Outputs du BLoC) ---
        // Définition de la classe abstraite de base pour tous les états du WebSocket.
        abstract class WebSocketState {}
        
        /// État initial avant toute action.
        class WebSocketInitial extends WebSocketState {}
        
        /// L'application est en train d'établir la connexion.
        class WebSocketConnecting extends WebSocketState {}
        
        /// La connexion est établie et prête pour la communication.
        class WebSocketConnected extends WebSocketState {}
        
        /// La connexion est fermée (par l'utilisateur ou le serveur).
        class WebSocketDisconnected extends WebSocketState {}
        
        /// Un message a été reçu du serveur. Contient le message.
        class WebSocketMessageReceivedState extends WebSocketState {
          final dynamic message;
          WebSocketMessageReceivedState(this.message);
        }
        
        /// Une erreur s'est produite (connexion, réception, etc.).
        class WebSocketErrorState extends WebSocketState {
          final String error;
          WebSocketErrorState(this.error);
        }
        
        
        // --- BLoC (Logique Métier) ---
        // Le BLoC gère la transition entre les événements et les états.
        class WebSocketBloc extends Bloc {
          // Le canal de communication WebSocket
          WebSocketChannel? _channel;
          // L'abonnement au Stream du WebSocket pour recevoir les messages
          StreamSubscription? _subscription;
        
          // Constructeur : initialise avec l'état initial.
          WebSocketBloc() : super(WebSocketInitial());
        
          // Méthode principale qui mappe un Événement à un nouvel État ou à une série d'États.
          @override
          Stream mapEventToState(WebSocketEvent event) async* {
            if (event is WebSocketConnect) {
              yield* _mapConnectToState(event);
            } else if (event is WebSocketSendMessage) {
              yield* _mapSendMessageToState(event);
            } else if (event is WebSocketDisconnect) {
              yield* _mapDisconnectToState();
            } else if (event is WebSocketMessageReceived) {
              // Un message reçu doit générer un état de réception
              yield WebSocketMessageReceivedState(event.message);
            } else if (event is WebSocketErrorEvent) {
              // Une erreur détectée doit générer un état d'erreur
              yield WebSocketErrorState(event.error);
            }
          }
        
          // Logique de connexion au WebSocket.
          Stream _mapConnectToState(WebSocketConnect event) async* {
            yield WebSocketConnecting();
            try {
              // 1. Établir la connexion
              _channel = IOWebSocketChannel.connect(event.url);
        
              // 2. S'abonner au stream de messages
              _subscription = _channel!.stream.listen(
                // Quand un message arrive, ajouter l'événement interne 'WebSocketMessageReceived'
                (message) => add(WebSocketMessageReceived(message)),
                // En cas d'erreur sur le stream, ajouter l'événement d'erreur
                onError: (error) => add(WebSocketErrorEvent(error.toString())),
                // Quand le stream est terminé (connexion fermée par le serveur), déconnecter
                onDone: () => add(WebSocketDisconnect()),
              );
        
              // 3. Notifier la connexion établie
              yield WebSocketConnected();
            } catch (e) {
              // 4. Gérer l'échec de la connexion initiale
              yield WebSocketErrorState('Erreur de connexion: $e');
            }
          }
        
          // Logique d'envoi d'un message.
          Stream _mapSendMessageToState(WebSocketSendMessage event) async* {
            if (_channel != null) {
              // 'sink.add' envoie le message via la connexion WebSocket.
              _channel!.sink.add(event.message);
            }
            // Note: L'envoi ne génère pas nécessairement un nouvel état ici.
          }
        
          // Logique de déconnexion.
          Stream _mapDisconnectToState() async* {
            // 1. Annuler l'écoute du stream
            await _subscription?.cancel();
            // 2. Fermer le canal (fermeture propre côté client)
            await _channel?.sink.close();
            // 3. Réinitialiser les références
            _channel = null;
            _subscription = null;
            // 4. Notifier la déconnexion
            yield WebSocketDisconnected();
          }
        
          // S'assurer que les ressources sont libérées quand le BLoC est fermé.
          @override
          Future close() {
            _subscription?.cancel();
            _channel?.sink.close();
            return super.close();
          }
        }
    2. Gestion d'État avec le Pattern Provider (ChangeNotifier) et WebSockets
      • Concept
        • Provider est un des systèmes de gestion d’état les plus simples et les plus recommandés en Flutter.
        • Il s'appuie sur InheritedWidget et utilise la classe ChangeNotifier pour notifier les widgets de l’interface lorsqu’un changement survient.
      • Définition Détaillée
        • Une classe comme WebSocketProvider hérite de ChangeNotifier et contient la logique métier du WebSocket.
        • Dès qu’un changement se produit (connexion établie, message reçu, déconnexion…), la méthode :notifyListeners()
        • est appelée pour informer tous les widgets qui consomment ce Provider.
      • Rôle dans les WebSockets
        • Quand le WebSocket reçoit un message dans son listen :
        • Le Provider ajoute le message dans la liste _messages.
        • Il appelle notifyListeners().
        • Tous les widgets qui affichent ces messages sont automatiquement reconstruits.
        • L’UI n’a pas besoin de gérer la logique métier : elle réagit simplement aux mises à jour.
      • ChangeNotifier
        • ChangeNotifier est une classe Flutter qui fournit un mécanisme complet pour notifier automatiquement les widgets abonnés qu’une modification de données a eu lieu.
        • C’est l’élément clé du Provider.
      • Consommation
        • L'IU peut accéder aux données :
        • avec un widget Consumer
        • avec Provider.of(context)
        • À chaque appel de notifyListeners() :
          • l’UI est mise à jour
          • les widgets se reconstruisent automatiquement avec les nouvelles données (messages, status de connexion, etc.)
        Code
        import 'package:flutter/foundation.dart'; // Pour ChangeNotifier et @override
        import 'package:web_socket_channel/web_socket_channel.dart';
        import 'package:web_socket_channel/io.dart'; // Pour IOWebSocketChannel
        
        /// Pattern Provider pour gérer l'état d'un WebSocket.
        // 'with ChangeNotifier' permet à la classe de notifier l'UI des changements.
        class WebSocketProvider with ChangeNotifier {
          WebSocketChannel? _channel;
          
          // État interne : Statut de la connexion
          bool _isConnected = false;
          // État interne : Liste des messages reçus
          final List _messages = [];
        
          // Getters pour accéder aux états depuis l'UI (lecture seule)
          bool get isConnected => _isConnected;
          List get messages => _messages;
        
          /// Tente d'établir la connexion WebSocket.
          Future connect(String url) async {
            try {
              _channel = IOWebSocketChannel.connect(url);
              _isConnected = true;
              // Notifier immédiatement l'UI que la connexion est en cours/établie
              notifyListeners(); 
        
              // Écouter le stream de messages du WebSocket
              _channel!.stream.listen(
                (message) {
                  // À la réception d'un message :
                  _messages.add(message);
                  // Notifier l'UI pour qu'elle se reconstruise avec le nouveau message
                  notifyListeners();
                },
                onError: (error) {
                  // En cas d'erreur :
                  print('Erreur WebSocket: $error');
                  _isConnected = false;
                  // Notifier la déconnexion/erreur
                  notifyListeners();
                },
                onDone: () {
                  // Quand le serveur ferme la connexion :
                  _isConnected = false;
                  // Notifier la déconnexion
                  notifyListeners();
                },
              );
            } catch (e) {
              // Gérer l'échec de la connexion initiale
              _isConnected = false;
              notifyListeners();
              throw Exception('Erreur de connexion WebSocket: $e');
            }
          }
        
          /// Envoie un message via le WebSocket.
          void sendMessage(dynamic message) {
            if (_isConnected && _channel != null) {
              _channel!.sink.add(message);
            }
          }
        
          /// Ferme la connexion WebSocket.
          void disconnect() {
            // Fermer le canal (libère les ressources)
            _channel?.sink.close();
            _channel = null;
            _isConnected = false;
            // Notifier la déconnexion
            notifyListeners();
          }
        
          /// Méthode appelée lorsque le Provider est retiré de l'arbre (widget dispose).
          @override
          void dispose() {
            // Assurer la fermeture propre de la connexion
            disconnect(); 
            super.dispose();
          }
        }
        
  8. Applications pratiques

    1. Application de chat en temps réel
        Code
        
        // Modèle de message
        class ChatMessage {
          final String id;
          final String sender;
          final String text;
          final DateTime timestamp;
        
          ChatMessage({
            required this.id,
            required this.sender,
            required this.text,
            required this.timestamp,
          });
        
          factory ChatMessage.fromJson(Map<String, dynamic> json) {
            return ChatMessage(
              id: json['id'],
              sender: json['sender'],
              text: json['text'],
              timestamp: DateTime.parse(json['timestamp']),
            );
          }
        
          Map<String, dynamic> toJson() {
            return {
              'id': id,
              'sender': sender,
              'text': text,
              'timestamp': timestamp.toIso8601String(),
            };
          }
        }
        
        // Service de chat WebSocket
        class ChatWebSocketService {
          final WebSocketChannel channel;
          final ValueChanged<ChatMessage> onMessageReceived;
          final VoidCallback onConnected;
          final ValueChanged<String> onError;
        
          ChatWebSocketService({
            required this.channel,
            required this.onMessageReceived,
            required this.onConnected,
            required this.onError,
          }) {
            channel.stream.listen(
              (message) {
                try {
                  final data = json.decode(message);
                  final chatMessage = ChatMessage.fromJson(data);
                  onMessageReceived(chatMessage);
                } catch (e) {
                  onError('Erreur de parsing du message: $e');
                }
              },
              onError: (error) => onError('Erreur WebSocket: $error'),
              onDone: () => onError('Connexion WebSocket fermée'),
            );
          }
        
          void sendMessage(ChatMessage message) {
            final jsonMessage = json.encode(message.toJson());
            channel.sink.add(jsonMessage);
          }
        
          void dispose() {
            channel.sink.close();
          }
        }
        
        // UI pour l'application de chat
        class ChatScreen extends StatefulWidget {
          final String username;
        
          const ChatScreen({Key? key, required this.username}) : super(key: key);
        
          @override
          _ChatScreenState createState() => _ChatScreenState();
        }
        
        class _ChatScreenState extends State<ChatScreen> {
          final TextEditingController _messageController = TextEditingController();
          final List<ChatMessage> _messages = [];
          late ChatWebSocketService _chatService;
        
          @override
          void initState() {
            super.initState();
            _connectToChat();
          }
        
          void _connectToChat() {
            final channel = IOWebSocketChannel.connect('ws://your-chat-server.com/chat');
            _chatService = ChatWebSocketService(
              channel: channel,
              onMessageReceived: _handleMessageReceived,
              onConnected: () => print('Connecté au chat'),
              onError: _handleError,
            );
          }
        
          void _handleMessageReceived(ChatMessage message) {
            setState(() {
              _messages.add(message);
            });
          }
        
          void _handleError(String error) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(error)),
            );
          }
        
          void _sendMessage() {
            if (_messageController.text.isNotEmpty) {
              final message = ChatMessage(
                id: DateTime.now().millisecondsSinceEpoch.toString(),
                sender: widget.username,
                text: _messageController.text,
                timestamp: DateTime.now(),
              );
              _chatService.sendMessage(message);
              _messageController.clear();
            }
          }
        
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(title: Text('Chat - ${widget.username}')),
              body: Column(
                children: [
                  Expanded(
                    child: ListView.builder(
                      itemCount: _messages.length,
                      itemBuilder: (context, index) {
                        final message = _messages[index];
                        return ListTile(
                          title: Text(message.sender),
                          subtitle: Text(message.text),
                          trailing: Text(
                            DateFormat('HH:mm').format(message.timestamp),
                          ),
                        );
                      },
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Row(
                      children: [
                        Expanded(
                          child: TextField(
                            controller: _messageController,
                            decoration: InputDecoration(
                              hintText: 'Tapez votre message...',
                            ),
                            onSubmitted: (_) => _sendMessage(),
                          ),
                        ),
                        IconButton(
                          icon: Icon(Icons.send),
                          onPressed: _sendMessage,
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            );
          }
        
          @override
          void dispose() {
            _chatService.dispose();
            _messageController.dispose();
            super.dispose();
          }
        }
        
    2. Service pour les données en temps réel
      • Description de l'Application
        • ? Vue d'ensemble
          • Cette application Flutter est un tableau de bord en temps réel qui affiche des métriques importantes via une connexion WebSocket. Elle permet de visualiser des données actualisées en continu telles que le nombre d'utilisateurs en ligne, les commandes du jour, les revenus, et d'autres indicateurs clés de performance (KPIs).
        • Architecture Client-Serveur :
          • Frontend: Application Flutter (client WebSocket)
          • Backend: Serveur Node.js avec WebSocket (serveur de données)
          • Communication: Protocole WebSocket pour les données temps réel
        • ? Objectifs
          • Visualiser des données en temps réel
          • Recevoir des mises à jour automatiques via WebSocket
          • Interface utilisateur responsive et moderne
          • Gestion robuste des erreurs de connexion
          • Thème clair/sombre adaptatif
        • ✨ Fonctionnalités principales
          • Connexion WebSocket persistante
          • Affichage de métriques en temps réel
          • Interface avec cartes colorées
          • Rafraîchissement manuel des données
          • Gestion des erreurs et reconnexion
          • Thème clair/sombre automatique
        • ?️ Architecture du Projet Complet
          • ? Structure Frontend (Flutter)
              lib/ ├── ? main.dart # Point d'entrée principal ├── ? app.dart # Configuration de l'application └── ? dashboard/ # Module principal du tableau de bord ├─----─ ? dashboard_screen.dart # Écran principal ├─----─ ? realtime_data_card.dart # Widget des cartes de données ├─----─ ? realtime_dashboard_service.dart # Service WebSocket └─----─ ? dashboard_data_model.dart # Modèles de données
          • ? Structure Backend (Node.js)
              mon-dashboard-server/ ├── ? server.js # Serveur WebSocket principal ├── ? package.json # Configuration Node.js ├── ? package-lock.json # Verrouillage des dépendances └── ? node_modules/ # Dépendances installées
      • Frontend : Application Flutter (client WebSocket)
          dashboard/dashboard_data_model.dart
          class DashboardData {
            final int onlineUsers;
            final int todayOrders;
            final double todayRevenue;
            final double conversionRate;
            final int visits;
            final int avgSessionTime;
          
            DashboardData({
              required this.onlineUsers,
              required this.todayOrders,
              required this.todayRevenue,
              required this.conversionRate,
              this.visits = 0,
              this.avgSessionTime = 0,
            });
          
            factory DashboardData.fromJson(Map json) {
              return DashboardData(
                onlineUsers: json['onlineUsers'] ?? 0,
                todayOrders: json['todayOrders'] ?? 0,
                todayRevenue: (json['todayRevenue'] ?? 0).toDouble(),
                conversionRate: (json['conversionRate'] ?? 0).toDouble(),
                visits: json['visits'] ?? 0,
                avgSessionTime: json['avgSessionTime'] ?? 0,
              );
            }
          
            Map toJson() {
              return {
                'onlineUsers': onlineUsers,
                'todayOrders': todayOrders,
                'todayRevenue': todayRevenue,
                'conversionRate': conversionRate,
                'visits': visits,
                'avgSessionTime': avgSessionTime,
              };
            }
          }
          dashboard/dashboard_screen.dart
          import 'package:flutter/material.dart';
          import 'package:web_socket_channel/io.dart';
          import 'realtime_dashboard_service.dart';
          import 'realtime_data_card.dart';
          
          class DashboardScreen extends StatefulWidget {
            @override
            _DashboardScreenState createState() => _DashboardScreenState();
          }
          
          class _DashboardScreenState extends State {
            final Map _data = {};
            late RealtimeDashboardService _dashboardService;
            bool _isConnected = false;
          
            @override
            void initState() {
              super.initState();
              _connectToDashboard();
            }
          
            void _connectToDashboard() {
              print('? Connexion WebSocket...');
              
              final channel = IOWebSocketChannel.connect(
                'ws://10.0.2.2:8080', // Pour émulateur Android
              );
              
              _dashboardService = RealtimeDashboardService(
                channel: channel,
                onDataUpdate: _handleDataUpdate,
                onConnected: () {
                  print('✅ Connecté!');
                  setState(() {
                    _isConnected = true;
                  });
                  _dashboardService.requestData('all');
                },
                onError: _handleError,
              );
            }
          
            void _handleDataUpdate(Map newData) {
              print('? Données reçues: $newData');
              
              if (mounted) {
                setState(() {
                  _data.addAll(newData);
                });
              }
            }
          
            void _handleError(String error) {
              print('❌ Erreur: $error');
              
              if (mounted) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text(error),
                    backgroundColor: Colors.red,
                  ),
                );
              }
            }
          
            // Fonctions helpers pour parser les données
            double _parseDouble(dynamic value) {
              if (value == null) return 0.0;
              if (value is double) return value;
              if (value is int) return value.toDouble();
              if (value is String) {
                final parsed = double.tryParse(value);
                return parsed ?? 0.0;
              }
              return 0.0;
            }
          
            int _parseInt(dynamic value) {
              if (value == null) return 0;
              if (value is int) return value;
              if (value is double) return value.toInt();
              if (value is String) {
                final parsed = int.tryParse(value);
                return parsed ?? 0;
              }
              return 0;
            }
          
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  title: const Text('Tableau de bord en temps réel'),
                  backgroundColor: Colors.blueGrey[800],
                  actions: [
                    Icon(
                      _isConnected ? Icons.wifi : Icons.wifi_off,
                      color: _isConnected ? Colors.green : Colors.red,
                    ),
                    const SizedBox(width: 8),
                    Text(
                      _isConnected ? 'Connecté' : 'Déconnecté',
                      style: TextStyle(
                        color: _isConnected ? Colors.green : Colors.red,
                      ),
                    ),
                    const SizedBox(width: 16),
                  ],
                ),
                body: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: GridView.count(
                    crossAxisCount: MediaQuery.of(context).size.width > 600 ? 3 : 2,
                    crossAxisSpacing: 16,
                    mainAxisSpacing: 16,
                    childAspectRatio: 1.2,
                    children: [
                      RealtimeDataCard(
                        title: 'Utilisateurs en ligne',
                        value: _parseInt(_data['onlineUsers']).toString(),
                        color: Colors.green,
                      ),
                      RealtimeDataCard(
                        title: 'Commandes aujourd\'hui',
                        value: _parseInt(_data['todayOrders']).toString(),
                        color: Colors.blue,
                      ),
                      RealtimeDataCard(
                        title: 'Revenu aujourd\'hui',
                        // CORRECTION ICI : utilisation de _parseDouble
                        value: '€${_parseDouble(_data['todayRevenue']).toStringAsFixed(2)}',
                        color: Colors.purple,
                      ),
                      RealtimeDataCard(
                        title: 'Taux de conversion',
                        // CORRECTION ICI : utilisation de _parseDouble
                        value: '${_parseDouble(_data['conversionRate']).toStringAsFixed(1)}%',
                        color: Colors.orange,
                      ),
                      RealtimeDataCard(
                        title: 'Visites',
                        value: _parseInt(_data['visits']).toString(),
                        color: Colors.deepPurple,
                      ),
                      RealtimeDataCard(
                        title: 'Temps moyen',
                        value: '${_parseInt(_data['avgSessionTime'])} min',
                        color: Colors.teal,
                      ),
                    ],
                  ),
                ),
                floatingActionButton: FloatingActionButton(
                  onPressed: () => _dashboardService.requestData('all'),
                  child: const Icon(Icons.refresh),
                ),
              );
            }
          
            @override
            void dispose() {
              _dashboardService.dispose();
              super.dispose();
            }
          }
          dashboard/realtime_dashboard_service.dart
          import 'dart:convert';
          import 'package:flutter/material.dart';
          import 'package:web_socket_channel/web_socket_channel.dart';
          
          class RealtimeDashboardService {
            final WebSocketChannel channel;
            final ValueChanged> onDataUpdate;
            final VoidCallback onConnected;
            final ValueChanged onError;
          
            RealtimeDashboardService({
              required this.channel,
              required this.onDataUpdate,
              required this.onConnected,
              required this.onError,
            }) {
              channel.stream.listen(
                (data) {
                  try {
                    final jsonData = json.decode(data) as Map;
                    onDataUpdate(jsonData);
                  } catch (e) {
                    onError('Erreur de parsing des données: $e');
                  }
                },
                onError: (error) => onError('Erreur WebSocket: $error'),
                onDone: () => onError('Connexion WebSocket fermée'),
              );
            }
          
            void requestData(String type) {
              final request = json.encode({'type': 'request', 'dataType': type});
              channel.sink.add(request);
            }
          
            void dispose() {
              channel.sink.close();
            }
          }
          dashboard/realtime_data_card.dart
          import 'package:flutter/material.dart';
          
          class RealtimeDataCard extends StatelessWidget {
            final String title;
            final dynamic value;
            final Color color;
          
            const RealtimeDataCard({
              Key? key,
              required this.title,
              required this.value,
              this.color = Colors.blue,
            }) : super(key: key);
          
            @override
            Widget build(BuildContext context) {
              return Card(
                elevation: 4,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
                color: color,
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        title,
                        style: const TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                          color: Colors.white,
                        ),
                      ),
                      const SizedBox(height: 8),
                      Text(
                        value.toString(),
                        style: const TextStyle(
                          fontSize: 24,
                          fontWeight: FontWeight.bold,
                          color: Colors.white,
                        ),
                      ),
                    ],
                  ),
                ),
              );
            }
          }
          main.dart
          import 'package:flutter/material.dart';
          import 'app.dart';
          import 'dashboard/dashboard_screen.dart';
          
          void main() {
            runApp(const DashboardAppWithSplash());
          }
          
          class DashboardAppWithSplash extends StatelessWidget {
            const DashboardAppWithSplash({super.key});
          
            @override
            Widget build(BuildContext context) {
              return MaterialApp(
                title: 'Tableau de Bord Temps Réel',
                theme: ThemeData(
                  primarySwatch: Colors.blueGrey,
                  useMaterial3: true,
                ),
                debugShowCheckedModeBanner: false,
                home: const SplashScreen(),
              );
            }
          }
          
          class SplashScreen extends StatefulWidget {
            const SplashScreen({super.key});
          
            @override
            State createState() => _SplashScreenState();
          }
          
          class _SplashScreenState extends State {
            @override
            void initState() {
              super.initState();
              _navigateToDashboard();
            }
          
            void _navigateToDashboard() async {
              // Simuler un temps de chargement
              await Future.delayed(const Duration(seconds: 2));
              
              if (mounted) {
                Navigator.of(context).pushReplacement(
                  MaterialPageRoute(
                    builder: (context) =>  DashboardScreen(),
                  ),
                );
              }
            }
          
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                backgroundColor: Colors.blueGrey[800],
                body: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const Icon(
                        Icons.insights,
                        size: 80,
                        color: Colors.white,
                      ),
                      const SizedBox(height: 20),
                      Text(
                        'Dashboard Pro',
                        style: TextStyle(
                          fontSize: 32,
                          fontWeight: FontWeight.bold,
                          color: Colors.blueGrey[100],
                        ),
                      ),
                      const SizedBox(height: 10),
                      const Text(
                        'Tableau de bord en temps réel',
                        style: TextStyle(
                          fontSize: 16,
                          color: Colors.white70,
                        ),
                      ),
                      const SizedBox(height: 40),
                      SizedBox(
                        width: 60,
                        height: 60,
                        child: CircularProgressIndicator(
                          color: Colors.blueGrey[200],
                          strokeWidth: 3,
                        ),
                      ),
                    ],
                  ),
                ),
              );
            }
          }
          app.dart
          import 'package:flutter/material.dart';
          import 'dashboard/dashboard_screen.dart';
          
          class DashboardApp extends StatelessWidget {
            const DashboardApp({super.key});
          
            @override
            Widget build(BuildContext context) {
              return MaterialApp(
                title: 'Tableau de Bord Temps Réel',
                theme: ThemeData(
                  useMaterial3: true,
                  colorScheme: ColorScheme.fromSeed(
                    seedColor: Colors.blueGrey,
                    brightness: Brightness.light,
                  ),
                  appBarTheme: const AppBarTheme(
                    backgroundColor: Color(0xFF263238),
                    elevation: 4,
                    titleTextStyle: TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                      color: Colors.white,
                    ),
                    centerTitle: true,
                  ),
                  cardTheme: CardThemeData(  // ✅ CardThemeData au lieu de CardTheme
                    elevation: 2,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(12),
                    ),
                  ),
                  floatingActionButtonTheme: const FloatingActionButtonThemeData(
                    backgroundColor: Colors.blueGrey,
                    foregroundColor: Colors.white,
                  ),
                  scaffoldBackgroundColor: Colors.grey[50],
                ),
                darkTheme: ThemeData(
                  useMaterial3: true,
                  colorScheme: ColorScheme.fromSeed(
                    seedColor: Colors.blueGrey,
                    brightness: Brightness.dark,
                  ),
                  appBarTheme: const AppBarTheme(
                    backgroundColor: Color(0xFF1E1E1E),
                    elevation: 4,
                  ),
                  cardTheme: CardThemeData(
                    elevation: 4,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(12),
                    ),
                  ),
                  floatingActionButtonTheme: const FloatingActionButtonThemeData(
                    backgroundColor: Colors.blueAccent,
                    foregroundColor: Colors.white,
                  ),
                ),
                themeMode: ThemeMode.system,
                debugShowCheckedModeBanner: false,
                home: DashboardScreen(),
              );
            }
          }
      • Backend : Serveur Node.js avec WebSocket (serveur de données)
      • mon-dashboard-server/package.json
        {
          "name": "dashboard-websocket-server",
          "version": "1.0.0",
          "description": "Serveur WebSocket pour le tableau de bord",
          "main": "server.js",
          "scripts": {
            "start": "node server.js",
            "dev": "node server.js"
          },
          "dependencies": {
            "ws": "^8.14.2"
          }
        }
        mon-dashboard-server/server.js
        const WebSocket = require('ws');
        
        // Création du serveur WebSocket sur le port 8080
        const wss = new WebSocket.Server({ 
          port: 8080  // ← CHANGEMENT ICI : juste le port, pas 'server: server'
        });
        
        console.log('Serveur WebSocket démarré...');
        console.log('En attente de connexions sur ws://localhost:8080');
        console.log('Votre application Flutter doit se connecter à cette URL');
        
        // Quand un client se connecte
        wss.on('connection', (ws) => {
          console.log(' Nouveau client connecté !');
          
          // Envoyer un message de bienvenue
          ws.send(JSON.stringify({
            type: 'connection',
            message: 'Connecté au serveur de dashboard',
            timestamp: new Date().toISOString(),
            status: 'connected'
          }));
          
          // Envoyer des données toutes les 2 secondes
          const interval = setInterval(() => {
            // Données aléatoires pour simulation
            const fakeData = {
              onlineUsers: Math.floor(Math.random() * 200) + 50,      // 50-250
              todayOrders: Math.floor(Math.random() * 100) + 10,      // 10-110
              todayRevenue: (Math.random() * 5000 + 1000).toFixed(2), // 1000-6000
              conversionRate: (Math.random() * 4 + 1).toFixed(1),     // 1.0-5.0
              visits: Math.floor(Math.random() * 2000) + 500,         // 500-2500
              avgSessionTime: Math.floor(Math.random() * 10) + 2,     // 2-12 min
              timestamp: new Date().toISOString(),
              updateId: Date.now()
            };
            
            console.log('? Envoi de données:', JSON.stringify(fakeData));
            
            // Envoyer au client Flutter
            ws.send(JSON.stringify(fakeData));
            
          }, 2000); // Toutes les 2 secondes
          
          // Quand le client envoie un message
          ws.on('message', (message) => {
            console.log('? Message reçu du client:', message.toString());
            
            try {
              const data = JSON.parse(message);
              
              if (data.type === 'request') {
                console.log(`? Demande de données: ${data.dataType}`);
                
                // Répondre immédiatement à la demande
                const fakeData = {
          onlineUsers: Math.floor(Math.random() * 200) + 50,      // ← 156 (nombre)
          todayOrders: Math.floor(Math.random() * 100) + 10,      // ← 42 (nombre)
          todayRevenue: Math.random() * 5000 + 1000,              // ← 1250.75 (nombre)
          conversionRate: Math.random() * 4 + 1,                  // ← 2.5 (nombre)
          visits: Math.floor(Math.random() * 2000) + 500,         // ← 1200 (nombre)
          avgSessionTime: Math.floor(Math.random() * 10) + 2,     // ← 5 (nombre)
          timestamp: new Date().toISOString(),
          updateId: Date.now()
        };
                
                ws.send(JSON.stringify(responseData));
                console.log(' Réponse envoyée:', JSON.stringify(responseData));
              }
            } catch (error) {
              console.log(' Erreur de parsing JSON:', error);
            }
          });
          
          // Quand le client se déconnecte
          ws.on('close', () => {
            console.log('Client déconnecté');
            clearInterval(interval); // Arrêter l'envoi de données
          });
          
          // En cas d'erreur
          ws.on('error', (error) => {
            console.log(' Erreur WebSocket:', error);
          });
        });
        
        // Gérer l'arrêt propre
        process.on('SIGINT', () => {
          console.log('\n Arrêt du serveur...');
          wss.close();
          process.exit(0);
        });
        
        console.log(' Serveur prêt. Appuyez sur Ctrl+C pour arrêter.');
  9. Bonnes pratiques et optimisation

    1. Gestion de la reconnexion
      
      class ReconnectWebSocket {
        final String url;
        final Duration reconnectInterval;
        final int maxReconnectAttempts;
        
        WebSocketChannel? _channel;
        StreamSubscription? _subscription;
        int _reconnectAttempts = 0;
        Timer? _reconnectTimer;
        
        ReconnectWebSocket({
          required this.url,
          this.reconnectInterval = const Duration(seconds: 5),
          this.maxReconnectAttempts = 10,
        });
        
        Future connect() async {
          try {
            _channel = IOWebSocketChannel.connect(url);
            _reconnectAttempts = 0;
            
            _subscription = _channel!.stream.listen(
              _onMessage,
              onError: _onError,
              onDone: _onDone,
            );
            
            print('WebSocket connecté avec succès');
          } catch (e) {
            print('Échec de connexion WebSocket: $e');
            _scheduleReconnect();
          }
        }
        
        void _onMessage(dynamic message) {
          // Traiter le message
        }
        
        void _onError(error) {
          print('Erreur WebSocket: $error');
          _scheduleReconnect();
        }
        
        void _onDone() {
          print('Connexion WebSocket fermée');
          _scheduleReconnect();
        }
        
        void _scheduleReconnect() {
          if (_reconnectAttempts < maxReconnectAttempts) {
            _reconnectTimer = Timer(reconnectInterval, () {
              _reconnectAttempts++;
              print('Tentative de reconnexion $_reconnectAttempts/$maxReconnectAttempts');
              connect();
            });
          } else {
            print('Nombre maximum de tentatives de reconnexion atteint');
          }
        }
        
        void send(dynamic message) {
          if (_channel != null) {
            _channel!.sink.add(message);
          }
        }
        
        void disconnect() {
          _reconnectTimer?.cancel();
          _subscription?.cancel();
          _channel?.sink.close();
        }
      }
      
    2. Compression des messages
      
      import 'dart:convert';
      import 'package:archive/archive.dart';
      
      class CompressedWebSocket {
        final WebSocketChannel channel;
        
        CompressedWebSocket(this.channel);
        
        // Compression d'un message JSON
        String compressMessage(Map message) {
          final jsonString = json.encode(message);
          final bytes = utf8.encode(jsonString);
          final compressed = GZipEncoder().encode(bytes);
          return base64.encode(compressed!);
        }
        
        // Décompression d'un message
        Map decompressMessage(String compressedMessage) {
          final compressedBytes = base64.decode(compressedMessage);
          final decompressedBytes = GZipDecoder().decodeBytes(compressedBytes);
          final jsonString = utf8.decode(decompressedBytes);
          return json.decode(jsonString);
        }
        
        void sendCompressed(Map message) {
          final compressed = compressMessage(message);
          channel.sink.add(compressed);
        }
        
        Stream> get decompressedStream {
          return channel.stream.map((compressedMessage) {
            return decompressMessage(compressedMessage);
          });
        }
      }
      
    3. Sécurisation des connexions
      
      import 'package:web_socket_channel/web_socket_channel.dart';
      import 'package:web_socket_channel/io.dart';
      
      class SecureWebSocketService {
        WebSocketChannel connectSecure(
          String url, {
          Map? headers,
          Duration? pingInterval,
        }) {
          // Utilisation de wss:// pour les connexions sécurisées
          if (!url.startsWith('wss://')) {
            throw ArgumentError('URL must use wss:// for secure connections');
          }
          
          return IOWebSocketChannel.connect(
            url,
            headers: headers,
            pingInterval: pingInterval,
          );
        }
        
        // Ajout de tokens d'authentification
        WebSocketChannel connectWithAuth(
          String url,
          String authToken, {
          Duration? pingInterval,
        }) {
          return IOWebSocketChannel.connect(
            url,
            headers: {'Authorization': 'Bearer $authToken'},
            pingInterval: pingInterval,
          );
        }
      }
      
  10. Exercices pratiques

    • Exercice 1: Application de notifications en temps réel
      • Créez une application qui affiche des notifications en temps réel en utilisant WebSockets.
      • Fonctionnalités:
        • Connexion à un serveur WebSocket de notifications
        • Affichage des notifications en temps réel
        • Marquer les notifications comme lues
        • Historique des notifications
    • Exercice 2: Tableau de bord de cryptomonnaies
      • Développez un tableau de bord qui affiche les prix des cryptomonnaies en temps réel.
      • Fonctionnalités:
        • Connexion à une API WebSocket de prix de cryptomonnaies
        • Affichage des prix en temps réel avec graphiques
        • Alertes de prix personnalisées
        • Portfolio de suivi
        • Solution
          • Analyse d'une Application de Dashboard Cryptomonnaie
            • Architecture de l'Application
                lib/ ├── main.dart ├── models/ │ ├── crypto_data.dart │ ├── price_point.dart │ ├── alert.dart │ └── portfolio_item.dart ├── services/ │ └── crypto_service.dart ├── widgets/ │ ├── summary_card.dart │ ├── crypto_grid.dart │ ├── chart_widget.dart │ ├── alerts_section.dart │ └── portfolio_section.dart └── screens/ └── crypto_dashboard.dart
            models/alert.dart
            class Alert {
              final String id;
              final String symbol;
              final String type; // 'above' ou 'below'
              final double price;
              bool triggered;
            
              Alert({
                required this.id,
                required this.symbol,
                required this.type,
                required this.price,
                this.triggered = false,
              });
            }
            models/crypto_data.dart
            import 'price_point.dart';
            
            class CryptoData {
              double price;
              double change;
              List history;
            
              CryptoData({
                required this.price,
                required this.change,
                required this.history,
              });
            }
            models/portfolio_item.dart
            class PortfolioItem {
              final String id;
              final String symbol;
              final double amount;
              final double buyPrice;
            
              PortfolioItem({
                required this.id,
                required this.symbol,
                required this.amount,
                required this.buyPrice,
              });
            }
            models/price_point.dart
            class PricePoint {
              final DateTime time;
              final double price;
            
              PricePoint(this.time, this.price);
            }
            screens/crypto_dashboard.dart
            import 'package:flutter/material.dart';
            import '../models/alert.dart';
            import '../models/portfolio_item.dart';
            import '../services/crypto_service.dart';
            import '../widgets/summary_card.dart';
            import '../widgets/crypto_grid.dart';
            import '../widgets/chart_widget.dart';
            import '../widgets/alerts_section.dart';
            import '../widgets/portfolio_section.dart';
            
            class CryptoDashboard extends StatefulWidget {
              const CryptoDashboard({Key? key}) : super(key: key);
            
              @override
              State createState() => _CryptoDashboardState();
            }
            
            class _CryptoDashboardState extends State {
              final CryptoService _cryptoService = CryptoService();
              List alerts = [];
              List portfolio = [];
              String selectedCrypto = 'BTC';
              bool isConnected = false;
            
              @override
              void initState() {
                super.initState();
                _startPriceUpdates();
              }
            
              @override
              void dispose() {
                _cryptoService.dispose();
                super.dispose();
              }
            
              void _startPriceUpdates() {
                setState(() => isConnected = true);
                
                _cryptoService.onAlertTriggered = (symbol, price) {
                  _showAlertNotification(symbol, price);
                };
                
                _cryptoService.startPriceUpdates(() {
                  setState(() {});
                }, alerts);
              }
            
              void _showAlertNotification(String symbol, double price) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('? $symbol a atteint ${price.toStringAsFixed(2)} USD'),
                    backgroundColor: Colors.green,
                    duration: const Duration(seconds: 4),
                  ),
                );
              }
            
              double _calculatePortfolioValue() {
                return portfolio.fold(0.0, (sum, item) {
                  final currentPrice = _cryptoService.getCryptoPrice(item.symbol);
                  return sum + (item.amount * currentPrice);
                });
              }
            
              double _calculatePortfolioPnL() {
                return portfolio.fold(0.0, (sum, item) {
                  final currentPrice = _cryptoService.getCryptoPrice(item.symbol);
                  return sum + ((currentPrice - item.buyPrice) * item.amount);
                });
              }
            
              @override
              Widget build(BuildContext context) {
                return Scaffold(
                  body: Container(
                    decoration: const BoxDecoration(
                      gradient: LinearGradient(
                        begin: Alignment.topLeft,
                        end: Alignment.bottomRight,
                        colors: [Color(0xFF0F172A), Color(0xFF581C87), Color(0xFF0F172A)],
                      ),
                    ),
                    child: SafeArea(
                      child: SingleChildScrollView(
                        padding: const EdgeInsets.all(16),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            // En-tête
                            Row(
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
                              children: [
                                const Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Text(
                                      '? Crypto Dashboard',
                                      style: TextStyle(
                                        fontSize: 28,
                                        fontWeight: FontWeight.bold,
                                        color: Colors.white,
                                      ),
                                    ),
                                    Text(
                                      'Suivi en temps réel',
                                      style: TextStyle(color: Color(0xFFC4B5FD)),
                                    ),
                                  ],
                                ),
                                Row(
                                  children: [
                                    Container(
                                      width: 12,
                                      height: 12,
                                      decoration: BoxDecoration(
                                        color: isConnected ? Colors.green : Colors.red,
                                        shape: BoxShape.circle,
                                      ),
                                    ),
                                    const SizedBox(width: 8),
                                    Text(
                                      isConnected ? 'Connecté' : 'Déconnecté',
                                      style: const TextStyle(color: Colors.white, fontSize: 12),
                                    ),
                                  ],
                                ),
                              ],
                            ),
                            const SizedBox(height: 24),
            
                            // Portfolio Summary
                            if (portfolio.isNotEmpty) ...[
                              Row(
                                children: [
                                  Expanded(
                                    child: SummaryCard(
                                      title: 'Valeur Portfolio',
                                      value: '\$${_calculatePortfolioValue().toStringAsFixed(2)}',
                                      icon: Icons.account_balance_wallet,
                                      color: Colors.blue,
                                    ),
                                  ),
                                  const SizedBox(width: 12),
                                  Expanded(
                                    child: SummaryCard(
                                      title: 'P&L Total',
                                      value: '\$${_calculatePortfolioPnL().toStringAsFixed(2)}',
                                      icon: _calculatePortfolioPnL() >= 0 
                                          ? Icons.trending_up 
                                          : Icons.trending_down,
                                      color: _calculatePortfolioPnL() >= 0 
                                          ? Colors.green 
                                          : Colors.red,
                                    ),
                                  ),
                                ],
                              ),
                              const SizedBox(height: 24),
                            ],
            
                            // Grille des cryptos
                            CryptoGrid(
                              cryptos: _cryptoService.cryptos,
                              selectedCrypto: selectedCrypto,
                              onCryptoSelected: (symbol) {
                                setState(() => selectedCrypto = symbol);
                              },
                            ),
                            const SizedBox(height: 24),
            
                            // Graphique
                            ChartWidget(
                              data: _cryptoService.cryptos[selectedCrypto]!.history,
                              symbol: selectedCrypto,
                            ),
                            const SizedBox(height: 24),
            
                            // Alertes et Portfolio
                            Row(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Expanded(
                                  child: AlertsSection(
                                    alerts: alerts,
                                    cryptoSymbols: _cryptoService.cryptos.keys.toList(),
                                    onAddAlert: (alert) {
                                      setState(() => alerts.add(alert));
                                    },
                                    onRemoveAlert: (alert) {
                                      setState(() => alerts.remove(alert));
                                    },
                                  ),
                                ),
                                const SizedBox(width: 12),
                                Expanded(
                                  child: PortfolioSection(
                                    portfolio: portfolio,
                                    cryptoSymbols: _cryptoService.cryptos.keys.toList(),
                                    getCryptoPrice: _cryptoService.getCryptoPrice,
                                    onAddItem: (item) {
                                      setState(() => portfolio.add(item));
                                    },
                                    onRemoveItem: (item) {
                                      setState(() => portfolio.remove(item));
                                    },
                                  ),
                                ),
                              ],
                            ),
                          ],
                        ),
                      ),
                    ),
                  ),
                );
              }
            }
            services/crypto_service.dart
            // Importation des bibliothèques nécessaires
            import 'dart:async'; // Pour utiliser Timer, qui permet de répéter des actions périodiquement
            import 'dart:math';  // Pour générer des nombres aléatoires
            
            import 'dart:convert';
            import 'package:http/http.dart' as http;
            import '../models/crypto_data.dart';
            import '../models/price_point.dart';
            import '../models/alert.dart';
            
            class CryptoService {
              final Map cryptos = {
                'BTC': CryptoData(price: 0, change: 0, history: []),
                'ETH': CryptoData(price: 0, change: 0, history: []),
                'BNB': CryptoData(price: 0, change: 0, history: []),
                'SOL': CryptoData(price: 0, change: 0, history: []),
              };
            
              Timer? _timer;
              Function(String, double)? onAlertTriggered;
            
              /// Récupère les prix réels depuis CoinGecko
              Future fetchRealPrices() async {
                final url = Uri.parse(
                  'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum,binancecoin,solana&vs_currencies=usd'
                );
            
                final response = await http.get(url);
                if (response.statusCode == 200) {
                  final data = jsonDecode(response.body);
                  cryptos['BTC']!.price = (data['bitcoin']['usd'] as num).toDouble();
                  cryptos['ETH']!.price = (data['ethereum']['usd'] as num).toDouble();
                  cryptos['BNB']!.price = (data['binancecoin']['usd'] as num).toDouble();
                  cryptos['SOL']!.price = (data['solana']['usd'] as num).toDouble();
                }
              }
            
              void startPriceUpdates(Function onUpdate, List alerts) {
                // D'abord, récupérer les prix réels
                fetchRealPrices().then((_) {
                  // Puis démarrer les mises à jour aléatoires comme avant
                  _timer = Timer.periodic(const Duration(seconds: 2), (timer) {
                    cryptos.forEach((symbol, data) {
                      final randomChange = (Random().nextDouble() - 0.5) * 0.02;
                      final oldPrice = data.price;
                      final newPrice = oldPrice * (1 + randomChange);
                      final change = ((newPrice - oldPrice) / oldPrice) * 100;
            
                      data.price = newPrice;
                      data.change = change.isNaN ? (Random().nextDouble() - 0.5) * 5 : change;
                      data.history.add(PricePoint(DateTime.now(), newPrice));
            
                      if (data.history.length > 20) data.history.removeAt(0);
            
                      _checkAlerts(symbol, newPrice, alerts);
                    });
            
                    onUpdate();
                  });
                });
              }
            
              /// Vérifie les alertes pour une crypto donnée
              void _checkAlerts(String symbol, double price, List alerts) {
                for (var alert in alerts) {
                  if (alert.symbol == symbol && !alert.triggered) { // Si l'alerte correspond à la crypto et n'est pas déjà déclenchée
                    if ((alert.type == 'above' && price >= alert.price) ||
                        (alert.type == 'below' && price <= alert.price)) {
                      alert.triggered = true; // Marque l'alerte comme déclenchée
                      onAlertTriggered?.call(symbol, alert.price); // Appel du callback pour notifier
                    }
                  }
                }
              }
            
              /// Arrête la mise à jour automatique des prix
              void stopPriceUpdates() {
                _timer?.cancel();
              }
            
              /// Retourne le prix actuel d'une crypto
              double getCryptoPrice(String symbol) {
                return cryptos[symbol]?.price ?? 0; // Retourne 0 si la crypto n'existe pas
              }
            
              /// Libère les ressources en annulant le timer
              void dispose() {
                _timer?.cancel();
              }
            }
            
            widgets/alerts_section.dart
            import 'package:flutter/material.dart';
            import '../models/alert.dart';
            
            class AlertsSection extends StatefulWidget {
              final List alerts;
              final List cryptoSymbols;
              final Function(Alert) onAddAlert;
              final Function(Alert) onRemoveAlert;
            
              const AlertsSection({
                Key? key,
                required this.alerts,
                required this.cryptoSymbols,
                required this.onAddAlert,
                required this.onRemoveAlert,
              }) : super(key: key);
            
              @override
              State createState() => _AlertsSectionState();
            }
            
            class _AlertsSectionState extends State {
              void _showAddAlertDialog() {
                String selectedSymbol = widget.cryptoSymbols.first;
                String alertType = 'above';
                final priceController = TextEditingController();
            
                showDialog(
                  context: context,
                  builder: (context) => StatefulBuilder(
                    builder: (context, setDialogState) => AlertDialog(
                      backgroundColor: const Color(0xFF1E293B),
                      title: const Text('Nouvelle Alerte', style: TextStyle(color: Colors.white)),
                      content: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          DropdownButtonFormField(
                            value: selectedSymbol,
                            dropdownColor: const Color(0xFF334155),
                            style: const TextStyle(color: Colors.white),
                            decoration: const InputDecoration(
                              labelText: 'Crypto',
                              labelStyle: TextStyle(color: Colors.purple),
                            ),
                            items: widget.cryptoSymbols.map((symbol) {
                              return DropdownMenuItem(value: symbol, child: Text(symbol));
                            }).toList(),
                            onChanged: (value) {
                              setDialogState(() => selectedSymbol = value!);
                            },
                          ),
                          const SizedBox(height: 16),
                          DropdownButtonFormField(
                            value: alertType,
                            dropdownColor: const Color(0xFF334155),
                            style: const TextStyle(color: Colors.white),
                            decoration: const InputDecoration(
                              labelText: 'Type',
                              labelStyle: TextStyle(color: Colors.purple),
                            ),
                            items: const [
                              DropdownMenuItem(value: 'above', child: Text('Au-dessus de')),
                              DropdownMenuItem(value: 'below', child: Text('En-dessous de')),
                            ],
                            onChanged: (value) {
                              setDialogState(() => alertType = value!);
                            },
                          ),
                          const SizedBox(height: 16),
                          TextField(
                            controller: priceController,
                            keyboardType: TextInputType.number,
                            style: const TextStyle(color: Colors.white),
                            decoration: const InputDecoration(
                              labelText: 'Prix cible',
                              labelStyle: TextStyle(color: Colors.purple),
                            ),
                          ),
                        ],
                      ),
                      actions: [
                        TextButton(
                          onPressed: () => Navigator.pop(context),
                          child: const Text('Annuler'),
                        ),
                        ElevatedButton(
                          style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
                          onPressed: () {
                            final price = double.tryParse(priceController.text);
                            if (price != null) {
                              widget.onAddAlert(Alert(
                                id: DateTime.now().toString(),
                                symbol: selectedSymbol,
                                type: alertType,
                                price: price,
                              ));
                              Navigator.pop(context);
                            }
                          },
                          child: const Text('Créer'),
                        ),
                      ],
                    ),
                  ),
                );
              }
            
              @override
              Widget build(BuildContext context) {
                return Container(
                  padding: const EdgeInsets.all(16),
                  decoration: BoxDecoration(
                    color: Colors.white.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(16),
                    border: Border.all(color: Colors.white.withOpacity(0.2)),
                  ),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          const Row(
                            children: [
                              Icon(Icons.notifications, color: Colors.white, size: 20),
                              SizedBox(width: 8),
                              Text(
                                'Alertes',
                                style: TextStyle(
                                  fontSize: 18,
                                  fontWeight: FontWeight.bold,
                                  color: Colors.white,
                                ),
                              ),
                            ],
                          ),
                          IconButton(
                            icon: const Icon(Icons.add, color: Color(0xFFA78BFA)),
                            onPressed: _showAddAlertDialog,
                          ),
                        ],
                      ),
                      const SizedBox(height: 12),
                      ...widget.alerts.map((alert) => Padding(
                        padding: const EdgeInsets.only(bottom: 8),
                        child: Container(
                          padding: const EdgeInsets.all(12),
                          decoration: BoxDecoration(
                            color: Colors.white.withOpacity(0.05),
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: [
                              Expanded(
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Text(
                                      '${alert.symbol} ${alert.type == 'above' ? '≥' : '≤'} \$${alert.price.toStringAsFixed(2)}',
                                      style: const TextStyle(color: Colors.white, fontSize: 14),
                                    ),
                                    if (alert.triggered)
                                      const Text(
                                        '✓ Déclenchée',
                                        style: TextStyle(color: Colors.green, fontSize: 12),
                                      ),
                                  ],
                                ),
                              ),
                              IconButton(
                                icon: const Icon(Icons.close, color: Colors.red, size: 18),
                                onPressed: () => widget.onRemoveAlert(alert),
                              ),
                            ],
                          ),
                        ),
                      )),
                      if (widget.alerts.isEmpty)
                        const Padding(
                          padding: EdgeInsets.symmetric(vertical: 20),
                          child: Center(
                            child: Text(
                              'Aucune alerte',
                              style: TextStyle(color: Color(0xFFC4B5FD)),
                            ),
                          ),
                        ),
                    ],
                  ),
                );
              }
            }
            widgets/chart_widget.dart
            import 'package:flutter/material.dart';
            import 'package:fl_chart/fl_chart.dart';
            import '../models/price_point.dart';
            
            class ChartWidget extends StatelessWidget {
              final List data;
              final String symbol;
            
              const ChartWidget({
                Key? key,
                required this.data,
                required this.symbol,
              }) : super(key: key);
            
              @override
              Widget build(BuildContext context) {
                return Container(
                  padding: const EdgeInsets.all(16),
                  decoration: BoxDecoration(
                    color: Colors.white.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(16),
                    border: Border.all(color: Colors.white.withOpacity(0.2)),
                  ),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Évolution $symbol',
                        style: const TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.bold,
                          color: Colors.white,
                        ),
                      ),
                      const SizedBox(height: 16),
                      SizedBox(
                        height: 200,
                        child: _buildChart(),
                      ),
                    ],
                  ),
                );
              }
            
              Widget _buildChart() {
                if (data.isEmpty) {
                  return const Center(
                    child: Text(
                      'Pas de données',
                      style: TextStyle(color: Colors.white),
                    ),
                  );
                }
            
                return LineChart(
                  LineChartData(
                    gridData: FlGridData(
                      show: true,
                      drawVerticalLine: false,
                      getDrawingHorizontalLine: (value) => FlLine(
                        color: Colors.white.withOpacity(0.1),
                        strokeWidth: 1,
                      ),
                    ),
                    titlesData: FlTitlesData(show: false),
                    borderData: FlBorderData(show: false),
                    lineBarsData: [
                      LineChartBarData(
                        spots: data.asMap().entries.map((e) {
                          return FlSpot(e.key.toDouble(), e.value.price);
                        }).toList(),
                        isCurved: true,
                        color: const Color(0xFFA78BFA),
                        barWidth: 3,
                        dotData: FlDotData(show: false),
                        belowBarData: BarAreaData(
                          show: true,
                          color: const Color(0xFFA78BFA).withOpacity(0.1),
                        ),
                      ),
                    ],
                  ),
                );
              }
            }
            widgets/crypto_grid.dart
            import 'package:flutter/material.dart';
            import '../models/crypto_data.dart';
            
            class CryptoGrid extends StatelessWidget {
              final Map cryptos;
              final String selectedCrypto;
              final Function(String) onCryptoSelected;
            
              const CryptoGrid({
                Key? key,
                required this.cryptos,
                required this.selectedCrypto,
                required this.onCryptoSelected,
              }) : super(key: key);
            
              @override
              Widget build(BuildContext context) {
                return GridView.builder(
                  shrinkWrap: true,
                  physics: const NeverScrollableScrollPhysics(),
                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    childAspectRatio: 1.3,
                    crossAxisSpacing: 12,
                    mainAxisSpacing: 12,
                  ),
                  itemCount: cryptos.length,
                  itemBuilder: (context, index) {
                    final symbol = cryptos.keys.elementAt(index);
                    final data = cryptos[symbol]!;
                    final isSelected = selectedCrypto == symbol;
            
                    return GestureDetector(
                      onTap: () => onCryptoSelected(symbol),
                      child: Container(
                        padding: const EdgeInsets.all(16),
                        decoration: BoxDecoration(
                          color: Colors.white.withOpacity(0.1),
                          borderRadius: BorderRadius.circular(16),
                          border: Border.all(
                            color: isSelected ? const Color(0xFFA78BFA) : Colors.white.withOpacity(0.2),
                            width: 2,
                          ),
                        ),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            Row(
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
                              children: [
                                Text(
                                  symbol,
                                  style: const TextStyle(
                                    fontSize: 20,
                                    fontWeight: FontWeight.bold,
                                    color: Colors.white,
                                  ),
                                ),
                                Icon(
                                  data.change >= 0 ? Icons.trending_up : Icons.trending_down,
                                  color: data.change >= 0 ? Colors.green : Colors.red,
                                  size: 24,
                                ),
                              ],
                            ),
                            Text(
                              '\$${data.price.toStringAsFixed(2)}',
                              style: const TextStyle(
                                fontSize: 24,
                                fontWeight: FontWeight.bold,
                                color: Colors.white,
                              ),
                            ),
                            Text(
                              '${data.change >= 0 ? '+' : ''}${data.change.toStringAsFixed(2)}%',
                              style: TextStyle(
                                fontSize: 16,
                                fontWeight: FontWeight.bold,
                                color: data.change >= 0 ? Colors.green : Colors.red,
                              ),
                            ),
                          ],
                        ),
                      ),
                    );
                  },
                );
              }
            }
            widgets/portfolio_section.dart
            import 'package:flutter/material.dart';
            import '../models/portfolio_item.dart';
            
            class PortfolioSection extends StatefulWidget {
              final List portfolio;
              final List cryptoSymbols;
              final Function(String) getCryptoPrice;
              final Function(PortfolioItem) onAddItem;
              final Function(PortfolioItem) onRemoveItem;
            
              const PortfolioSection({
                Key? key,
                required this.portfolio,
                required this.cryptoSymbols,
                required this.getCryptoPrice,
                required this.onAddItem,
                required this.onRemoveItem,
              }) : super(key: key);
            
              @override
              State createState() => _PortfolioSectionState();
            }
            
            class _PortfolioSectionState extends State {
              void _showAddPortfolioDialog() {
                String selectedSymbol = widget.cryptoSymbols.first;
                final amountController = TextEditingController();
                final priceController = TextEditingController();
            
                showDialog(
                  context: context,
                  builder: (context) => StatefulBuilder(
                    builder: (context, setDialogState) => AlertDialog(
                      backgroundColor: const Color(0xFF1E293B),
                      title: const Text('Ajouter au Portfolio', style: TextStyle(color: Colors.white)),
                      content: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          DropdownButtonFormField(
                            value: selectedSymbol,
                            dropdownColor: const Color(0xFF334155),
                            style: const TextStyle(color: Colors.white),
                            decoration: const InputDecoration(
                              labelText: 'Crypto',
                              labelStyle: TextStyle(color: Colors.purple),
                            ),
                            items: widget.cryptoSymbols.map((symbol) {
                              return DropdownMenuItem(value: symbol, child: Text(symbol));
                            }).toList(),
                            onChanged: (value) {
                              setDialogState(() => selectedSymbol = value!);
                            },
                          ),
                          const SizedBox(height: 16),
                          TextField(
                            controller: amountController,
                            keyboardType: TextInputType.number,
                            style: const TextStyle(color: Colors.white),
                            decoration: const InputDecoration(
                              labelText: 'Quantité',
                              labelStyle: TextStyle(color: Colors.purple),
                            ),
                          ),
                          const SizedBox(height: 16),
                          TextField(
                            controller: priceController,
                            keyboardType: TextInputType.number,
                            style: const TextStyle(color: Colors.white),
                            decoration: const InputDecoration(
                              labelText: "Prix d'achat",
                              labelStyle: TextStyle(color: Colors.purple),
                            ),
                          ),
                        ],
                      ),
                      actions: [
                        TextButton(
                          onPressed: () => Navigator.pop(context),
                          child: const Text('Annuler'),
                        ),
                        ElevatedButton(
                          style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
                          onPressed: () {
                            final amount = double.tryParse(amountController.text);
                            final price = double.tryParse(priceController.text);
                            if (amount != null && price != null) {
                              widget.onAddItem(PortfolioItem(
                                id: DateTime.now().toString(),
                                symbol: selectedSymbol,
                                amount: amount,
                                buyPrice: price,
                              ));
                              Navigator.pop(context);
                            }
                          },
                          child: const Text('Ajouter'),
                        ),
                      ],
                    ),
                  ),
                );
              }
            
              @override
              Widget build(BuildContext context) {
                return Container(
                  padding: const EdgeInsets.all(16),
                  decoration: BoxDecoration(
                    color: Colors.white.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(16),
                    border: Border.all(color: Colors.white.withOpacity(0.2)),
                  ),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          const Row(
                            children: [
                              Icon(Icons.account_balance_wallet, color: Colors.white, size: 20),
                              SizedBox(width: 8),
                              Text(
                                'Portfolio',
                                style: TextStyle(
                                  fontSize: 18,
                                  fontWeight: FontWeight.bold,
                                  color: Colors.white,
                                ),
                              ),
                            ],
                          ),
                          IconButton(
                            icon: const Icon(Icons.add, color: Color(0xFFA78BFA)),
                            onPressed: _showAddPortfolioDialog,
                          ),
                        ],
                      ),
                      const SizedBox(height: 12),
                      ...widget.portfolio.map((item) {
                        final currentPrice = widget.getCryptoPrice(item.symbol);
                        final value = item.amount * currentPrice;
                        final pnl = (currentPrice - item.buyPrice) * item.amount;
                        final pnlPercent = ((currentPrice - item.buyPrice) / item.buyPrice) * 100;
            
                        return Padding(
                          padding: const EdgeInsets.only(bottom: 8),
                          child: Container(
                            padding: const EdgeInsets.all(12),
                            decoration: BoxDecoration(
                              color: Colors.white.withOpacity(0.05),
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Row(
                                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                                  children: [
                                    Text(
                                      item.symbol,
                                      style: const TextStyle(
                                        color: Colors.white,
                                        fontWeight: FontWeight.bold,
                                      ),
                                    ),
                                    IconButton(
                                      icon: const Icon(Icons.close, color: Colors.red, size: 18),
                                      padding: EdgeInsets.zero,
                                      constraints: const BoxConstraints(),
                                      onPressed: () => widget.onRemoveItem(item),
                                    ),
                                  ],
                                ),
                                Text(
                                  'Quantité: ${item.amount}',
                                  style: const TextStyle(color: Color(0xFFC4B5FD), fontSize: 12),
                                ),
                                Text(
                                  'Valeur: \$${value.toStringAsFixed(2)}',
                                  style: const TextStyle(color: Color(0xFFC4B5FD), fontSize: 12),
                                ),
                                Text(
                                  'P&L: \$${pnl.toStringAsFixed(2)} (${pnl >= 0 ? '+' : ''}${pnlPercent.toStringAsFixed(2)}%)',
                                  style: TextStyle(
                                    color: pnl >= 0 ? Colors.green : Colors.red,
                                    fontSize: 12,
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                              ],
                            ),
                          ),
                        );
                      }),
                      if (widget.portfolio.isEmpty)
                        const Padding(
                          padding: EdgeInsets.symmetric(vertical: 20),
                          child: Center(
                            child: Text(
                              'Portfolio vide',
                              style: TextStyle(color: Color(0xFFC4B5FD)),
                            ),
                          ),
                        ),
                    ],
                  ),
                );
              }
            }
            widgets/summary_card.dart
            import 'package:flutter/material.dart';
            
            class SummaryCard extends StatelessWidget {
              final String title;
              final String value;
              final IconData icon;
              final Color color;
            
              const SummaryCard({
                Key? key,
                required this.title,
                required this.value,
                required this.icon,
                required this.color,
              }) : super(key: key);
            
              @override
              Widget build(BuildContext context) {
                return Container(
                  padding: const EdgeInsets.all(16),
                  decoration: BoxDecoration(
                    color: Colors.white.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(16),
                    border: Border.all(color: Colors.white.withOpacity(0.2)),
                  ),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Icon(icon, color: color, size: 20),
                          const SizedBox(width: 8),
                          Expanded(
                            child: Text(
                              title,
                              style: const TextStyle(color: Color(0xFFC4B5FD), fontSize: 12),
                            ),
                          ),
                        ],
                      ),
                      const SizedBox(height: 8),
                      Text(
                        value,
                        style: TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.bold,
                          color: color,
                        ),
                      ),
                    ],
                  ),
                );
              }
            }
            main.dart
            import 'package:flutter/material.dart';
            import 'screens/crypto_dashboard.dart';
            
            void main() {
              runApp(const CryptoApp());
            }
            
            class CryptoApp extends StatelessWidget {
              const CryptoApp({Key? key}) : super(key: key);
            
              @override
              Widget build(BuildContext context) {
                return MaterialApp(
                  title: 'Crypto Dashboard',
                  theme: ThemeData.dark().copyWith(
                    scaffoldBackgroundColor: const Color(0xFF0F172A),
                    primaryColor: const Color(0xFFA78BFA),
                  ),
                  home: const CryptoDashboard(),
                );
              }
            }
    • Exercice 3: Jeu multi-joueur simple
      • Créez un jeu multi-joueur simple utilisant WebSockets pour la synchronisation.
      • Fonctionnalités:
        • Connexion à un serveur de jeu WebSocket
        • Salles de jeu multi-joueurs
        • Synchronisation en temps réel des positions des joueurs
        • Chat intégré pour les joueurs
  11. Ressources pour s'entraîner

    • WebSocket Echo Server: ws://echo.websocket.org
    • Cryptocurrency WebSocket APIs: Binance, Coinbase, Kraken
    • WebSocket Test Servers: https://www.piesocket.com/websocket-tester