Flutter Chat Plugin

A flexible, feature-rich chat plugin for Flutter applications that provides real-time messaging capabilities using Socket.IO. This plugin handles all the complex aspects of chat implementation, including real-time messaging, online status, typing indicators, and message delivery/read receipts.

Features

  • πŸ’¬ Real-time Messaging: Instant message delivery using Socket.IO
  • πŸ“± Cross-Platform: Works on iOS, Android, and Web
  • πŸ“Š Message Status Tracking: Sent, delivered, and read receipts
  • ⌨️ Typing Indicators: Show when users are typing
  • 🟒 Online Status: Track user online/offline status with last seen timestamps
  • πŸ“‚ Chat Rooms: Easily manage multiple conversations
  • πŸ“ Customizable: Use your own API endpoints and UI components
  • πŸ”„ Event-Based Architecture: React to changes with a comprehensive event system

Installation

dependencies:
  chat_plugin: ^<latest_version>

Quick Start

1. Initialize the Plugin

First, initialize the chat plugin when your user logs in:

import 'package:flutter_chat_plugin/flutter_chat_plugin.dart';

// Initialize configuration
final config = ChatConfig(
  apiUrl: 'https://your-api-url.com',
  userId: currentUserId,
  token: authToken,
  enableTypingIndicators: true,
  enableReadReceipts: true,
  enableOnlineStatus: true,
  autoMarkAsRead: true,
  maxReconnectionAttempts: 5
);

// Set up API handlers
final apiHandlers = ChatApiHandlers(
  loadMessagesHandler: ({page = 1, limit = 20, searchText = ""}) async {
    // Implement API call to load messages
    final response = await http.get(Uri.parse(
      'https://your-api-url.com/api/chat/messages?currentUserId=$userId&senderId=$userId&receiverId=${ChatService.instance.receiverId}&page=$page&limit=$limit'
    ));
    
    if (response.statusCode == 200) {
      List<dynamic> data = jsonDecode(response.body);
      return data.map((msg) => ChatMessage.fromMap(msg, userId)).toList();
    }
    return [];
  },
  
  loadChatRoomsHandler: () async {
    // Implement API call to load chat rooms
    final response = await http.get(Uri.parse(
      'https://your-api-url.com/api/chat/chat-room'
    ));
    
    if (response.statusCode == 200) {
      List<dynamic> data = jsonDecode(response.body);
      return data.map((room) => ChatRoom.fromMap(room)).toList();
    }
    return [];
  },
  
  sendMessageHandler: (String text) async {
    // Implement sending a message
    // Return a ChatMessage object
  },
  
  // Add other handlers as needed
);

// Initialize the chat service
ChatService.instance.updateConfig(config);
ChatService.instance.setApiHandlers(apiHandlers);
await ChatService.instance.initialize();

2. Show Chat Room List

Create a screen to display all chat rooms:

class ChatRoomsScreen extends StatefulWidget {
  @override
  _ChatRoomsScreenState createState() => _ChatRoomsScreenState();
}

class _ChatRoomsScreenState extends State<ChatRoomsScreen> {
  final ChatService _chatService = ChatService.instance;
  bool _isLoading = true;
  
  @override
  void initState() {
    super.initState();
    
    // Register event listener for chat rooms updates
    _chatService.addEventListener(
      ChatEventType.chatRoomsChanged,
      'chat_rooms_screen',
      (_) {
        setState(() {
          _isLoading = false;
        });
      }
    );
    
    // Load chat rooms
    _loadChatRooms();
  }
  
  Future<void> _loadChatRooms() async {
    setState(() {
      _isLoading = true;
    });
    await _chatService.loadChatRooms();
  }
  
