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

Application de chat en temps réel

Un cours complet pour maîtriser Application de chat en temps réel.

0 chapitres 0 QCM Accès gratuit
  1. Cahier des charges du projet

    1. Contexte
      • Une startup souhaite développer une application mobile de messagerie instantanée destinée aux apprenants et formateurs afin de faciliter la communication en temps réel.
      • L’application doit être réalisée avec Flutter pour assurer une compatibilité Android et iOS, et doit reposer sur une architecture client–serveur avec WebSockets.
      • Le projet doit permettre aux utilisateurs d’échanger des messages texte en temps réel, de consulter l’historique des conversations et de gérer leur statut de connexion.
    2. Principaux objectifs
      • Messagerie instantanée bidirectionnelle (faible latence).
      • Persistance de l’historique des messages.
      • Connexions sécurisées et authentifiées.
      • Bon comportement en cas de perte de connexion (reconnexion automatique, file d’envoi locale).
      • Extensibilité (conversations de groupe, pièces jointes, emojis, etc.).
    3. Fonctionnalités attendues
      • Authentification des utilisateurs
        • Inscription (username, email, mot de passe).
        • Connexion sécurisée avec JWT.
      • Messagerie
        • Envoi et réception de messages en temps réel.
        • Affichage de l’historique des messages (stockage en base + cache local avec sqflite).
        • Statut des messages : envoyé, délivré, lu.
      • Indicateurs
        • Présence en ligne / hors ligne.
        • Indicateur « en train d’écrire ».
      • Interface utilisateur (Flutter)
        • Page de connexion / inscription.
        • Page de liste de conversations.
        • Écran de discussion (ChatScreen avec ListView.builder).
      • Gestion des erreurs
        • Déconnexion automatique en cas d’inactivité ou d’expiration du token.
        • Tentative de reconnexion automatique en cas de perte de réseau.
    4. Exigences techniques
      • Frontend (client Flutter)
        • Framework : Flutter (Android & iOS).
        • Packages recommandés :
          • web_socket_channel (connexion WebSocket).
          • provider / riverpod / bloc (gestion d’état).
          • shared_preferences (stockage léger des préférences utilisateur).
          • sqflite (base de données locale pour messages offline).
        • Composants :
          • ChatScreen (interface principale de chat).
          • Service WebSocket réutilisable (classe ChatWebSocketService).
          • Stockage local pour cache + file d’envoi offline.
      • Backend (serveur)
        • WebSocket endpoint (exemple : wss://api.messagerie.com/ws/chat).
        • Authentification via JWT.
        • Base de données SQL (PostgreSQL) ou NoSQL (MongoDB).
        • API REST complémentaire (authentification, récupération historique).
    5. Modèle de données
      • Table users
        • id (uuid)
        • username
        • email
        • password_hash
        • avatar_url
        • last_seen
      • Table messages
        • id
        • sender_id
        • recipient_id
        • text
        • status (sent/delivered/read)
        • timestamp
    6. Protocole WebSocket (Flutter)
      • Format général JSON :
      {
        "type": "message.send",
        "payload": {
          "id": "12345",
          "sender": "alice",
          "recipient": "bob",
          "text": "Salut",
          "timestamp": "2025-09-16T08:00:00Z"
        }
      }
    7. Types d’événements :
      • message.send — client → serveur.
      • message.receive — serveur → client.
      • message.ack — accusé de réception.
      • typing.start / typing.stop — indicateur de saisie.
      • presence.update — état de connexion.
    8. Comportements côté client (Flutter)
      • Connexion via WebSocket + JWT.
      • Optimistic UI (message affiché avant ACK serveur).
      • Reconnexion automatique (exponential backoff).
      • Stockage offline avec sqflite.
    9. User stories
      • US-01 : Connexion utilisateur (login/logout).
      • US-02 : Envoyer un message en temps réel.
      • US-03 : Lire l’historique d’une conversation.
      • US-04 : Voir l’indicateur de saisie.
    10. Tests & qualité
      • Tests unitaires (modèle ChatMessage, service WebSocket).
      • Tests d’intégration (flux complet login → envoi → réception message).
      • Tests de charge (simuler plusieurs connexions).
      • Tests de sécurité (XSS, injection, validation token).
    11. Livrables
      • Code source Flutter (frontend).
      • Code backend (WebSocket + API REST).
      • Scripts de déploiement (Docker, CI/CD).
      • Plan de tests et documentation technique.
    12. Recommandations finales
      • Utiliser wss:// en production (TLS obligatoire).
      • Ne pas exposer les tokens en clair.
      • Utiliser Redis pub/sub si plusieurs serveurs backend.
      • Garder un protocole JSON simple et extensible.
  2. Réalisation du projet

    1. Structure du projet
      
      lib/
      ├── main.dart
      ├── core/
      │   ├── constants.dart
      │   ├── services/
      │   │   ├── auth_service.dart
      │   │   ├── socket_service.dart
      │   │   ├── message_service.dart
      │   │   └── database_service.dart
      │   ├── models/
      │   │   ├── user_model.dart
      │   │   ├── message_model.dart
      │   │   └── conversation_model.dart
      │   └── utils/
      │       ├── validators.dart
      │       └── helpers.dart
      ├── providers/
      │   ├── auth_provider.dart
      │   ├── chat_provider.dart
      │   └── connection_provider.dart
      ├── ui/
      │   ├── widgets/
      │   │   ├── message_bubble.dart
      │   │   ├── typing_indicator.dart
      │   │   ├── online_indicator.dart
      │   │   └── custom_textfield.dart
      │   ├── screens/
      │   │   ├── auth/
      │   │   │   ├── login_screen.dart
      │   │   │   └── register_screen.dart
      │   │   ├── conversations_screen.dart
      │   │   ├── chat_screen.dart
      │   │   └── splash_screen.dart
      │   └── theme.dart
      └── data/
          ├── repositories/
          │   ├── auth_repository.dart
          │   └── message_repository.dart
          └── local/
              └── local_database.dart
      
    2. Configuration initiale
      1. pubspec.yaml

          
          name: realtime_chat
          description: Application de messagerie instantanée en temps réel
          version: 1.0.0+1
          environment:
            sdk: ">=3.0.0 <4.0.0"
          dependencies:
            flutter:
              sdk: flutter
            cupertino_icons: ^1.0.2
            web_socket_channel: ^2.4.0
            provider: ^6.0.5
            shared_preferences: ^2.2.2
            sqflite: ^2.3.0
            path: ^1.8.3
            intl: ^0.18.1
            crypto: ^3.0.3
            jwt_decoder: ^2.0.1
            connectivity_plus: ^5.0.1
            flutter_secure_storage: ^8.0.0
            cached_network_image: ^3.3.0
          
          dev_dependencies:
            flutter_test:
              sdk: flutter
            flutter_lints: ^2.0.0
          
          flutter:
            uses-material-design: true
            assets:
              - assets/images/
          
    3. Modèles de données
      1. user_model.dart

          
          class User {
            final String id;
            final String username;
            final String email;
            final String? avatarUrl;
            final DateTime lastSeen;
            final bool isOnline;
          
            User({
              required this.id,
              required this.username,
              required this.email,
              this.avatarUrl,
              required this.lastSeen,
              required this.isOnline,
            });
          
            factory User.fromJson(Map json) {
              return User(
                id: json['id'],
                username: json['username'],
                email: json['email'],
                avatarUrl: json['avatarUrl'],
                lastSeen: DateTime.parse(json['lastSeen']),
                isOnline: json['isOnline'],
              );
            }
          
            Map toJson() {
              return {
                'id': id,
                'username': username,
                'email': email,
                'avatarUrl': avatarUrl,
                'lastSeen': lastSeen.toIso8601String(),
                'isOnline': isOnline,
              };
            }
          }
          
      2. message_model.dart

          
          enum MessageStatus { sent, delivered, read }
          enum MessageType { text, image, file }
          
          class Message {
            final String id;
            final String conversationId;
            final String senderId;
            final String text;
            final MessageType type;
            final MessageStatus status;
            final DateTime timestamp;
            final Map? metadata;
          
            Message({
              required this.id,
              required this.conversationId,
              required this.senderId,
              required this.text,
              this.type = MessageType.text,
              this.status = MessageStatus.sent,
              required this.timestamp,
              this.metadata,
            });
          
            factory Message.fromJson(Map json) {
              return Message(
                id: json['id'],
                conversationId: json['conversationId'],
                senderId: json['senderId'],
                text: json['text'],
                type: MessageType.values.firstWhere(
                  (e) => e.toString() == 'MessageType.${json['type']}',
                  orElse: () => MessageType.text,
                ),
                status: MessageStatus.values.firstWhere(
                  (e) => e.toString() == 'MessageStatus.${json['status']}',
                  orElse: () => MessageStatus.sent,
                ),
                timestamp: DateTime.parse(json['timestamp']),
                metadata: json['metadata'],
              );
            }
          
            Map toJson() {
              return {
                'id': id,
                'conversationId': conversationId,
                'senderId': senderId,
                'text': text,
                'type': type.toString().split('.').last,
                'status': status.toString().split('.').last,
                'timestamp': timestamp.toIso8601String(),
                'metadata': metadata,
              };
            }
          
            Message copyWith({
              MessageStatus? status,
            }) {
              return Message(
                id: id,
                conversationId: conversationId,
                senderId: senderId,
                text: text,
                type: type,
                status: status ?? this.status,
                timestamp: timestamp,
                metadata: metadata,
              );
            }
          }
          
      3. conversation_model.dart

          
          enum ConversationType { private, group }
          
          class Conversation {
            final String id;
            final ConversationType type;
            final String name;
            final String? avatarUrl;
            final List participants;
            final Message? lastMessage;
            final int unreadCount;
            final DateTime createdAt;
          
            Conversation({
              required this.id,
              required this.type,
              required this.name,
              this.avatarUrl,
              required this.participants,
              this.lastMessage,
              this.unreadCount = 0,
              required this.createdAt,
            });
          
            factory Conversation.fromJson(Map json) {
              return Conversation(
                id: json['id'],
                type: ConversationType.values.firstWhere(
                  (e) => e.toString() == 'ConversationType.${json['type']}',
                  orElse: () => ConversationType.private,
                ),
                name: json['name'],
                avatarUrl: json['avatarUrl'],
                participants: List.from(json['participants']),
                lastMessage: json['lastMessage'] != null
                    ? Message.fromJson(json['lastMessage'])
                    : null,
                unreadCount: json['unreadCount'] ?? 0,
                createdAt: DateTime.parse(json['createdAt']),
              );
            }
          
            Map toJson() {
              return {
                'id': id,
                'type': type.toString().split('.').last,
                'name': name,
                'avatarUrl': avatarUrl,
                'participants': participants,
                'lastMessage': lastMessage?.toJson(),
                'unreadCount': unreadCount,
                'createdAt': createdAt.toIso8601String(),
              };
            }
          
            Conversation copyWith({
              Message? lastMessage,
              int? unreadCount,
            }) {
              return Conversation(
                id: id,
                type: type,
                name: name,
                avatarUrl: avatarUrl,
                participants: participants,
                lastMessage: lastMessage ?? this.lastMessage,
                unreadCount: unreadCount ?? this.unreadCount,
                createdAt: createdAt,
              );
            }
          }
          
          
    4. Services et gestion d'état
      1. socket_service.dart

          
          import 'package:web_socket_channel/web_socket_channel.dart';
          import 'package:web_socket_channel/io.dart';
          import 'dart:convert';
          
          class SocketService {
            WebSocketChannel? _channel;
            String? _token;
            bool _isConnected = false;
            Function(Map)? onMessageReceived;
            Function()? onConnected;
            Function()? onDisconnected;
          
            static final SocketService _instance = SocketService._internal();
            factory SocketService() => _instance;
            SocketService._internal();
          
            void connect(String url, String token) {
              _token = token;
              try {
                _channel = IOWebSocketChannel.connect(
                  '$url?token=$token',
                  headers: {'Authorization': 'Bearer $token'},
                );
          
                _channel!.stream.listen(
                  (data) {
                    final message = json.decode(data);
                    if (onMessageReceived != null) {
                      onMessageReceived!(message);
                    }
                  },
                  onDone: () {
                    _isConnected = false;
                    if (onDisconnected != null) onDisconnected!();
                  },
                  onError: (error) {
                    _isConnected = false;
                    if (onDisconnected != null) onDisconnected!();
                  },
                );
          
                _isConnected = true;
                if (onConnected != null) onConnected!();
              } catch (e) {
                print('WebSocket connection error: $e');
              }
            }
          
            void disconnect() {
              _channel?.sink.close();
              _isConnected = false;
            }
          
            void sendMessage(Map message) {
              if (_isConnected && _channel != null) {
                _channel!.sink.add(json.encode(message));
              }
            }
          
            bool get isConnected => _isConnected;
          
            void setMessageHandler(Function(Map) handler) {
              onMessageReceived = handler;
            }
          }
          
      2. auth_service.dart

          
          import 'dart:convert';
          import 'package:http/http.dart' as http;
          import 'package:shared_preferences/shared_preferences.dart';
          import 'package:flutter_secure_storage/flutter_secure_storage.dart';
          
          class AuthService {
            static const String baseUrl = 'https://api.example.com';
            final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
          
            Future> login(String email, String password) async {
              final response = await http.post(
                Uri.parse('$baseUrl/api/auth/login'),
                headers: {'Content-Type': 'application/json'},
                body: json.encode({
                  'email': email,
                  'password': password,
                }),
              );
          
              if (response.statusCode == 200) {
                final data = json.decode(response.body);
                await _secureStorage.write(key: 'access_token', value: data['accessToken']);
                await _secureStorage.write(key: 'refresh_token', value: data['refreshToken']);
                
                // Store user data in shared preferences
                final prefs = await SharedPreferences.getInstance();
                await prefs.setString('user', json.encode(data['user']));
                
                return {'success': true, 'user': data['user']};
              } else {
                return {'success': false, 'error': 'Invalid credentials'};
              }
            }
          
            Future> register(String username, String email, String password) async {
              final response = await http.post(
                Uri.parse('$baseUrl/api/auth/register'),
                headers: {'Content-Type': 'application/json'},
                body: json.encode({
                  'username': username,
                  'email': email,
                  'password': password,
                }),
              );
          
              if (response.statusCode == 201) {
                final data = json.decode(response.body);
                await _secureStorage.write(key: 'access_token', value: data['accessToken']);
                await _secureStorage.write(key: 'refresh_token', value: data['refreshToken']);
                
                final prefs = await SharedPreferences.getInstance();
                await prefs.setString('user', json.encode(data['user']));
                
                return {'success': true, 'user': data['user']};
              } else {
                return {'success': false, 'error': 'Registration failed'};
              }
            }
          
            Future logout() async {
              final prefs = await SharedPreferences.getInstance();
              await prefs.remove('user');
              await _secureStorage.delete(key: 'access_token');
              await _secureStorage.delete(key: 'refresh_token');
            }
          
            Future getAccessToken() async {
              return await _secureStorage.read(key: 'access_token');
            }
          
            Future?> getCurrentUser() async {
              final prefs = await SharedPreferences.getInstance();
              final userString = prefs.getString('user');
              if (userString != null) {
                return json.decode(userString);
              }
              return null;
            }
          
            Future isLoggedIn() async {
              final token = await getAccessToken();
              return token != null;
            }
          }
          chat_provider.dart
          dart
          import 'package:flutter/foundation.dart';
          import '../core/models/message_model.dart';
          import '../core/models/conversation_model.dart';
          import '../core/services/socket_service.dart';
          import '../core/services/message_service.dart';
          
          class ChatProvider with ChangeNotifier {
            final SocketService _socketService;
            final MessageService _messageService;
            
            List _conversations = [];
            List _messages = [];
            Conversation? _currentConversation;
            bool _isTyping = false;
            String _typingUser = '';
          
            List get conversations => _conversations;
            List get messages => _messages;
            Conversation? get currentConversation => _currentConversation;
            bool get isTyping => _isTyping;
            String get typingUser => _typingUser;
          
            ChatProvider(this._socketService, this._messageService) {
              _socketService.setMessageHandler(_handleSocketMessage);
            }
          
            void _handleSocketMessage(Map message) {
              final type = message['type'];
              final payload = message['payload'];
          
              switch (type) {
                case 'message.receive':
                  final newMessage = Message.fromJson(payload);
                  _addMessage(newMessage);
                  break;
                case 'message.ack':
                  _updateMessageStatus(payload['id'], MessageStatus.values.firstWhere(
                    (e) => e.toString() == 'MessageStatus.${payload['status']}',
                    orElse: () => MessageStatus.delivered,
                  ));
                  break;
                case 'typing.start':
                  _setTyping(true, payload['userId']);
                  break;
                case 'typing.stop':
                  _setTyping(false, '');
                  break;
                case 'presence.update':
                  _updateUserPresence(payload['userId'], payload['isOnline']);
                  break;
              }
            }
          
            void _addMessage(Message message) {
              _messages.add(message);
              notifyListeners();
            }
          
            void _updateMessageStatus(String messageId, MessageStatus status) {
              final index = _messages.indexWhere((msg) => msg.id == messageId);
              if (index != -1) {
                _messages[index] = _messages[index].copyWith(status: status);
                notifyListeners();
              }
            }
          
            void _setTyping(bool typing, String userId) {
              _isTyping = typing;
              _typingUser = userId;
              notifyListeners();
            }
          
            void _updateUserPresence(String userId, bool isOnline) {
              // Update presence in conversations and messages
              notifyListeners();
            }
          
            Future loadConversations() async {
              _conversations = await _messageService.getConversations();
              notifyListeners();
            }
          
            Future loadMessages(String conversationId) async {
              _messages = await _messageService.getMessages(conversationId);
              notifyListeners();
            }
          
            Future sendMessage(String text) async {
              if (_currentConversation == null) return;
          
              final message = Message(
                id: DateTime.now().millisecondsSinceEpoch.toString(),
                conversationId: _currentConversation!.id,
                senderId: 'current_user_id', // Should be replaced with actual user ID
                text: text,
                timestamp: DateTime.now(),
              );
          
              // Optimistic UI update
              _addMessage(message);
          
              // Send via socket
              _socketService.sendMessage({
                'type': 'message.send',
                'payload': message.toJson(),
              });
          
              // Save to local database
              await _messageService.saveMessage(message);
            }
          
            void setCurrentConversation(Conversation conversation) {
              _currentConversation = conversation;
              loadMessages(conversation.id);
            }
          
            void startTyping() {
              _socketService.sendMessage({
                'type': 'typing.start',
                'payload': {'conversationId': _currentConversation?.id},
              });
            }
          
            void stopTyping() {
              _socketService.sendMessage({
                'type': 'typing.stop',
                'payload': {'conversationId': _currentConversation?.id},
              });
            }
          
            void markAsRead(String messageId) {
              _updateMessageStatus(messageId, MessageStatus.read);
              _socketService.sendMessage({
                'type': 'message.ack',
                'payload': {'messageId': messageId, 'status': 'read'},
              });
            }
          }
          
    5. Interface utilisateur
      1. login_screen.dart

          
          import 'package:flutter/material.dart';
          import 'package:provider/provider.dart';
          import '../../providers/auth_provider.dart';
          
          class LoginScreen extends StatefulWidget {
            const LoginScreen({super.key});
          
            @override
            State createState() => _LoginScreenState();
          }
          
          class _LoginScreenState extends State {
            final _formKey = GlobalKey();
            final _emailController = TextEditingController();
            final _passwordController = TextEditingController();
            bool _isLoading = false;
          
            void _login() async {
              if (!_formKey.currentState!.validate()) return;
          
              setState(() => _isLoading = true);
              
              final authProvider = Provider.of(context, listen: false);
              final result = await authProvider.login(
                _emailController.text,
                _passwordController.text,
              );
          
              setState(() => _isLoading = false);
          
              if (result['success'] == true) {
                Navigator.pushReplacementNamed(context, '/conversations');
              } else {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text(result['error'])),
                );
              }
            }
          
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                body: Padding(
                  padding: const EdgeInsets.all(20.0),
                  child: Form(
                    key: _formKey,
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text(
                          'Connexion',
                          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 30),
                        TextFormField(
                          controller: _emailController,
                          decoration: const InputDecoration(
                            labelText: 'Email',
                            border: OutlineInputBorder(),
                          ),
                          validator: (value) {
                            if (value == null || value.isEmpty) {
                              return 'Veuillez entrer votre email';
                            }
                            if (!value.contains('@')) {
                              return 'Email invalide';
                            }
                            return null;
                          },
                        ),
                        const SizedBox(height: 15),
                        TextFormField(
                          controller: _passwordController,
                          decoration: const InputDecoration(
                            labelText: 'Mot de passe',
                            border: OutlineInputBorder(),
                          ),
                          obscureText: true,
                          validator: (value) {
                            if (value == null || value.isEmpty) {
                              return 'Veuillez entrer votre mot de passe';
                            }
                            if (value.length < 6) {
                              return 'Le mot de passe doit contenir au moins 6 caractères';
                            }
                            return null;
                          },
                        ),
                        const SizedBox(height: 20),
                        _isLoading
                            ? const CircularProgressIndicator()
                            : ElevatedButton(
                                onPressed: _login,
                                child: const Text('Se connecter'),
                              ),
                        const SizedBox(height: 15),
                        TextButton(
                          onPressed: () {
                            Navigator.pushNamed(context, '/register');
                          },
                          child: const Text('Créer un compte'),
                        ),
                      ],
                    ),
                  ),
                ),
              );
            }
          }
          
      2. conversations_screen.dart

          
          import 'package:flutter/material.dart';
          import 'package:provider/provider.dart';
          import '../providers/chat_provider.dart';
          import '../core/models/conversation_model.dart';
          
          class ConversationsScreen extends StatefulWidget {
            const ConversationsScreen({super.key});
          
            @override
            State createState() => _ConversationsScreenState();
          }
          
          class _ConversationsScreenState extends State {
            @override
            void initState() {
              super.initState();
              WidgetsBinding.instance.addPostFrameCallback((_) {
                Provider.of(context, listen: false).loadConversations();
              });
            }
          
            @override
            Widget build(BuildContext context) {
              final chatProvider = Provider.of(context);
          
              return Scaffold(
                appBar: AppBar(
                  title: const Text('Conversations'),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.search),
                      onPressed: () {},
                    ),
                    IconButton(
                      icon: const Icon(Icons.logout),
                      onPressed: () {
                        // Implement logout
                      },
                    ),
                  ],
                ),
                body: chatProvider.conversations.isEmpty
                    ? const Center(child: Text('Aucune conversation'))
                    : ListView.builder(
                        itemCount: chatProvider.conversations.length,
                        itemBuilder: (context, index) {
                          final conversation = chatProvider.conversations[index];
                          return ConversationListItem(
                            conversation: conversation,
                            onTap: () {
                              chatProvider.setCurrentConversation(conversation);
                              Navigator.pushNamed(context, '/chat');
                            },
                          );
                        },
                      ),
                floatingActionButton: FloatingActionButton(
                  onPressed: () {
                    // Navigate to new conversation screen
                  },
                  child: const Icon(Icons.message),
                ),
              );
            }
          }
          
          class ConversationListItem extends StatelessWidget {
            final Conversation conversation;
            final VoidCallback onTap;
          
            const ConversationListItem({
              super.key,
              required this.conversation,
              required this.onTap,
            });
          
            @override
            Widget build(BuildContext context) {
              return ListTile(
                leading: CircleAvatar(
                  backgroundImage: conversation.avatarUrl != null
                      ? NetworkImage(conversation.avatarUrl!)
                      : null,
                  child: conversation.avatarUrl == null
                      ? Text(conversation.name[0])
                      : null,
                ),
                title: Text(conversation.name),
                subtitle: conversation.lastMessage != null
                    ? Text(
                        conversation.lastMessage!.text,
                        overflow: TextOverflow.ellipsis,
                      )
                    : null,
                trailing: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    if (conversation.lastMessage != null)
                      Text(
                        _formatTime(conversation.lastMessage!.timestamp),
                        style: const TextStyle(fontSize: 12),
                      ),
                    if (conversation.unreadCount > 0)
                      Container(
                        padding: const EdgeInsets.all(4),
                        decoration: const BoxDecoration(
                          color: Colors.blue,
                          shape: BoxShape.circle,
                        ),
                        child: Text(
                          conversation.unreadCount.toString(),
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 12,
                          ),
                        ),
                      ),
                  ],
                ),
                onTap: onTap,
              );
            }
          
            String _formatTime(DateTime timestamp) {
              final now = DateTime.now();
              final difference = now.difference(timestamp);
          
              if (difference.inDays > 0) {
                return '${difference.inDays}j';
              } else if (difference.inHours > 0) {
                return '${difference.inHours}h';
              } else if (difference.inMinutes > 0) {
                return '${difference.inMinutes}min';
              } else {
                return 'Maintenant';
              }
            }
          }
          
      3. chat_screen.dart

          
          import 'package:flutter/material.dart';
          import 'package:provider/provider.dart';
          import '../providers/chat_provider.dart';
          import '../widgets/message_bubble.dart';
          import '../widgets/typing_indicator.dart';
          
          class ChatScreen extends StatefulWidget {
            const ChatScreen({super.key});
          
            @override
            State createState() => _ChatScreenState();
          }
          
          class _ChatScreenState extends State {
            final _messageController = TextEditingController();
            final _scrollController = ScrollController();
            bool _isTyping = false;
          
            @override
            void initState() {
              super.initState();
              WidgetsBinding.instance.addPostFrameCallback((_) {
                _scrollToBottom();
              });
            }
          
            void _scrollToBottom() {
              if (_scrollController.hasClients) {
                _scrollController.animateTo(
                  _scrollController.position.maxScrollExtent,
                  duration: const Duration(milliseconds: 300),
                  curve: Curves.easeOut,
                );
              }
            }
          
            void _sendMessage() {
              final text = _messageController.text.trim();
              if (text.isEmpty) return;
          
              final chatProvider = Provider.of(context, listen: false);
              chatProvider.sendMessage(text);
          
              _messageController.clear();
              _scrollToBottom();
            }
          
            void _onTypingChanged(bool isTyping) {
              setState(() {
                _isTyping = isTyping;
              });
            }
          
            @override
            Widget build(BuildContext context) {
              final chatProvider = Provider.of(context);
              final currentConversation = chatProvider.currentConversation;
          
              return Scaffold(
                appBar: AppBar(
                  title: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(currentConversation?.name ?? 'Chat'),
                      if (chatProvider.isTyping)
                        const Text(
                          'en train d\'écrire...',
                          style: TextStyle(fontSize: 12),
                        )
                      else
                        const Text(
                          'En ligne',
                          style: TextStyle(fontSize: 12),
                        ),
                    ],
                  ),
                  leading: IconButton(
                    icon: const Icon(Icons.arrow_back),
                    onPressed: () {
                      Navigator.pop(context);
                    },
                  ),
                ),
                body: Column(
                  children: [
                    Expanded(
                      child: ListView.builder(
                        controller: _scrollController,
                        itemCount: chatProvider.messages.length + (chatProvider.isTyping ? 1 : 0),
                        itemBuilder: (context, index) {
                          if (index < chatProvider.messages.length) {
                            final message = chatProvider.messages[index];
                            return MessageBubble(message: message);
                          } else {
                            return const TypingIndicator();
                          }
                        },
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Row(
                        children: [
                          Expanded(
                            child: TextField(
                              controller: _messageController,
                              decoration: InputDecoration(
                                hintText: 'Tapez un message...',
                                border: OutlineInputBorder(
                                  borderRadius: BorderRadius.circular(20),
                                ),
                                contentPadding: const EdgeInsets.symmetric(horizontal: 16),
                              ),
                              onChanged: (value) {
                                if (value.isNotEmpty && !_isTyping) {
                                  chatProvider.startTyping();
                                  _onTypingChanged(true);
                                } else if (value.isEmpty && _isTyping) {
                                  chatProvider.stopTyping();
                                  _onTypingChanged(false);
                                }
                              },
                              onSubmitted: (_) => _sendMessage(),
                            ),
                          ),
                          IconButton(
                            icon: const Icon(Icons.send),
                            onPressed: _sendMessage,
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              );
            }
          
            @override
            void dispose() {
              _messageController.dispose();
              _scrollController.dispose();
              super.dispose();
            }
          }
          
      4. message_bubble.dart

          
          import 'package:flutter/material.dart';
          import '../../core/models/message_model.dart';
          
          class MessageBubble extends StatelessWidget {
            final Message message;
            final bool isMe;
          
            const MessageBubble({
              super.key,
              required this.message,
              this.isMe = false, // This should be determined based on current user
            });
          
            @override
            Widget build(BuildContext context) {
              return Align(
                alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
                child: Container(
                  margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
                  padding: const EdgeInsets.all(12),
                  decoration: BoxDecoration(
                    color: isMe ? Colors.blue : Colors.grey[300],
                    borderRadius: BorderRadius.circular(16),
                  ),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.end,
                    children: [
                      Text(
                        message.text,
                        style: TextStyle(
                          color: isMe ? Colors.white : Colors.black,
                        ),
                      ),
                      const SizedBox(height: 4),
                      Text(
                        _formatTime(message.timestamp),
                        style: TextStyle(
                          color: isMe ? Colors.white70 : Colors.grey[600],
                          fontSize: 10,
                        ),
                      ),
                    ],
                  ),
                ),
              );
            }
          
            String _formatTime(DateTime timestamp) {
              return '${timestamp.hour}:${timestamp.minute.toString().padLeft(2, '0')}';
            }
          }
          
    6. Point d'entrée principal
    7. main.dart

        
        import 'package:flutter/material.dart';
        import 'package:provider/provider.dart';
        import 'ui/screens/splash_screen.dart';
        import 'ui/screens/auth/login_screen.dart';
        import 'ui/screens/auth/register_screen.dart';
        import 'ui/screens/conversations_screen.dart';
        import 'ui/screens/chat_screen.dart';
        import 'providers/auth_provider.dart';
        import 'providers/chat_provider.dart';
        import 'core/services/auth_service.dart';
        import 'core/services/socket_service.dart';
        import 'core/services/message_service.dart';
        import 'ui/theme.dart';
        
        void main() {
          runApp(const MyApp());
        }
        
        class MyApp extends StatelessWidget {
          const MyApp({super.key});
        
          @override
          Widget build(BuildContext context) {
            return MultiProvider(
              providers: [
                ChangeNotifierProvider(
                  create: (context) => AuthProvider(AuthService()),
                ),
                ChangeNotifierProxyProvider(
                  create: (context) => ChatProvider(
                    SocketService(),
                    MessageService(),
                  ),
                  update: (context, authProvider, chatProvider) {
                    if (authProvider.isAuthenticated) {
                      // Reconnect socket with new token
                      final token = authProvider.token;
                      if (token != null) {
                        chatProvider?.connectSocket(token);
                      }
                    } else {
                      chatProvider?.disconnectSocket();
                    }
                    return chatProvider!;
                  },
                ),
              ],
              child: MaterialApp(
                title: 'Realtime Chat',
                theme: appTheme,
                initialRoute: '/splash',
                routes: {
                  '/splash': (context) => const SplashScreen(),
                  '/login': (context) => const LoginScreen(),
                  '/register': (context) => const RegisterScreen(),
                  '/conversations': (context) => const ConversationsScreen(),
                  '/chat': (context) => const ChatScreen(),
                },
              ),
            );
          }
        }
        
    8. splash_screen.dart

        
        import 'package:flutter/material.dart';
        import 'package:provider/provider.dart';
        import '../providers/auth_provider.dart';
        
        class SplashScreen extends StatefulWidget {
          const SplashScreen({super.key});
        
          @override
          State createState() => _SplashScreenState();
        }
        
        class _SplashScreenState extends State {
          @override
          void initState() {
            super.initState();
            _checkAuthStatus();
          }
        
          void _checkAuthStatus() async {
            final authProvider = Provider.of(context, listen: false);
            final isLoggedIn = await authProvider.checkAuthStatus();
        
            Future.delayed(const Duration(seconds: 2), () {
              if (isLoggedIn) {
                Navigator.pushReplacementNamed(context, '/conversations');
              } else {
                Navigator.pushReplacementNamed(context, '/login');
              }
            });
          }
        
          @override
          Widget build(BuildContext context) {
            return const Scaffold(
              body: Center(
                child: FlutterLogo(size: 100),
              ),
            );
          }
        }
        
  3. Configuration du thème
    1. theme.dart

        
        import 'package:flutter/material.dart';
        
        final ThemeData appTheme = ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
          appBarTheme: const AppBarTheme(
            elevation: 0,
            backgroundColor: Colors.white,
            iconTheme: IconThemeData(color: Colors.black),
            titleTextStyle: TextStyle(
              color: Colors.black,
              fontSize: 20,
              fontWeight: FontWeight.bold,
            ),
          ),
          inputDecorationTheme: InputDecorationTheme(
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(8),
            ),
          ),
          elevatedButtonTheme: ElevatedButtonThemeData(
            style: ElevatedButton.styleFrom(
              padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(8),
              ),
            ),
          ),
        );
        
      1. Gestion de la connexion
        1. connection_provider.dart

            
            import 'package:flutter/foundation.dart';
            import 'package:connectivity_plus/connectivity_plus.dart';
            
            class ConnectionProvider with ChangeNotifier {
              final Connectivity _connectivity = Connectivity();
              bool _isConnected = true;
            
              bool get isConnected => _isConnected;
            
              ConnectionProvider() {
                _initConnectivity();
                _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
              }
            
              void _initConnectivity() async {
                final result = await _connectivity.checkConnectivity();
                _updateConnectionStatus(result);
              }
            
              void _updateConnectionStatus(ConnectivityResult result) {
                final newStatus = result != ConnectivityResult.none;
                if (_isConnected != newStatus) {
                  _isConnected = newStatus;
                  notifyListeners();
                }
              }
            }
            
      2. Conclusion
        • Cette implémentation fournit une base solide pour une application de chat en temps réel avec Flutter. L'architecture suit les meilleures pratiques avec une séparation claire des responsabilités, l'utilisation de providers pour la gestion d'état, et des services pour la logique métier.
        • Pour compléter l'application, vous devrez :
        • Implémenter le backend selon les spécifications fournies
        • Configurer les variables d'environnement pour les URLs d'API
        • Ajouter la gestion des pièces jointes et des médias
        • Implémenter les notifications push
        • Ajouter des tests unitaires et d'intégration
        • Configurer le déploiement avec Docker et CI/CD
        • Cette architecture est extensible et peut être adaptée pour supporter des fonctionnalités supplémentaires comme les conversations de groupe, les appels vocaux/vidéo, et les réactions aux messages.