CometChat Flutter SDK v5 — Method Compositions
How to chain and combine SDK methods for common app flows. Each composition shows the method sequence, error handling between steps, and the data flow.
Registration Flow: createUser → login
Registration requires two SDK calls in sequence. createUser creates the CometChat user, then login authenticates them.
Future<void> register(String uid, String name, String authKey) async {
// Step 1: Create user in CometChat
await CometChat.createUser(User(uid: uid, name: name), authKey,
onSuccess: (user) => debugPrint("Created: ${user.uid}"),
onError: (e) => throw e,
);
// Step 2: Login the newly created user
await CometChat.login(uid, authKey,
onSuccess: (user) => debugPrint("Logged in: ${user.uid}"),
onError: (e) => throw e,
);
}
Error handling between steps:
- If
createUsersucceeds butloginfails → the user already exists in CometChat. Retryloginonly — callingcreateUseragain will fail with user-already-exists. - If
createUserfails → check the error code.ERR_UID_ALREADY_EXISTSmeans the user was created previously (e.g., on a prior attempt). In that case, skip straight tologin.
// Robust registration with intermediate failure handling
Future<User> registerRobust(String uid, String name, String authKey) async {
try {
await CometChat.createUser(User(uid: uid, name: name), authKey,
onSuccess: (_) {},
onError: (e) => throw e,
);
} catch (e) {
// If user already exists, that's fine — proceed to login
if (e is CometChatException && e.code == 'ERR_UID_ALREADY_EXISTS') {
debugPrint("User already exists, proceeding to login");
} else {
rethrow;
}
}
final c = Completer<User>();
CometChat.login(uid, authKey,
onSuccess: (user) => c.complete(user),
onError: (e) => c.completeError(e),
);
return c.future;
}
Bootstrap Sequence: init → configure → session check
The full SDK startup chain includes configuration calls after init that are often missed.
Future<User?> bootstrapSDK(String appId, AppSettings settings) async {
// 1. Initialize
await CometChat.init(appId, settings, onSuccess: (_) {}, onError: (e) => throw e);
// 2. Configure (often missed — needed for analytics and platform tracking)
CometChat.setSource("your_app", "flutter", "dart");
CometChat.setPlatformParams(platform: "flutter", sdkVersion: "1.0.0");
CometChat.setDemoMetaInfo(jsonObject: {"app": "your_app"});
// 3. Check existing session
final completer = Completer<User?>();
CometChat.getLoggedInUser(
onSuccess: (user) => completer.complete(user),
onError: (_) => completer.complete(null),
);
return completer.future;
}
Conversation Patching: MessageListener → getConversation → patch list
When a new message arrives, fetch only the affected conversation instead of re-fetching the entire list.
Step 1: Extract conversation partner from BaseMessage
/// Determine which conversation a message belongs to.
/// For groups: use receiverUid (the GUID).
/// For users: compare sender against logged-in user to determine direction.
(String conversationWith, String conversationType) extractPartner(
BaseMessage message,
String loggedInUid,
) {
if (message.receiverType == 'group') {
return (message.receiverUid, 'group');
}
// For 1:1 — if we sent it, conversation is with receiver.
// If we received it, conversation is with sender.
final partner = message.sender?.uid == loggedInUid
? message.receiverUid
: message.sender?.uid ?? message.receiverUid;
return (partner, 'user');
}
Step 2: Fetch and patch
Future<void> patchConversation(
BaseMessage message,
String loggedInUid,
List<Conversation> conversations,
) async {
final (conversationWith, conversationType) = extractPartner(message, loggedInUid);
final completer = Completer<Conversation>();
CometChat.getConversation(conversationWith, conversationType,
onSuccess: (conv) => completer.complete(conv),
onError: (e) => completer.completeError(e),
);
final updated = await completer.future;
// Remove old entry, insert updated at top
conversations.removeWhere((c) => c.conversationId == updated.conversationId);
conversations.insert(0, updated);
}
Step 3: Debounce rapid messages
When multiple messages arrive in quick succession (e.g., a burst of messages in a group), debounce the patch to avoid redundant getConversation calls:
Timer? _patchDebounce;
void onNewMessage(BaseMessage message) {
_patchDebounce?.cancel();
_patchDebounce = Timer(const Duration(milliseconds: 500), () {
patchConversation(message, loggedInUid, conversations);
});
}
// Cancel in dispose:
@override
void dispose() {
_patchDebounce?.cancel();
super.dispose();
}
This coalesces rapid message bursts into a single getConversation call for the latest message.
Anti-pattern: full list refresh on every message
// ❌ WRONG — nukes list, causes full rebuild, O(n) network call
void onNewMessage() {
conversations = [];
fetchAllConversations();
}
// ✅ CORRECT — single conversation fetch, O(1) patch
void onNewMessage(BaseMessage msg) => patchConversation(msg, myUid, conversations);
Typing State on Conversation List: MessageListener → map TypingIndicator to conversation
Similar to conversation patching (above), typing indicators on a conversation list require mapping a real-time event to a specific conversation row. But unlike message patching, typing is transient state — no SDK fetch needed, just map the TypingIndicator fields to the right conversation.
Mapping TypingIndicator to a conversation
The key insight: TypingIndicator.sender.uid identifies who is typing, and TypingIndicator.receiverId + receiverType identifies where. For 1:1 chats, the conversation key is the sender's UID. For groups, it's the receiverId (the GUID).
/// Build a conversation lookup key from a TypingIndicator.
/// This key matches against User.uid or Group.guid in the conversation list.
String typingConversationKey(TypingIndicator indicator) {
return indicator.receiverType == 'group'
? indicator.receiverId // GUID — the group conversation
: indicator.sender.uid; // sender's UID — the 1:1 conversation
}
Composition with MessageListener
Add onTypingStarted/onTypingEnded to the same MessageListener already used for conversation patching. Both patterns share the same listener — no extra registration needed.
// Same MessageListener handles both message patching AND typing state
@override
void onTypingStarted(TypingIndicator indicator) {
final key = typingConversationKey(indicator);
setState(() => _typingMap[key] = indicator.sender.name);
}
@override
void onTypingEnded(TypingIndicator indicator) {
final key = typingConversationKey(indicator);
setState(() => _typingMap.remove(key));
}
// When rendering each conversation row:
// final typingName = _typingMap[conversationPartnerUidOrGuid];
// if (typingName != null) → show typing indicator instead of last message preview
Anti-pattern: separate listener for typing on conversation list
// ❌ WRONG — registering a second MessageListener just for typing
// This either uses a duplicate ID (silently replaces the message listener)
// or adds unnecessary listener overhead
CometChat.addMessageListener("chats_typing", typingOnlyListener);
CometChat.addMessageListener("chats_messages", messageOnlyListener);
// ✅ CORRECT — single listener handles both messages and typing
CometChat.addMessageListener("chats_screen", combinedListener);
SUGGESTION: Consider adding a safety-net timer (5-8 seconds) that auto-clears stale entries from the typing map. The SDK does not guarantee onTypingEnded fires if the sender disconnects or goes idle without explicitly calling endTyping.
AI Feature Gating: isAIFeatureEnabled → conditional AI calls
Always check if an AI feature is enabled before calling AI methods. Calling without checking produces silent failures.
Future<List<String>> getSmartRepliesIfEnabled(
String receiverId,
String receiverType,
) async {
// Gate: check feature availability first
final completer = Completer<bool>();
CometChat.isAIFeatureEnabled('smart-replies',
onSuccess: (enabled) => completer.complete(enabled),
onError: (_) => completer.complete(false),
);
final enabled = await completer.future;
if (!enabled) return [];
// Feature is available — fetch replies
final repliesCompleter = Completer<List<String>>();
CometChat.getConversationStarter(receiverId, receiverType,
onSuccess: (starters) => repliesCompleter.complete(starters),
onError: (_) => repliesCompleter.complete([]),
);
return repliesCompleter.future;
}
Context-aware AI: starters vs replies
Use different AI methods depending on conversation state:
- Empty conversation (no messages yet) →
getConversationStarter— suggests ice-breaker prompts - Active conversation (messages exist) →
getSmartReplies— suggests contextual replies based on recent messages
Future<void> loadAISuggestions(String receiverId, String receiverType, bool hasMessages) async {
final enabled = await isAIFeatureEnabled('smart-replies');
if (!enabled) return;
if (!hasMessages) {
// New conversation — show conversation starters
final starters = await getConversationStarter(receiverId, receiverType);
setState(() => _smartReplies = starters);
} else {
// Active conversation — show contextual smart replies
final replies = await getSmartReplies(receiverId, receiverType);
setState(() => _smartReplies = replies.values.toList());
}
}
Conversation Open: markAsRead → navigate → refresh on return
When opening a conversation, mark it as read before entering the chat. On return, refresh the conversation list to reflect updated state.
Future<void> openConversation(String uid, String type) async {
// 1. Mark as read immediately
CometChat.markConversationAsRead(uid, type,
onSuccess: (_) {},
onError: (_) {},
);
// 2. Navigate to chat screen
await Navigator.push(context, MaterialPageRoute(builder: (_) => ChatScreen(...)));
// 3. On return — refresh conversation list to pick up any changes
refreshConversations();
}
Parallel SDK Fetching with Future.wait
When you need multiple independent SDK results (e.g., loading a screen that shows both joined groups and all groups), fetch them in parallel:
// ❌ WRONG — sequential, doubles the wait time
final myGroups = await fetchGroups(joinedOnlyRequest);
final allGroups = await fetchGroups(discoverRequest);
// ✅ CORRECT — parallel, total time = max(request1, request2)
final results = await Future.wait([
fetchGroups(joinedOnlyRequest),
fetchGroups(discoverRequest),
]);
final myGroups = results[0];
final allGroups = results[1];
This works for any independent SDK calls: group + members, user list + online count, unread counts for users + groups, etc. Only use Future.wait when the calls are truly independent — if call B depends on the result of call A, keep them sequential.
UI Suggestions (Non-Binding)
These patterns worked well in practice. Use them if they fit your app's design.
- SUGGESTION: Consider separating SDK init from auth UI into a dedicated bootstrap step — keeps auth screens testable without SDK dependencies
- SUGGESTION: Consider showing ConnectionListener status as a subtle indicator rather than blocking UI
Checklist
- Registration chains createUser → login (not login alone)
- Bootstrap includes setSource/setPlatformParams/setDemoMetaInfo after init
- Conversation updates use getConversation for single-item patch, not full re-fetch
- Conversation list typing uses keyed map from TypingIndicator, shares same MessageListener as message patching
- AI methods gated behind isAIFeatureEnabled check
- markConversationAsRead called when opening a conversation
- Partner extraction handles both sender and receiver direction for 1:1 chats
- Conversation patching debounced (300-500ms) to coalesce rapid message bursts
- Parallel SDK fetching used for independent data loads (Future.wait)
- AI suggestions context-aware: starters for empty conversations, smart replies for active ones