  @override
  Widget build(BuildContext context) {
    final chatRooms = _chatService.chatRooms;
    
    return Scaffold(
      appBar: AppBar(
        title: Text('Messages'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: _loadChatRooms,
          ),
        ],
      ),
      body: _isLoading
          ? Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: chatRooms.length,
              itemBuilder: (context, index) {
                final room = chatRooms[index];
                return ListTile(
                  leading: CircleAvatar(
                    backgroundImage: room.avatarUrl != null 
                        ? NetworkImage(room.avatarUrl!) 
                        : null,
                    child: room.avatarUrl == null 
                        ? Text(room.username[0]) 
                        : null,
                  ),
                  title: Text(room.username),
                  subtitle: Text(room.latestMessage),
                  trailing: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.end,
                    children: [
                      Text(_formatTime(room.latestMessageTime)),
                      if (room.unreadCount > 0)
                        Container(
                          padding: EdgeInsets.all(6),
                          decoration: BoxDecoration(
                            color: Colors.green,
                            shape: BoxShape.circle,
                          ),
                          child: Text(
                            '${room.unreadCount}',
                            style: TextStyle(color: Colors.white, fontSize: 12),
                          ),
                        ),
                    ],
                  ),
                  onTap: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => ChatScreen(
                          receiverId: room.userId,
                          receiverName: room.username,
                        ),
                      ),
                    );
                  },
                );
              },
            ),
    );
  }
  
  String _formatTime(DateTime time) {
    // Implement time formatting logic
    return '12:30 PM'; // Example
  }
  
  @override
  void dispose() {
    _chatService.removeEventListener(ChatEventType.chatRoomsChanged, 'chat_rooms_screen');
    super.dispose();
  }
}

3. Implement Chat Screen

Create a screen for chatting with a specific user:

class ChatScreen extends StatefulWidget {
  final String receiverId;
  final String receiverName;
  
  ChatScreen({
    required this.receiverId,
    required this.receiverName,
  });
  
  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> with WidgetsBindingObserver {
  final ChatService _chatService = ChatService.instance;
  final TextEditingController _messageController = TextEditingController();
  final ScrollController _scrollController = ScrollController();
  bool _isLoading = true;
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    
    // Register event listeners
    _registerEventListeners();
    
    // Initialize chat with this user
    _initChat();
    
    // Add listener for typing indicator
    _messageController.addListener(_onTextChanged);
  }
  
  void _registerEventListeners() {
    // Listen for messages updates
    _chatService.addEventListener(
      ChatEventType.messagesChanged,
      'chat_screen',
      (_) {
        setState(() {
          _isLoading = false;
        });
        
        // Scroll to bottom on new messages
        WidgetsBinding.instance.addPostFrameCallback((_) {
          if (_scrollController.hasClients) {
            _scrollController.animateTo(
              _scrollController.position.maxScrollExtent,
              duration: Duration(milliseconds: 300),
              curve: Curves.easeOut,
            );
          }
        });
      }
    );
    
    // Listen for typing status changes
    _chatService.addEventListener(
      ChatEventType.typingStatusChanged,
      'chat_screen_typing',
      (isTyping) {
        setState(() {});
      }
    );
    
    // Listen for online status changes
    _chatService.addEventListener(
      ChatEventType.onlineStatusChanged,
      'chat_screen_online',
      (data) {
        setState(() {});
      }
    );
  }
  
  Future<void> _initChat() async {
    setState(() {
      _isLoading = true;
    });
    
    await _chatService.initChat(widget.receiverId);
  }
  
  bool _isTyping = false;
  DateTime _lastTypingTime = DateTime.now();
  Timer? _debounceTimer;
  
