Each component’s BLoC manages data fetching, state transitions, and business logic. You can configure it via RequestBuilders, provide a custom BLoC instance, or extend the default one.
Configuring Data Fetching with RequestBuilders
Use request builders to control what data the component fetches. Pass the builder instance — not the result of .build().
CometChatMessageList(
user: user,
messagesRequestBuilder: MessagesRequestBuilder()
..uid = user.uid
..searchKeyword = 'hello'
..limit = 30,
)
The following parameters in MessagesRequestBuilder will always be altered inside the message list: UID, GUID, types, categories.
Request Builder by Component
| Component | Builder Property | Builder Type |
|---|
CometChatConversations | conversationsRequestBuilder | ConversationsRequestBuilder |
CometChatUsers | usersRequestBuilder | UsersRequestBuilder |
CometChatGroups | groupsRequestBuilder | GroupsRequestBuilder |
CometChatGroupMembers | groupMembersRequestBuilder | GroupMembersRequestBuilder |
CometChatMessageList | messagesRequestBuilder | MessagesRequestBuilder |
Providing a Custom BLoC
Each component accepts an optional BLoC parameter. Provide your own instance to override default behavior:
final myBloc = MessageListBloc(
user: user,
parentMessageId: null,
types: MessageTemplateUtils.getAllMessageTypes(),
categories: MessageTemplateUtils.getAllMessageCategories(),
);
CometChatMessageList(
user: user,
messageListBloc: myBloc,
)
BLoC Parameters by Component
| Component | BLoC Property | BLoC Type |
|---|
CometChatConversations | conversationsBloc | ConversationsBloc |
CometChatMessageList | messageListBloc | MessageListBloc |
CometChatMessageComposer | messageComposerBloc | MessageComposerBloc |
Extending the Default BLoC
Extend the default BLoC class and override hooks to add custom behavior:
class CustomMessageListBloc extends MessageListBloc {
CustomMessageListBloc({required User user})
: super(user: user);
@override
void onItemAdded(BaseMessage item, List<BaseMessage> updatedList) {
// Custom logic when a message is added
debugPrint('Message added: ${item.id}');
super.onItemAdded(item, updatedList);
}
@override
void onItemRemoved(BaseMessage item, List<BaseMessage> updatedList) {
// Custom logic when a message is removed
super.onItemRemoved(item, updatedList);
}
@override
void onItemUpdated(BaseMessage oldItem, BaseMessage newItem, List<BaseMessage> updatedList) {
// Custom logic when a message is updated
super.onItemUpdated(oldItem, newItem, updatedList);
}
}
ListBase Hooks
All list-based BLoCs use the ListBase mixin with these override hooks:
| Hook | Called When |
|---|
onItemAdded(item, updatedList) | An item is added to the list |
onItemRemoved(item, updatedList) | An item is removed from the list |
onItemUpdated(oldItem, newItem, updatedList) | An item is updated in the list |
onListCleared(previousList) | The list is cleared |
onListReplaced(previousList, newList) | The entire list is replaced |
Component-Specific BLoC Events
Each component’s BLoC has its own events and methods. See the individual component docs for details:
Lifecycle Callbacks
CometChatMessageList(
user: user,
onLoad: (messages) {
debugPrint('Loaded ${messages.length} messages');
},
onEmpty: () {
debugPrint('No messages found');
},
onError: (e) {
debugPrint('Error: ${e.message}');
},
)
Repository & Datasource Overrides
Both CometChatConversations and CometChatMessageList follow the same clean architecture stack. There are two override points:
Widget → BLoC → UseCase → Repository → DataSource (Remote + Local)
↑ ↑
override here or here
1. Datasource Override
Implement the abstract datasource interfaces to swap the data layer entirely — e.g. a REST API instead of the CometChat SDK, or a persistent cache instead of in-memory.
Conversations
abstract class ConversationsRemoteDataSource {
Future<List<Conversation>> getConversations({int limit, String? fromId});
Future<Conversation> getConversation(String conversationId);
Future<void> deleteConversation(String conversationWith, String conversationType);
Future<void> markMessageAsRead(BaseMessage message);
Future<Conversation> updateConversation(Conversation conversation);
}
abstract class ConversationsLocalDataSource {
Future<void> cacheConversations(List<Conversation> conversations);
Future<List<Conversation>> getCachedConversations();
Future<void> cacheConversation(Conversation conversation);
Future<Conversation?> getCachedConversation(String conversationId);
Future<void> removeCachedConversation(String conversationId);
Future<void> clearCache();
Future<bool> hasCachedData();
}
Message List
abstract class MessageListRemoteDataSource {
Future<GetMessagesResult> getMessages({...});
Future<List<BaseMessage>> fetchPreviousMessages({required MessagesRequest request});
Future<List<BaseMessage>> fetchNextMessages({required MessagesRequest request});
Future<void> markAsRead(BaseMessage message);
Future<void> markAsDelivered(BaseMessage message);
Future<User?> getLoggedInUser();
Future<Conversation> getConversation({...});
Future<Conversation> markMessageAsUnread(BaseMessage message);
}
abstract class MessageListLocalDataSource {
Future<void> cacheMessages(String conversationId, List<BaseMessage> messages);
Future<List<BaseMessage>> getCachedMessages(String conversationId);
// ... cache CRUD methods
}
2. Repository Override
The repository interfaces sit above the datasources. Override here to change business logic — caching strategy, error handling, retry logic — without touching the datasource layer.
Conversations
abstract class ConversationsRepository {
Future<Result<List<Conversation>>> getConversations({int limit, String? fromId});
Future<Result<Conversation>> getConversationById(String conversationId);
Future<Result<void>> deleteConversation(String conversationId);
Future<Result<Conversation>> updateConversation(Conversation conversation);
Future<Result<User?>> getLoggedInUser();
Future<Result<void>> markAsDelivered(BaseMessage message);
Future<Result<Conversation>> getConversation({...});
}
Message List
abstract class MessageListRepository {
Future<Result<List<BaseMessage>>> getMessages({...});
Future<Result<List<BaseMessage>>> fetchPreviousMessages({...});
Future<Result<List<BaseMessage>>> fetchNextMessages({...});
Future<Result<void>> markAsRead(BaseMessage message);
Future<Result<void>> markAsDelivered(BaseMessage message);
Future<Result<User?>> getLoggedInUser();
Future<Result<Conversation>> getConversation({...});
Future<Result<Conversation>> markMessageAsUnread(BaseMessage message);
}
3. Wiring via the Service Locator
The service locators are the injection point. Both are singletons with a setup() method. Call reset() first, then setup() with your custom implementations before the widget mounts.
Datasource-level override (Conversations)
class MyConversationsRemoteDataSource implements ConversationsRemoteDataSource {
@override
Future<List<Conversation>> getConversations({int limit = 30, String? fromId}) async {
// Your REST API call, mock data, etc.
}
// ... implement remaining methods
}
// Before mounting CometChatConversations:
await ConversationsServiceLocator.instance.reset();
final remote = MyConversationsRemoteDataSource();
final local = ConversationsLocalDataSourceImpl(); // keep default cache
final repo = ConversationsRepositoryImpl(
remoteDataSource: remote,
localDataSource: local,
);
Repository-level override (Message List)
class MyMessageListRepository implements MessageListRepository {
@override
Future<Result<List<BaseMessage>>> getMessages({...}) async {
// Custom logic
}
// ...
}
// Before mounting CometChatMessageList:
await MessageListServiceLocator.instance.reset();
// Wire your repo into use cases and pass to the BLoC
Key Points
| Point | Detail |
|---|
reset() | Sets _isInitialized = false — always call before re-wiring |
setup() | Guarded by _isInitialized — safe to call multiple times normally |
localDataSource in MessageListRepositoryImpl | Optional — pass null to disable caching entirely |
| Fallback caching | ConversationsRepositoryImpl falls back to local cache when remote fails — your custom remote inherits this if you use ConversationsRepositoryImpl with a custom datasource |
Result<T> | All repository methods return Result<T> (Success/Failure) — your implementations must do the same |