Receive messages
Handle standard real-time, offline-synchronized, and online-only message events in the OpenIM Flutter SDK.
Standard real-time messages, offline messages synchronized upon login, and online-only messages are received through onRecvNewMessage, onRecvOfflineNewMessage, and onRecvOnlineOnlyMessage callbacks respectively. Each Flutter callback delivers a single Message instance rather than an array of messages.
Chat screens typically handle initial history snapshots upon opening, real-time message arrivals, offline sync batches following reconnection, and transient online-only notifications. Historical queries establish the base snapshot, while listeners apply incremental updates. Never treat listener delivery as the completion callback for a query or mark-as-read request.
Message types
Choose a renderer based on contentType and non-null element payloads: textElem, atTextElem, and customElem represent text, mentions, and custom payloads; pictureElem, soundElem, videoElem, and fileElem contain download URLs, byte sizes, file names, durations, or thumbnail previews, requiring no re-upload on the receiver. Render unsupported-message placeholders for unrecognized types, and parse custom messages using your application's agreed data schema.
If your product requires sending multiple files together, send consecutive file messages or encapsulate the file list in a single custom message. Regardless of presentation grouping, the application state layer must track each item by its stable clientMsgID.
Parameters and fields
Flutter's Message model does not contain a conversationID property. Global listeners must resolve conversation routing from message fields first, then request the conversation ID from the SDK.
The following table details fields commonly used for routing, display, and state merging (all declared nullable except exMap):
| Parameter | Type | Required | Description |
|---|---|---|---|
clientMsgID | String? | Yes | Client-assigned unique ID. When non-empty, serves as the stable merge key. |
serverMsgID | String? | No | Server-assigned message ID. Does not replace clientMsgID for local merging. |
sessionType | int? | Yes | Conversation type from ConversationType, determining single or group routing. |
sendID | String? | Yes | Sender user ID. Combined with the current user ID to identify counterparties. |
recvID | String? | No | Recipient user ID, used to identify the peer when the current user is the sender. |
groupID | String? | No | Destination group ID in group chats. |
contentType | int? | Yes | Content type constant from MessageType, determining the active content element. |
sendTime | int? | No | Unix timestamp (in milliseconds) when sent, used for display ordering. |
status | int? | No | Delivery status from MessageStatus. |
isRead | bool? | No | Read status currently tracked by the SDK. |
Text, image, audio, video, file, mention, custom, quote, and merged payloads reside in their corresponding nullable element properties. Practice defensive rendering by verifying both contentType and the element payload. If an expected element is missing, render a fallback placeholder instead of force-unwrapping.
Code example
Future<String?> resolveMessageConversationID(
Message message,
String currentUserID,
) async {
final sessionType = message.sessionType;
if (sessionType == null) return null;
final String? sourceID;
if (sessionType == ConversationType.single) {
sourceID = message.sendID == currentUserID ? message.recvID : message.sendID;
} else {
sourceID = message.groupID;
}
if (sourceID == null || sourceID.isEmpty) return null;
final value = await OpenIM.iMManager.conversationManager
.getConversationIDBySessionType(
sourceID: sourceID,
sessionType: sessionType,
);
final conversationID = value?.toString();
return conversationID == null || conversationID.isEmpty
? null
: conversationID;
}
Future<void> mergeReceivedMessage(Message message) async {
final clientMsgID = message.clientMsgID;
if (clientMsgID == null || clientMsgID.isEmpty) return;
final conversationID =
await resolveMessageConversationID(message, currentUserID);
if (conversationID == null) return;
upsertMessage(conversationID, clientMsgID, message);
}
Future<void> handleNewMessage(Message message) =>
mergeReceivedMessage(message);
Future<void> handleOfflineMessage(Message message) =>
mergeReceivedMessage(message);
Future<void> handleOnlineOnlyMessage(Message message) async {
final conversationID =
await resolveMessageConversationID(message, currentUserID);
if (conversationID == null) return;
showEphemeralMessage(conversationID, message);
}State changes after the call
Register these handlers within the application's single OnAdvancedMsgListener. For centralized listener setup, see Events overview.
Attach the listener once during SDK initialization and authentication, dispatching received messages to active conversation stores via an application-level event bus. Avoid reattaching listeners whenever opening chat screens, as subsequent registrations overwrite prior callbacks.
Merge regular and offline messages idempotently using the resolved composite key conversationID:clientMsgID to prevent duplicate insertions across history pagination and sync intervals. If a specific chat view is active, compare the resolved conversation ID against the active conversation; route messages belonging to other chats to their background stores.
Messages received via onRecvOnlineOnlyMessage (sent with isOnlineOnly: true) are not persisted to local databases and should not be stored as replayable history. Resolve the conversation routing to avoid displaying transient notifications from other conversations in the active screen.
History snapshots and event increments
When opening a conversation for the first time, load an initial history snapshot using the screen's conversationID. When scrolling upwards, use the earliest message as the boundary cursor. Because historical queries and event listeners may deliver identical records, deduplicate both using clientMsgID. For pagination details, see Load older messages.
After the user opens and views the chat, call markConversationMessageAsRead(conversationID: conversationID) to clear conversation unread counts. This action is separate from group-member read receipts.
Future completion indicates that the mark-as-read request completed, but does not guarantee that conversation-list events have reached all clients. Continue updating conversation unread counters and badge counts via conversation events, re-querying when state reconciliation is needed.
Related guides
Was this page helpful?