  void _onTextChanged() {
    final now = DateTime.now();
    
    _debounceTimer?.cancel();
    
    if (_messageController.text.isNotEmpty) {
      if (!_isTyping || now.difference(_lastTypingTime).inSeconds >= 2) {
        _isTyping = true;
        _lastTypingTime = now;
        _chatService.sendTypingIndicator(true);
      }
      
      _debounceTimer = Timer(Duration(milliseconds: 1500), () {
        if (_isTyping) {
          _isTyping = false;
          _chatService.sendTypingIndicator(false);
        }
      });
    } else if (_messageController.text.isEmpty && _isTyping) {
      _isTyping = false;
      _chatService.sendTypingIndicator(false);
    }
  }
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      _chatService.updateUserStatus(true);
    } else if (state == AppLifecycleState.paused) {
      _chatService.updateUserStatus(false);
    }
  }
  
  void _sendMessage() async {
    if (_messageController.text.isEmpty) return;
    
    final text = _messageController.text;
    _messageController.clear();
    
    try {
      await _chatService.sendMessage(text);
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to send message')),
      );
    }
  }
  
  @override
  Widget build(BuildContext context) {
    final messages = _chatService.messages;
    final isReceiverTyping = _chatService.isReceiverTyping;
    final isReceiverOnline = _chatService.isReceiverOnline;
    final lastSeen = _chatService.lastSeen;
    
    return Scaffold(
      appBar: AppBar(
        title: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(widget.receiverName),
            Text(
              isReceiverOnline 
                ? 'Online'
                : lastSeen != null 
                    ? 'Last seen ${_formatLastSeen(lastSeen)}'
                    : 'Offline',
              style: TextStyle(
                fontSize: 12,
                color: isReceiverOnline ? Colors.green : Colors.grey,
              ),
            ),
          ],
        ),
      ),
      body: Column(
        children: [
          Expanded(
            child: _isLoading
                ? Center(child: CircularProgressIndicator())
                : ListView.builder(
                    controller: _scrollController,
                    itemCount: messages.length,
                    itemBuilder: (context, index) {
                      final message = messages[index];
                      return _buildMessageBubble(message);
                    },
                  ),
          ),
          
          // Typing indicator
          if (isReceiverTyping)
            Container(
              padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              alignment: Alignment.centerLeft,
              child: Text(
                '${widget.receiverName} is typing...',
                style: TextStyle(
                  color: Colors.grey,
                  fontStyle: FontStyle.italic,
                ),
              ),
            ),
          
          // Message input
          Padding(
            padding: EdgeInsets.all(8.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _messageController,
                    decoration: InputDecoration(
                      hintText: 'Type a message',
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(24),
                      ),
                      contentPadding: EdgeInsets.symmetric(
                        horizontal: 16,
                        vertical: 8,
                      ),
                    ),
                    textInputAction: TextInputAction.send,
                    onSubmitted: (_) => _sendMessage(),
                  ),
                ),
                SizedBox(width: 8),
                CircleAvatar(
                  radius: 24,
                  child: IconButton(
                    icon: Icon(Icons.send),
                    onPressed: _sendMessage,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
  
  Widget _buildMessageBubble(ChatMessage message) {
    final isMine = message.isMine;
    
    return Align(
      alignment: isMine ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
        padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
        decoration: BoxDecoration(
          color: isMine ? Colors.blue[100] : Colors.grey[200],
          borderRadius: BorderRadius.circular(12),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.end,
          children: [
            Text(message.message),
            SizedBox(height: 2),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  _formatTime(message.createdAt),
                  style: TextStyle(
                    fontSize: 10,
                    color: Colors.grey[600],
                  ),
                ),
                if (isMine) SizedBox(width: 4),
                if (isMine) _buildMessageStatus(message.status),
              ],
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildMessageStatus(String status) {
    switch (status) {
      case 'sent':
        return Icon(Icons.check, size: 12, color: Colors.grey);
      case 'delivered':
        return Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.check, size: 12, color: Colors.grey),
            Icon(Icons.check, size: 12, color: Colors.grey),
          ],
        );
      case 'read':
        return Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.check, size: 12, color: Colors.blue),
            Icon(Icons.check, size: 12, color: Colors.blue),
          ],
        );
      default:
        return SizedBox();
    }
  }
  
  String _formatTime(DateTime time) {
    final now = DateTime.now();
    if (time.day == now.day && 
        time.month == now.month && 
        time.year == now.year) {
      return '${time.hour}:${time.minute.toString().padLeft(2, '0')}';
    } else {
      return '${time.day}/${time.month} ${time.hour}:${time.minute.toString().padLeft(2, '0')}';
    }
  }
  
  String _formatLastSeen(DateTime time) {
    final now = DateTime.now();
    final difference = now.difference(time);
    
    if (difference.inMinutes < 1) {
      return 'just now';
    } else if (difference.inHours < 1) {
      return '${difference.inMinutes} min ago';
    } else if (difference.inDays < 1) {
      return '${difference.inHours} h ago';
    } else {
      return '${time.day}/${time.month}/${time.year}';
    }
  }
  
  @override
  void dispose() {
    // Remove event listeners
    _chatService.removeEventListener(ChatEventType.messagesChanged, 'chat_screen');
    _chatService.removeEventListener(ChatEventType.typingStatusChanged, 'chat_screen_typing');
    _chatService.removeEventListener(ChatEventType.onlineStatusChanged, 'chat_screen_online');
    
    // Remove message controller listener
    _messageController.removeListener(_onTextChanged);
    _messageController.dispose();
    _scrollController.dispose();
    
    // Handle chat cleanup
    _chatService.leaveChat(widget.receiverId);
    
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
}

Advanced Usage

Custom Socket Events

You can register and handle custom socket events:

// Register custom event handlers
Map<String, Function(dynamic)> customEvents = {
  'custom_event_name': (data) {
    print('Received custom event: $data');
    // Handle custom event data
  }
};

ChatService.instance.registerCustomSocketEvents(customEvents);

