Objectif
API WebSockets
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.
Cas d’utilisation
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
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- WebSocket (Full-Duplex)
- Comparaison détaillée
// Connexion initiale client -> HTTP Upgrade -> serveur client <- HTTP 101 Switching Protocols <- serveur // Communication continue client <-> Messages bidirectionnels <-> serveurAspect HTTP WebSocket Modèle de communication Request-Response Full-Duplex Latence Élevée (nouvelle connexion par requête) Faible (connexion persistante) Overhead Headers HTTP pour chaque requête Headers minimaux après handshake Usage optimal Données statiques, API REST Données temps réel, streaming Support serveur Universel Nécessite un serveur WebSocket Comment fonctionne le WebSocket ?
-
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.

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

-
-
Exemple de Service WebSocket en Ligne
- 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(); } } - Connexion : Votre client (par exemple, un script JavaScript dans votre navigateur ou un outil de test) établit une connexion à
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. -
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 :
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
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
-
- 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 :
Implémentation des WebSockets en Flutter
- Flutter utilise le package
web_socket_channelpour 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.-
É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
- Commencez par créer votre projet Flutter, puis ajoutez la dépendance dans le fichier
É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(); }); }
-
É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(Mapdata) { 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; }
-
É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(); }
-
- Flutter utilise le package
Gestion d'état avec les WebSockets
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.
- Lors d’une connexion WebSocket :
- É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(); } }
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(); } }
- Concept
Applications pratiques
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(); } }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
- ? Structure Frontend (Flutter)
- ? Vue d'ensemble
- 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(Mapjson) { 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 ValueChangeddashboard/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 StatecreateState() => _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.');- Description de l'Application
Bonnes pratiques et optimisation
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, }); Futureconnect() 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(); } } 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(Mapmessage) { 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 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, ); } }
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
- 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; Listhistory; 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 StatecreateState() => _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 Mapcryptos = { '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 Listalerts; 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 Listdata; 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 Mapcryptos; 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 Listportfolio; 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(), ); } } - Architecture de l'Application
Solution
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
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
- WebSocket Echo Server: