WebSockets et Communication en Temps Réel avec Flutter
[sommaire]
WebSockets et Communication en Temps Réel avec Flutter
-
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 ».
-
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)
- WebSocket (Full-Duplex)
- Comparaison détaillée
-
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 !".
-
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 :
- La réponse du serveur est la suivante :
-
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:
- 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 -
É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.
-
É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.
-
É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
-
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.
- É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
-
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.)
-
Applications pratiques
-
Application de chat en temps réel
-
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)
- 📁 Structure Backend (Node.js)
- Frontend : Application Flutter (client WebSocket)
- Backend : Serveur Node.js avec WebSocket (serveur de données)
-
Bonnes pratiques et optimisation
-
Gestion de la reconnexion
-
Compression des messages
-
Sécurisation des connexions
-
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
-
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
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.
// Modèle traditionnel
client -> GET /data -> serveur
client <- Response <- serveur
// Nouvelle requête nécessaire pour obtenir des updates
// Connexion initiale
client -> HTTP Upgrade -> serveur
client <- HTTP 101 Switching Protocols <- serveur
// Communication continue
client <-> Messages bidirectionnels <-> serveur
| Aspect | 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 |


| 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. |
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();
}
}
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
HTTP/1.1 101 Switching Protocols
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: superchat
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
# ... autres dépendances
web_socket_channel: ^3.0.2
# pubspec.yaml
dependencies:
web_socket_channel: ^3.0.2
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();
});
}
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;
}
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();
}
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();
}
}
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();
}
}
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();
}
}
-
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
-
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
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
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(),
);
}
}
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.');
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();
}
}
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
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,
);
}
}
Solution
-
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(),
);
}
}