// Emit a custom event
ChatService.instance.emitCustomEvent('custom_event_name', {
  'userId': 'user123',
  'data': 'some data'
});

Custom Event Listeners

You can listen for specific chat events in your application:

// Register an event listener with a unique ID
ChatService.instance.addEventListener(
  ChatEventType.messageStatusChanged,
  'unique_listener_id',
  (data) {
    print('Message status changed: $data');
    // Handle message status change
  }
);

// Trigger a custom event from anywhere in your app
ChatService.instance.triggerCustomEvent(
  'custom_app_event',
  {'key': 'value'}
);

// Remember to remove listeners when done
ChatService.instance.removeEventListener(
  ChatEventType.messageStatusChanged,
  'unique_listener_id'
);

Message Deletion

Delete messages using the ChatService:

// Delete a message
bool success = await ChatService.instance.deleteMessage('message_id');
if (success) {
  print('Message deleted successfully');
} else {
  print('Failed to delete message');
}

Refresh Connections

Handle connection refreshes and state changes:

// Refresh global connection
ChatService.instance.refreshGlobalConnection();

// Check connection status
bool isConnected = ChatService.instance.isSocketConnected;

// Log socket state for debugging
ChatService.instance.logSocketState();

Models

The plugin provides several model classes for working with chat data:

ChatMessage

Represents a single chat message:

ChatMessage message = ChatMessage(
  messageId: 'msg123',
  senderId: 'user1',
  receiverId: 'user2',
  message: 'Hello there!',
  createdAt: DateTime.now(),
  status: 'sent',
  isMine: true,
);

ChatRoom

Represents a chat conversation:

ChatRoom room = ChatRoom(
  userId: 'user123',
  username: 'John Doe',
  latestMessage: 'Hello there!',
  latestMessageTime: DateTime.now(),
  unreadCount: 3,
  latestMessageStatus: 'delivered',
  avatarUrl: 'https://example.com/avatar.jpg',
);

ChatUser

Represents a user in the chat system:

ChatUser user = ChatUser(
  userId: 'user123',
  username: 'John Doe',
  isOnline: true,
  lastSeen: DateTime.now(),
  avatarUrl: 'https://example.com/avatar.jpg',
);

Event Types

The plugin provides these event types:

  • ChatEventType.messagesChanged: When messages list changes
  • ChatEventType.chatRoomsChanged: When chat rooms list changes
  • ChatEventType.typingStatusChanged: When receiver's typing status changes
  • ChatEventType.onlineStatusChanged: When receiver's online status changes
  • ChatEventType.messageStatusChanged: When message status changes
  • ChatEventType.connectionStatusChanged: When socket connection status changes
  • ChatEventType.error: When an error occurs
  • ChatEventType.custom: For custom events

Configuration Options

The ChatConfig class offers these configuration options:

ChatConfig config = ChatConfig(
  apiUrl: 'https://your-api-url.com',
  userId: 'current-user-id',
  token: 'auth-token',
  enableTypingIndicators: true,
  enableReadReceipts: true,
  enableOnlineStatus: true,
  autoMarkAsRead: true,
  maxReconnectionAttempts: 5,
);

Best Practices

  1. Always remove event listeners in the dispose() method of your widgets to prevent memory leaks.

  2. Handle socket connection errors by listening for the connectionStatusChanged event.

  3. Use unique IDs for event listeners to avoid conflicts between different widgets.

  4. Initialize the chat service early in your application lifecycle, typically after user login.

  5. Set custom API handlers to integrate with your specific backend implementation.

  6. Properly handle app lifecycle changes to update user online status correctly.

Troubleshooting

Common Issues

  1. Socket connection errors:

    • Check network connectivity
    • Verify API URL is correct
    • Ensure authentication token is valid
  2. Messages not being sent/received:

    • Check socket connection status
    • Verify you have the correct user IDs
    • Check API handlers implementation
  3. Event listeners not working:

    • Ensure you're using unique listener IDs
    • Check that you're registering listeners before they're needed
    • Make sure you haven't removed the listener prematurely
  4. Performance issues:

    • Use pagination when loading messages
    • Dispose of resources properly
    • Consider using memory-efficient widgets for large chat histories

Debug Mode

Enable debug mode to view detailed logs:

ChatService.instance.setDebugMode(true);

⚑ Donate

If you like my work, you can support me buying a cup of :coffee:

Code and documentation Copyright 2025 SnippetCoder. Code released under the Apache License. Docs released under Creative Commons.

Libraries

chat_plugin