StreamChat(
client: client,
componentBuilders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
messageWidget: (context, props) {
return DefaultStreamMessage(
props: props.copyWith(
actionsBuilder: (context, defaultActions) {
return [
...defaultActions,
StreamContextMenuAction(
leading: const Icon(Icons.star),
label: const Text('Favourite'),
onTap: () => _favourite(props.message),
),
];
},
),
);
},
),
),
child: const MyHomePage(),
)Message
Customizing Messages with the Component Factory
Introduction
Every application provides a unique look and feel to their own messaging interface. In v10 (design-refresh), message customization uses a centralized component factory pattern instead of per-widget builder callbacks.
The component factory provides:
- App-wide message customization without repeating configuration on every widget
- A consistent, composable sub-component architecture
- The ability to replace individual sub-components (avatar, footer, reactions, text)
Using Component Builders
The simplest way to register app-wide component builders is via the componentBuilders parameter on StreamChat:
For subtree-scoped overrides (e.g., customizing builders only in one part of the app), wrap that subtree with StreamComponentFactory directly:
StreamComponentFactory(
builders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
messageWidget: (context, props) => MySpecialMessage(props: props),
),
),
child: const ChatDetailScreen(),
)Sub-Components
DefaultStreamMessage is composed of named sub-components you can replace individually:
| Sub-component | Description |
|---|---|
DefaultStreamMessage | Top-level default renderer; composes all sub-components |
StreamMessageContent | Bubble, attachments, text, reactions, thread replies |
StreamMessageFooter | Username, timestamp, sending status, edited indicator |
StreamMessageLeading | Author avatar |
StreamMessageReactions | Clustered reaction chips around the bubble |
StreamMessageText | Markdown-rendered message text |
StreamMessageDeleted | Deleted message placeholder |
StreamMessageSendingStatus | Delivery status icon |
Per-List Customization
For per-list customization, use messageBuilder on StreamMessageListView. The callback now receives StreamMessageWidgetProps instead of a pre-built widget:
StreamMessageListView(
messageBuilder: (context, message, defaultProps) {
// Build default widget (goes through component factory)
return StreamMessageWidget.fromProps(props: defaultProps);
},
),Customize props before building:
StreamMessageListView(
messageBuilder: (context, message, defaultProps) {
return StreamMessageWidget.fromProps(
props: defaultProps.copyWith(
actionsBuilder: (context, actions) => [...actions, myAction],
),
);
},
),Or replace entirely with your own widget:
StreamMessageListView(
messageBuilder: (context, message, defaultProps) {
return MyCustomMessageWidget(message: message);
},
),Custom Reaction Icons
Configure custom reactions globally via reactionIconResolver. Extend DefaultReactionIconResolver and override only what you need:
class MyReactionIconResolver extends DefaultReactionIconResolver {
const MyReactionIconResolver();
@override
Set<String> get defaultReactions => const {'like', 'love', 'celebrate'};
@override
String? emojiCode(String type) {
if (type == 'celebrate') return '🎉';
return super.emojiCode(type);
}
}
StreamChat(
client: client,
streamChatConfigData: StreamChatConfigurationData(
reactionIconResolver: const MyReactionIconResolver(),
),
child: ...,
)Custom Attachment Builders
Register custom attachment builders globally:
class LocationAttachmentBuilder extends StreamAttachmentWidgetBuilder {
@override
bool canHandle(
Message message,
Map<String, List<Attachment>> attachments,
) {
final locationAttachments = attachments['location'];
return locationAttachments != null && locationAttachments.isNotEmpty;
}
@override
Widget build(
BuildContext context,
Message message,
Map<String, List<Attachment>> attachments,
) {
final attachment = attachments['location']!.first;
return LocationMapWidget(
latitude: attachment.extraData['latitude'] as double,
longitude: attachment.extraData['longitude'] as double,
);
}
}
StreamChat(
client: client,
streamChatConfigData: StreamChatConfigurationData(
attachmentBuilders: [
LocationAttachmentBuilder(),
...defaultAttachmentBuilders,
],
),
child: ...,
)Theming
Message styling is split across two theme layers:
Design-system level — StreamMessageItemThemeData controls structural visibility and layout. It is part of StreamTheme (from stream_core_flutter) and can be overridden for a subtree using StreamMessageItemTheme:
StreamMessageItemTheme(
data: StreamMessageItemThemeData(
leadingVisibility: StreamMessageStyleVisibility(
incoming: StreamVisibility.visible,
outgoing: StreamVisibility.gone,
),
footerVisibility: StreamMessageStyleVisibility(
incoming: StreamVisibility.visible,
outgoing: StreamVisibility.visible,
),
incoming: StreamMessageItemStyle(
padding: const EdgeInsets.all(4),
backgroundColor: Colors.white,
),
outgoing: StreamMessageItemStyle(
padding: const EdgeInsets.all(4),
backgroundColor: Colors.blue.shade50,
),
),
child: StreamMessageListView(controller: controller),
)Chat-specific level — StreamMessageThemeData controls text styles, colors, and link appearance for own and other messages. It is configured via StreamChatThemeData:
StreamChatThemeData(
ownMessageTheme: StreamMessageThemeData(
messageTextStyle: const TextStyle(color: Colors.white),
messageBackgroundColor: Colors.blue,
),
otherMessageTheme: StreamMessageThemeData(
messageTextStyle: const TextStyle(color: Colors.black87),
messageBackgroundColor: Colors.grey.shade200,
),
)Customizing the Channel List Item via Component Factory
The component factory also handles channel list items:
StreamComponentFactory(
builders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
channelListItem: (context, props) => StreamChannelListTile(
avatar: StreamChannelAvatar(channel: props.channel),
title: Text(props.channel.name ?? ''),
onTap: props.onTap,
onLongPress: props.onLongPress,
selected: props.selected,
),
),
),
child: ...,
)