StreamMessageListView(
messageBuilder: (context, message, defaultProps) {
// defaultProps is the fully-populated StreamMessageItemProps for this message.
// copyWith overrides only the fields you care about; everything else stays as-is.
return StreamMessageItem.fromProps(
// fromProps rebuilds the default widget using the modified props.
props: defaultProps.copyWith(
swipeToReply: true,
maxWidth: 300,
),
);
},
)Customizing Widgets
The Flutter SDK provides several patterns for customizing and replacing built-in components. Depending on the scope of your change, you can use builder callbacks on individual widgets, StreamComponentFactory for app-wide overrides, or StreamTheme / StreamChatThemeData for visual styling.
Props and copyWith
Most SDK widgets accept a props argument that is a typed object containing all the data the default implementation uses. You can use this to customize the widget by updating the properties, or creating a fully custom widget based on this data.
- Per-widget builder callbacks — when you provide a
messageBuilderoritemBuilder, the callback receivesdefaultPropsalready populated with all the data for that item. See Per-widget builder callbacks below for the full list. StreamComponentFactorybuilders — each slot receives a typedpropsargument with all the data the default implementation uses.
To customize a widget without reimplementing it from scratch, call copyWith() to override only the fields you need. Then pass the result to the component's .fromProps() factory constructor to rebuild the default widget with your overrides applied:
StreamComponentFactory
StreamComponentFactory is the primary extension point for replacing entire component implementations. It uses the StreamComponentBuilders API to register custom builders for each slot.
StreamComponentFactory is an InheritedWidget, so its overrides apply to its subtree only — exactly like Flutter's Theme. Place it once at the root of your app to apply custom builders globally, or nest it deeper in the tree to scope overrides to a single screen or section. When multiple StreamComponentFactory ancestors are present, the nearest one wins for any given slot.
StreamComponentBuilders accepts named slots for both chat-specific components (via streamChatComponentBuilders) and core design-system components such as button, avatar, textInput, and loadingSpinner:
StreamComponentFactory(
builders: StreamComponentBuilders(
// Core component — replace every StreamButton in the subtree.
button: (context, props) {
return FilledButton(
onPressed: props.onPressed,
style: FilledButton.styleFrom(
backgroundColor: props.style == StreamButtonStyle.destructive
? Colors.red
: Theme.of(context).colorScheme.primary,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (props.iconLeft != null) ...[
Icon(props.iconLeft, size: 18),
const SizedBox(width: 6),
],
if (props.child != null) props.child!,
],
),
);
},
// Chat-specific components — passed via the extensions list.
extensions: streamChatComponentBuilders(
channelListItem: (context, props) => MyCustomChannelListItem(props: props),
messageItem: (context, props) => MyCustomMessageWidget(props: props),
),
),
child: MyApp(),
)Each builder receives a typed props object containing all the data the default implementation uses. StreamButtonProps, for example, exposes child, onPressed, style, type, size, iconLeft, iconRight, isSelected, isFloating, themeStyle.
Spread multiple extension sets together when composing overrides from different sources:
extensions: [
...streamChatComponentBuilders(
channelListItem: (context, props) => MyChannelListItem(props: props),
),
...myOtherBuilders(),
]The nearest StreamComponentFactory ancestor wins, so you can scope an override to a single screen without affecting the rest of the app.
What is a builder key?
A builder key is the slot name you pass to StreamComponentBuilders or streamChatComponentBuilders(...). Each key maps to a specific component type. Your builder receives a typed Props object containing the data the default implementation uses. You return the widget you want rendered in its place.
Full signature pattern:
// Core component (direct named parameter on StreamComponentBuilders)
StreamComponentBuilders(
button: (BuildContext context, StreamButtonProps props) {
return FilledButton(onPressed: props.onPressed, child: props.child);
},
)
// Chat-specific component (via the extensions list)
StreamComponentBuilders(
extensions: streamChatComponentBuilders(
messageItem: (BuildContext context, StreamMessageItemProps props) {
// Use props.copyWith() to adjust defaults, or return a completely custom widget.
return DefaultStreamMessageItem(
props: props.copyWith(
actionsBuilder: (context, defaultActions) => [
...defaultActions,
StreamContextMenuAction(
leading: const Icon(Icons.star),
label: const Text('Favourite'),
onTap: () => _favourite(props.message),
),
],
),
);
},
),
)See the Props and copyWith section above for details on working with props classes.
Core components (stream_core_flutter)
These are design-system primitives shared across Stream SDKs. Pass them directly as named parameters on StreamComponentBuilders.
| Builder key | Props class | Description |
|---|---|---|
appBar | StreamAppBarProps | App bar |
avatar | StreamAvatarProps | Single avatar image |
avatarGroup | StreamAvatarGroupProps | Overlapping group of avatars |
avatarStack | StreamAvatarStackProps | Stacked avatar layout |
badgeCount | StreamBadgeCountProps | Numeric unread/count badge |
badgeNotification | StreamBadgeNotificationProps | Notification dot badge |
bottomAppBar | StreamBottomAppBarProps | Bottom app bar |
button | StreamButtonProps | Button |
checkbox | StreamCheckboxProps | Checkbox |
commandChip | StreamCommandChipProps | Slash-command suggestion chip |
contextMenuAction | StreamContextMenuActionProps | Item inside a context menu |
emoji | StreamEmojiProps | Emoji renderer |
emojiButton | StreamEmojiButtonProps | Button that opens the emoji picker |
emojiChip | StreamEmojiChipProps | Single emoji chip (reaction) |
emojiChipBar | StreamEmojiChipBarProps | Row of emoji chips |
errorBadge | StreamErrorBadgeProps | Error badge overlay |
fileTypeIcon | StreamFileTypeIconProps | Icon representing a file type |
imageSourceBadge | StreamImageSourceBadgeProps | Badge showing the image source |
jumpToUnreadButton | StreamJumpToUnreadButtonProps | Floating button to jump to first unread |
listTile | StreamListTileProps | Generic list tile |
loadingSpinner | StreamLoadingSpinnerProps | Loading spinner |
mediaViewer | StreamMediaViewerProps | Full-screen media viewer |
messageAnnotation | StreamMessageAnnotationProps | System/annotation message row |
messageBubble | StreamMessageBubbleProps | Message bubble container |
messageComposerAttachment | StreamMessageComposerAttachmentProps | Generic attachment chip in the composer |
messageComposerEditMessageAttachment | StreamMessageComposerEditMessageAttachmentProps | Edit-message preview in the composer |
messageComposerFileAttachment | StreamMessageComposerFileAttachmentProps | File attachment preview in the composer |
messageComposerLinkPreviewAttachment | StreamMessageComposerLinkPreviewAttachmentProps | Link preview in the composer |
messageComposerMediaAttachment | StreamMessageComposerMediaAttachmentProps | Image/video preview in the composer |
messageComposerReplyAttachment | StreamMessageComposerReplyAttachmentProps | Reply preview in the composer |
messageComposerUnsupportedAttachment | StreamMessageComposerUnsupportedAttachmentProps | Unsupported attachment placeholder in the composer |
messageContent | StreamMessageContentProps | Message content layout (bubble + metadata) |
messageMetadata | StreamMessageMetadataProps | Timestamp and delivery status row |
messageReplies | StreamMessageRepliesProps | Thread reply count link |
messageText | StreamMessageTextProps | Message body (markdown renderer) |
networkImage | StreamNetworkImageProps | Network image with loading/error states |
onlineIndicator | StreamOnlineIndicatorProps | Online presence dot |
playbackSpeedToggle | StreamPlaybackSpeedToggleProps | Audio playback speed button |
progressBar | StreamProgressBarProps | Audio/upload progress bar |
reactionPicker | StreamReactionPickerProps | Reaction picker overlay |
reactions | StreamReactionsProps | Reaction row beneath a message |
retryBadge | StreamRetryBadgeProps | Failed-send retry badge |
sheetHeader | StreamSheetHeaderProps | Bottom sheet header/handle |
skeletonLoading | StreamSkeletonLoadingProps | Shimmer skeleton loading placeholder |
stepper | StreamStepperProps | Numeric increment/decrement stepper |
textInput | StreamTextInputProps | Text input field |
toggleSwitch | StreamSwitchProps | Toggle switch |
Chat-specific components (stream_chat_flutter)
These are chat domain components registered via streamChatComponentBuilders(...) passed to the extensions list.
Channel and thread list
| Builder key | Props class | Description |
|---|---|---|
channelListItem | StreamChannelListItemProps | Row in the channel list |
threadListItem | StreamThreadListTileProps | Row in the thread list |
Example — custom channel list item:
StreamComponentFactory(
builders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
channelListItem: (context, props) {
return StreamChannelListTile(
avatar: StreamChannelAvatar(channel: props.channel),
title: Text(props.channel.name ?? ''),
subtitle: Text(props.channel.lastMessageAt?.toString() ?? ''),
onTap: props.onTap,
onLongPress: props.onLongPress,
selected: props.selected,
);
},
),
),
child: MyApp(),
)Message list
| Builder key | Props class | Description |
|---|---|---|
messageItem | StreamMessageItemProps | Individual message item |
messageLeading | StreamMessageLeadingProps | Leading slot of the message row (avatar area for other users' messages) |
messageHeader | StreamMessageHeaderProps | Header row above the bubble (sender name, thread-parent label, etc.) |
messageFooter | StreamMessageFooterProps | Footer row below the bubble (timestamp, delivery state, edited indicator) |
Example — add a custom context-menu action to every message:
StreamComponentFactory(
builders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
messageItem: (context, props) {
return DefaultStreamMessageItem(
props: props.copyWith(
actionsBuilder: (context, defaultActions) => [
...defaultActions,
StreamContextMenuAction(
leading: const Icon(Icons.star),
label: const Text('Favourite'),
onTap: () => _favourite(props.message),
),
],
),
);
},
),
),
child: MyApp(),
)Message composer
| Builder key | Props class | Description |
|---|---|---|
messageComposer | MessageComposerProps | Full composer widget |
messageComposerLeading | MessageComposerLeadingProps | Left side of the composer bar |
messageComposerTrailing | MessageComposerTrailingProps | Right side of the composer bar |
messageComposerInput | MessageComposerInputProps | Input field area |
messageComposerInputCenter | MessageComposerInputCenterProps | Center section of the input field |
messageComposerInputLeading | MessageComposerInputLeadingProps | Leading icon(s) inside the input field |
messageComposerInputHeader | MessageComposerInputHeaderProps | Header row above the input field |
messageComposerInputTrailing | MessageComposerInputTrailingProps | Trailing icon(s) inside the input field |
messageComposerAttachmentList | StreamMessageComposerAttachmentListProps | Horizontal list of attachment previews |
messageComposerAttachment | StreamMessageComposerAttachmentProps | Individual attachment preview chip |
Message composer
Example — move the send button outside the text input and add an emoji picker inside it:
StreamComponentFactory(
builders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
// Remove send button from inside the input field
messageComposerInputTrailing: (context, props) => const SizedBox.shrink(),
// Render send button outside (to the right of the whole composer bar)
messageComposerTrailing: (context, props) =>
DefaultStreamMessageComposerInputTrailing(props: props),
// Add an emoji button inside the input on the left
messageComposerInputLeading: (context, props) => StreamButton.icon(
icon: Icon(context.streamIcons.emoji),
type: StreamButtonType.ghost,
style: StreamButtonStyle.secondary,
size: StreamButtonSize.small,
onPressed: () { /* open emoji picker */ },
),
),
),
child: StreamMessageComposer(),
)Attachments
| Builder key | Props class | Description |
|---|---|---|
imageAttachment | StreamImageAttachmentProps | Single image attachment |
videoAttachment | StreamVideoAttachmentProps | Video attachment with playback |
giphyAttachment | StreamGiphyAttachmentProps | Giphy/GIF attachment |
galleryAttachment | StreamGalleryAttachmentProps | Multi-image gallery attachment |
fileAttachment | StreamFileAttachmentProps | Generic file attachment |
linkPreviewAttachment | StreamLinkPreviewAttachmentProps | Link preview card |
voiceRecordingAttachment | StreamVoiceRecordingAttachmentProps | Voice recording playback attachment |
pollAttachment | StreamPollAttachmentProps | Poll attachment |
quotedMessage | StreamQuotedMessageProps | Quoted/replied message preview |
unsupportedAttachment | StreamUnsupportedAttachmentProps | Fallback for unrecognised attachment types |
mediaGallery | StreamMediaGalleryProps | Full-screen media gallery |
mediaGalleryPreview | StreamMediaGalleryPreviewProps | Media gallery preview thumbnail strip |
Note: The factory slots above replace the pre-built attachment widget for that type everywhere in the app. If you need to add a new custom attachment type or replace how built-in types render per-message, use StreamAttachmentWidgetBuilder instead — see Custom Attachment Builders.
Per-widget builder callbacks
Many high-level widgets expose itemBuilder or messageBuilder callbacks for customizing individual items without replacing the whole component:
StreamChannelListView(
controller: _controller,
itemBuilder: (context, channels, index, defaultTile) {
final channel = channels[index];
return ListTile(
title: Text(channel.name ?? ''),
onTap: () => openChannel(channel),
);
},
)StreamMessageListView(
messageBuilder: (context, message, defaultProps) {
return StreamMessageItem.fromProps(
props: defaultProps.copyWith(
actionsBuilder: (context, defaultActions) => [
...defaultActions,
StreamContextMenuAction(
leading: const Icon(Icons.star),
label: const Text('Favourite'),
onTap: () => _favourite(message),
),
],
),
);
},
)Theming vs. component replacement
Use theming for visual changes (colors, typography, spacing) and component replacement for structural or behavioral changes.
| Goal | Approach |
|---|---|
| Change colors, fonts, sizing | StreamTheme / StreamChatThemeData |
| Hide or reorder parts of a component | copyWith() on the props class |
| Replace a component entirely | StreamComponentFactory builder |
| Customize a single list item | itemBuilder / messageBuilder callback |
See the Theming page for details on visual customization.