chat_plugin 1.0.0
chat_plugin: ^1.0.0 copied to clipboard
A flexible, feature-rich chat plugin for Flutter applications that provides real-time messaging capabilities using Socket.IO.
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 changesChatEventType.chatRoomsChanged
: When chat rooms list changesChatEventType.typingStatusChanged
: When receiver's typing status changesChatEventType.onlineStatusChanged
: When receiver's online status changesChatEventType.messageStatusChanged
: When message status changesChatEventType.connectionStatusChanged
: When socket connection status changesChatEventType.error
: When an error occursChatEventType.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 #
-
Always remove event listeners in the
dispose()
method of your widgets to prevent memory leaks. -
Handle socket connection errors by listening for the
connectionStatusChanged
event. -
Use unique IDs for event listeners to avoid conflicts between different widgets.
-
Initialize the chat service early in your application lifecycle, typically after user login.
-
Set custom API handlers to integrate with your specific backend implementation.
-
Properly handle app lifecycle changes to update user online status correctly.
Troubleshooting #
Common Issues #
-
Socket connection errors:
- Check network connectivity
- Verify API URL is correct
- Ensure authentication token is valid
-
Messages not being sent/received:
- Check socket connection status
- Verify you have the correct user IDs
- Check API handlers implementation
-
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
-
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 β
Copyright & License #
Code and documentation Copyright 2025 SnippetCoder. Code released under the Apache License. Docs released under Creative Commons.