StreamChatTheme(
data: StreamChatThemeData(
colorTheme: StreamColorTheme.light(
accentPrimary: Colors.blue,
),
),
child: MyApp(),
)v10.0
Stream Chat Flutter SDK v10.0.0 Migration Guide
This guide covers all breaking changes in Stream Chat Flutter SDK v10.0.0. Whether you're upgrading from v9.x or from a v10 beta, this document provides the complete migration path.
Table of Contents
- Quick Reference
- Theming
- Channel List Item
- Message Item
- Message List
- Message Actions
- Reactions
- Avatars
- Message Composer
- Attachment Picker
- Image CDN
- Unread Indicator
- Audio
- Icons & Headers
- Localizations
- Media Viewer
- Attachments & Polls
- Message State & Deletion
- File Upload
- onAttachmentTap
- Unread Threads Banner
- Low-Level Client Changes
- Removed APIs
- Migration Checklist
Quick Reference
| Feature Area | Key Changes |
|---|---|
| Theming | StreamChatTheme wrapper replaced by StreamTheme extension on MaterialApp.theme |
| Channel List | StreamChannelListTile → StreamChannelListItem, theme class renames |
| Message Item | Props-based API via StreamMessageItemProps, removed show* booleans and builder callbacks |
| Message Actions | StreamContextMenuAction<T> replaces StreamMessageAction, actionsBuilder replaces customActions |
| Reactions | Reaction object API, StreamMessageReactionPicker, reactionIconResolver, ReactionDetailSheet |
| Avatars | Size enums replace BoxConstraints, StreamGroupAvatar → StreamUserAvatarGroup |
| Message Composer | StreamMessageInput → StreamMessageComposer, hideSendAsDm → canAlsoSendToChannelFromThread (inverted), StreamMessageInputController → StreamMessageComposerController |
| Attachment Picker | Sealed class hierarchy (AttachmentsPicked, PollCreated, AttachmentPickerError), builder pattern for options, typed errors |
| Image CDN | getResizedImageUrl → StreamImageCDN.resolveUrl(), ResizeMode/CropMode enums |
| Unread Indicator | Named constructors, updated UnreadIndicatorButton callbacks |
| Audio | Moved to stream_core_flutter, theming via StreamTheme |
| Icons & Headers | StreamSvgIcon deprecated → Icon(context.streamIcons.*), header default changes |
| Message State | MessageDeleteScope replaces bool hard, delete-for-me support |
| File Upload | Four new abstract methods on AttachmentFileUploader |
| onAttachmentTap | New FutureOr<bool> signature with BuildContext and fallback support |
| Unread Threads Banner | Wrapper pattern with child, enabled, onRefresh; removed onTap, minHeight |
Theming
StreamChatTheme → StreamTheme via MaterialApp extensions
The StreamChatTheme wrapper widget has been replaced by a StreamTheme extension registered on MaterialApp.theme.
Before:
After:
MaterialApp(
theme: ThemeData(
extensions: [
StreamTheme(
brightness: Brightness.light,
colorScheme: StreamColorScheme.light().copyWith(
primary: Colors.blue,
),
),
],
),
home: StreamChat(client: client, child: MyHomePage()),
)Use convenience factories StreamTheme.light() or StreamTheme.dark() as a starting point:
MaterialApp(
theme: ThemeData(extensions: [StreamTheme.light()]),
darkTheme: ThemeData(
brightness: Brightness.dark,
extensions: [StreamTheme.dark()],
),
home: MyHomePage(),
)If no StreamTheme is provided, a default theme is automatically derived from Theme.of(context).brightness.
Channel List Item
StreamChannelListTile → StreamChannelListItem
StreamChannelListTile slot parameters (leading, title, subtitle, trailing) have been removed. The new StreamChannelListItem takes only interaction props; slot customization moves to StreamComponentFactory.
Before:
StreamChannelListTile(
channel: channel,
onTap: () => openChannel(channel),
tileColor: Colors.white,
selectedTileColor: Colors.blue.shade50,
selected: isSelected,
leading: StreamChannelAvatar(channel: channel),
title: StreamChannelName(channel: channel),
)After:
StreamChannelListItem(
channel: channel,
onTap: () => openChannel(channel),
selected: isSelected,
)To customize slots, use StreamComponentFactory:
StreamComponentFactory(
builders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
channelListItem: (context, props) => StreamChannelListTile(
avatar: StreamChannelAvatar(channel: props.channel),
title: Text(props.channel.name ?? ''),
onTap: props.onTap,
selected: props.selected,
),
),
),
child: MyApp(),
)Theme: StreamChannelPreviewThemeData → StreamChannelListItemThemeData
| Old | New |
|---|---|
StreamChatThemeData.channelPreviewTheme | StreamChatThemeData.channelListItemTheme |
StreamChannelPreviewThemeData | StreamChannelListItemThemeData |
StreamChannelPreviewTheme.of(context) | StreamChannelListItemTheme.of(context) |
lastMessageAtStyle | timestampStyle |
lastMessageAtFormatter (theme prop) | ChannelLastMessageDate(formatter: ...) widget param |
tileColor / selectedTileColor | backgroundColor: WidgetStateProperty<Color?> |
Before:
StreamChatThemeData(
channelPreviewTheme: StreamChannelPreviewThemeData(
titleStyle: TextStyle(fontWeight: FontWeight.bold),
lastMessageAtStyle: TextStyle(fontSize: 12),
unreadCounterColor: Colors.red,
),
)After:
StreamChatThemeData(
channelListItemTheme: StreamChannelListItemThemeData(
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
timestampStyle: const TextStyle(fontSize: 12),
// unreadCounterColor → use StreamBadgeNotificationThemeData
),
)State-dependent colors:
StreamChannelListItemThemeData(
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) return Colors.blue.shade50;
return Colors.white;
}),
)Message Item
Props-based API
The old 50+ parameter StreamMessageItem is now a thin shell. Configuration moves to StreamMessageItemProps with copyWith().
Before (messageBuilder with copyWith on widget):
StreamMessageListView(
messageBuilder: (context, details, messages, defaultWidget) {
return defaultWidget.copyWith(showReactions: false);
},
)After (messageBuilder with copyWith on props):
StreamMessageListView(
messageBuilder: (context, message, defaultProps) {
return StreamMessageItem.fromProps(
props: defaultProps.copyWith(
actionsBuilder: (context, actions) => actions,
),
);
},
)Removed Visibility Booleans
All show* boolean parameters have been removed. Visibility is now controlled via StreamMessageItemThemeData and channel permissions:
| Old Parameter | Migration Path |
|---|---|
showReactions | StreamMessageItemThemeData visibility |
showDeleteMessage / showEditMessage | Channel permissions |
showUsername / showTimestamp / showSendingIndicator | StreamMessageItemThemeData.metadataVisibility |
showUserAvatar | StreamMessageItemThemeData.avatarVisibility |
showThreadReplyIndicator | StreamMessageItemThemeData.repliesVisibility |
showErrorBadge | StreamMessageItemThemeData.errorBadgeVisibility |
showAnnotations | StreamMessageItemThemeData.annotationVisibility |
showReactionTail | Tail is shown automatically when the picker is visible |
Removed Builder Callbacks
| Old Parameter | Migration Path |
|---|---|
userAvatarBuilder | Component factory (replace DefaultStreamMessageItem) |
textBuilder | Component factory (replace StreamMessageContent) |
quotedMessageBuilder | Component factory (replace StreamMessageContent) |
deletedMessageBuilder | Component factory (replace StreamMessageContent) |
reactionPickerBuilder | StreamChatConfigurationData.reactionIconResolver |
customActions | actionsBuilder on StreamMessageItemProps |
onCustomActionTap | onTap per StreamContextMenuAction |
Changed Callback Signatures
| Old | New |
|---|---|
onLinkTap: void Function(String url) | onMessageLinkTap: void Function(Message, String) |
onMentionTap: void Function(User) | onUserMentionTap: void Function(User) |
onQuotedMessageTap: void Function(String?) | onQuotedMessageTap: void Function(Message) |
Typedef Changes
| Old | New |
|---|---|
MessageBuilder = Widget Function(BuildContext, MessageDetails, List<Message>, StreamMessageItem) | StreamMessageItemBuilder = Widget Function(BuildContext, Message, StreamMessageItemProps) |
ParentMessageBuilder | StreamMessageItemBuilder |
StreamDeletedMessage | StreamMessageDeleted |
Theme: Automatic Resolution
The messageTheme parameter has been removed. Theme is resolved automatically from context:
// Before
StreamMessageItem(message: message, messageTheme: streamTheme.ownMessageTheme)
// After
StreamMessageItem(message: message)Message Actions
StreamMessageAction → StreamContextMenuAction<T>
StreamMessageAction (data class) and StreamMessageActionItem (render widget) have been merged into StreamContextMenuAction<T>, a self-rendering widget.
Property mapping:
StreamMessageAction | StreamContextMenuAction |
|---|---|
action: T | value: T? |
title: Widget? | label: Widget (now required) |
leading: Widget? | leading: Widget? |
isDestructive: bool | isDestructive: bool or .destructive constructor |
Before:
StreamMessageAction(
action: CopyMessage(message: message),
leading: const StreamSvgIcon(icon: StreamSvgIcons.copy),
title: Text('Copy'),
)After:
StreamContextMenuAction<MessageAction>(
value: CopyMessage(message: message),
leading: Icon(context.streamIcons.copy),
label: const Text('Copy'),
onTap: () => _copyMessage(message),
)customActions + onCustomActionTap → actionsBuilder
Before:
StreamMessageItem(
message: message,
customActions: [
StreamMessageAction(
action: CustomMessageAction(message: message),
leading: const Icon(Icons.star),
title: const Text('Favourite'),
),
],
onCustomActionTap: (action) => _favourite(action.message),
)After:
StreamMessageItem(
message: message,
actionsBuilder: (context, defaultActions) => [
...defaultActions,
StreamContextMenuAction(
leading: const Icon(Icons.star),
label: const Text('Favourite'),
onTap: () => _favourite(message),
),
],
)showStreamDialog
Replace showDialog with showStreamDialog when presenting Stream modals:
final action = await showStreamDialog<MessageAction>(
context: context,
builder: (_) => StreamMessageActionsModal(...),
);Reactions
The reaction system has been updated with a unified Reaction object API, renamed widgets, and a resolver-based icon system.
SendReaction
Key Changes
sendReactionmethod now accepts a fullReactionobject instead of individual parameters.
Migration Steps
Before:
channel.sendReaction(
message,
'like',
score: 1,
extraData: {'custom_field': 'value'},
);
client.sendReaction(
messageId,
'love',
enforceUnique: true,
extraData: {'custom_field': 'value'},
);After:
channel.sendReaction(
message,
Reaction(
type: 'like',
score: 1,
emojiCode: '👍',
extraData: {'custom_field': 'value'},
),
);
client.sendReaction(
messageId,
Reaction(
type: 'love',
emojiCode: '❤️',
extraData: {'custom_field': 'value'},
),
enforceUnique: true,
);Important:
- The
sendReactionmethod now requires aReactionobject- Optional parameters like
enforceUniqueandskipPushremain as method parameters- You can now specify custom emoji codes for reactions using the
emojiCodefield
StreamMessageReactionPicker
Key Changes
StreamReactionPickerhas been renamed toStreamMessageReactionPickeronReactionPickedis optional
Migration Steps
Before:
StreamReactionPicker(message: message)After:
StreamMessageReactionPicker(
message: message,
onReactionPicked: onReactionPicked,
)Note:
StreamReactionPickerstill exists instream_core_flutter, but as a primitive that takesitems: List<StreamReactionPickerItem>,onReactionPicked, andonAddReactionTap. The chat-domain wrapper for v9 callers is the newStreamMessageReactionPicker(message,onReactionPicked).
reactionIcons → reactionIconResolver
Migration Steps
Before:
StreamChatConfigurationData(
reactionIcons: [/* list of icon builders */],
)After:
StreamChatConfigurationData(
reactionIconResolver: const MyReactionIconResolver(),
)Extend DefaultReactionIconResolver to customize the available reactions:
class MyReactionIconResolver extends DefaultReactionIconResolver {
const MyReactionIconResolver();
@override
Set<String> get defaultReactions => const {'like', 'love', 'haha', 'wow', 'sad'};
}MessageReactionsModal → ReactionDetailSheet
Migration Steps
Before:
showDialog(
context: context,
builder: (_) => MessageReactionsModal(message: message),
)After:
await ReactionDetailSheet.show(context: context, message: message)Important: Await the
ReactionDetailSheet.show()call to get the result instead of providing anonReactionPickedcallback.
Avatars
constraints → size enum
All avatar components now use size enums instead of BoxConstraints.
Before:
StreamUserAvatar(
user: user,
constraints: BoxConstraints.tight(const Size(40, 40)),
showOnlineStatus: true,
onTap: (user) => showProfile(user),
)After:
GestureDetector(
onTap: () => showProfile(user),
child: StreamUserAvatar(
user: user,
size: StreamAvatarSize.lg, // 40px
showOnlineIndicator: true,
),
)Important:
showOnlineStatusis renamed toshowOnlineIndicatoronTaphas been removed from avatar widgets — wrap withGestureDetectororInkWellinstead
StreamGroupAvatar → StreamUserAvatarGroup
// Before
StreamGroupAvatar(
channel: channel,
members: otherMembers,
constraints: BoxConstraints.tight(const Size(40, 40)),
)
// After
StreamUserAvatarGroup(
users: otherMembers.map((m) => m.user!),
size: StreamAvatarGroupSize.lg,
)New: StreamUserAvatarStack
For overlapping avatar stacks (e.g. thread participants):
StreamUserAvatarStack(
users: threadParticipants,
size: StreamAvatarStackSize.xs,
max: 3,
)Message Composer
StreamMessageInput → StreamMessageComposer
The full-featured composer widget has been renamed. The behaviour, parameters, and customisation hooks are unchanged — only the class name is different.
// Before
StreamMessageInput()
// After
StreamMessageComposer()hideSendAsDm → canAlsoSendToChannelFromThread (logic inverted)
| Old | New |
|---|---|
hideSendAsDm: true | canAlsoSendToChannelFromThread: false |
hideSendAsDm: false | canAlsoSendToChannelFromThread: true (new default) |
// Before
StreamMessageInput(hideSendAsDm: true)
// After
StreamMessageComposer(canAlsoSendToChannelFromThread: false)StreamMessageInputController → StreamMessageComposerController
stream_chat_flutter_core renamed the composer's controller class for consistency with StreamMessageComposer, and changed edit-mode semantics.
Renames:
| Old | New |
|---|---|
StreamMessageInputController | StreamMessageComposerController |
StreamRestorableMessageInputController | StreamRestorableMessageComposerController |
StreamMessageComposerController.editingOriginalMessage | messageBeingEdited |
StreamMessageComposer.messageInputController param | messageComposerController |
Edit-mode semantics:
- The constructor no longer accepts a non-initial message. To enter edit mode, call
editMessage(message)after constructing. cancelEditMessage()is now a no-op when not in edit mode.clear()no longer exits edit mode; usecancelEditMessage()explicitly.
Before:
final controller = StreamMessageComposerController(message: existingMessage);
controller.clear(); // also exited edit mode
StreamMessageComposer(
messageInputController: controller,
)After:
final controller = StreamMessageComposerController();
controller.editMessage(existingMessage); // enter edit mode explicitly
controller.cancelEditMessage(); // exit edit mode
StreamMessageComposer(
messageComposerController: controller,
)Attachment Picker
The attachment picker system has been redesigned with a sealed class hierarchy, improved type safety, and a flexible builder pattern for customization.
AttachmentPickerType
Key Changes
AttachmentPickerTypeenum replaced with sealed class hierarchy- Now supports extensible custom types like contact and location pickers
- Use built-in types like
AttachmentPickerType.imagesor define your own viaCustomAttachmentPickerType
Migration Steps
Before:
// Using enum-based attachment types
final attachmentType = AttachmentPickerType.images;After:
// Using sealed class attachment types
final attachmentType = AttachmentPickerType.images;
// For custom types
class LocationAttachmentPickerType extends CustomAttachmentPickerType {
const LocationAttachmentPickerType();
}Important: The enum is now a sealed class, but the basic usage remains the same for built-in types.
StreamAttachmentPickerOption
Key Changes
StreamAttachmentPickerOptionreplaced with two sealed classes:SystemAttachmentPickerOptionfor system pickers (camera, files)TabbedAttachmentPickerOptionfor tabbed pickers (gallery, polls, location)
Migration Steps
Before:
final option = AttachmentPickerOption(
title: 'Gallery',
icon: Icons.photo_library,
supportedTypes: [AttachmentPickerType.images, AttachmentPickerType.videos],
optionViewBuilder: (context, controller) {
return GalleryPickerView(controller: controller);
},
);
final webOrDesktopOption = WebOrDesktopAttachmentPickerOption(
title: 'File Upload',
icon: Icons.upload_file,
type: AttachmentPickerType.files,
);After:
// For custom UI pickers (gallery, polls)
final tabbedOption = TabbedAttachmentPickerOption(
title: 'Gallery',
icon: Icons.photo_library,
supportedTypes: [AttachmentPickerType.images, AttachmentPickerType.videos],
optionViewBuilder: (context, controller) {
return GalleryPickerView(controller: controller);
},
);
// For system pickers (camera, file dialogs)
final systemOption = SystemAttachmentPickerOption(
title: 'Camera',
icon: Icons.camera_alt,
supportedTypes: [AttachmentPickerType.images],
onTap: (context, controller) => pickFromCamera(),
);Important:
- Use
SystemAttachmentPickerOptionfor system pickers (camera, file dialogs)- Use
TabbedAttachmentPickerOptionfor custom UI pickers (gallery, polls)
showStreamAttachmentPickerModalBottomSheet
The previous showStreamAttachmentPickerModalBottomSheet modal helper has been removed. The attachment picker is now inline inside StreamMessageComposer. To customize the picker, pass an attachmentPickerOptionsBuilder (or tabbedAttachmentPickerBuilder / systemAttachmentPickerBuilder) to StreamMessageComposer.
See the StreamMessageComposer constructor for the full set of customization options.
AttachmentPickerBottomSheet
StreamTabbedAttachmentPicker and StreamSystemAttachmentPicker are now inline widgets embedded inside StreamMessageComposer. They are no longer presented as bottom sheets. To customize them, pass tabbedAttachmentPickerBuilder or systemAttachmentPickerBuilder to StreamMessageComposer.
customAttachmentPickerOptions
Key Changes
customAttachmentPickerOptionshas been removed. UseattachmentPickerOptionsBuilderinstead.- New builder pattern provides access to default options which can be modified, reordered, or extended.
Migration Steps
Before:
StreamMessageInput(
customAttachmentPickerOptions: [
TabbedAttachmentPickerOption(
key: 'custom-location',
icon: Icons.location_on,
supportedTypes: [AttachmentPickerType.images],
optionViewBuilder: (context, controller) {
return CustomLocationPicker();
},
),
],
)After:
StreamMessageComposer(
attachmentPickerOptionsBuilder: (context, defaultOptions) {
return [
...defaultOptions,
TabbedAttachmentPickerOption(
key: 'custom-location',
icon: Icons.location_on,
supportedTypes: [AttachmentPickerType.images],
optionViewBuilder: (context, controller) {
return CustomLocationPicker();
},
),
];
},
)Example: Filtering default options
StreamMessageComposer(
attachmentPickerOptionsBuilder: (context, defaultOptions) {
// Remove poll option
return defaultOptions.where((option) => option.key != 'poll').toList();
},
)Example: Reordering options
StreamMessageComposer(
attachmentPickerOptionsBuilder: (context, defaultOptions) {
return defaultOptions.reversed.toList();
},
)Important:
- The builder pattern gives you access to default options, allowing more flexible customization
- The builder works with both mobile (tabbed) and desktop (system) pickers
onCustomAttachmentPickerResult
Key Changes
onCustomAttachmentPickerResulthas been removed. UseonAttachmentPickerResultwhich returnsFutureOr<bool>.- Result handler can now short-circuit default behavior by returning
true.
Migration Steps
Before:
StreamMessageInput(
onCustomAttachmentPickerResult: (result) {
if (result is CustomAttachmentPickerResult) {
final data = result.data;
// Handle custom result
}
},
)After:
StreamMessageComposer(
onAttachmentPickerResult: (result) {
if (result is CustomAttachmentPickerResult) {
final data = result.data;
// Handle custom result
return true; // Indicate we handled it - skips default processing
}
return false; // Let default handler process other result types
},
)Important:
onAttachmentPickerResultreplacesonCustomAttachmentPickerResultand must return a boolean- Return
truefromonAttachmentPickerResultto skip default handling- Return
falseto allow the default handler to process the result
StreamAttachmentPickerController
Key Changes
- Replaced
ArgumentError('The size of the attachment is...')withAttachmentTooLargeError. - Replaced
ArgumentError('The maximum number of attachments is...')withAttachmentLimitReachedError.
Migration Steps
Before:
try {
await controller.addAttachment(attachment);
} on ArgumentError catch (e) {
showError(e.message);
}After:
try {
await controller.addAttachment(attachment);
} on AttachmentTooLargeError catch (e) {
showError('File is too large. Max size is ${e.maxSize} bytes.');
} on AttachmentLimitReachedError catch (e) {
showError('Cannot add more attachments. Maximum is ${e.maxCount}.');
}Important:
- Replace
ArgumentErrorcatches with the specific typed errorsAttachmentTooLargeErrorprovidesfileSizeandmaxSizepropertiesAttachmentLimitReachedErrorprovidesmaxCountproperty
Image CDN
getResizedImageUrl → StreamImageCDN.resolveUrl
Before:
final url = imageUrl.getResizedImageUrl(
width: 200,
height: 300,
resize: 'clip',
crop: 'center',
);After:
const imageCDN = StreamImageCDN();
final url = imageCDN.resolveUrl(
imageUrl,
resize: ImageResize(
width: 200,
height: 300,
mode: ResizeMode.clip,
crop: CropMode.center,
),
);
final cacheKey = imageCDN.cacheKey(url); // stable key for CachedNetworkImageFor custom CDN support, extend StreamImageCDN and inject via StreamChatConfigurationData.imageCDN.
Attachment Thumbnail Parameters
The three separate thumbnail params on attachment widgets are replaced by a single resize parameter:
// Before
StreamImageAttachmentThumbnail(
image: attachment,
thumbnailSize: const Size(200, 300),
thumbnailResizeType: 'clip',
thumbnailCropType: 'center',
)
// After
StreamImageAttachmentThumbnail(
image: attachment,
resize: ImageResize(width: 200, height: 300, mode: ResizeMode.clip, crop: CropMode.center),
)Unread Indicator
StreamUnreadIndicator
Styling params (backgroundColor, textColor, textStyle) removed — styling is now controlled via StreamTheme.
Named constructors added:
StreamUnreadIndicator() // total unread messages
StreamUnreadIndicator.channels() // unread channels
StreamUnreadIndicator.threads() // unread threadsUnreadIndicatorButton
Callback signatures changed:
// Before
UnreadIndicatorButton(
onTap: () => _scrollToUnread(),
onDismiss: () => _markAllRead(),
)
// After
UnreadIndicatorButton(
onJumpTap: (lastReadMessageId) async => _scrollToUnread(lastReadMessageId),
onDismissTap: () async => _markAllRead(),
)Audio
StreamAudioWaveform and StreamAudioWaveformSlider have moved to stream_core_flutter but are re-exported via stream_chat_flutter, so import paths are unchanged.
Audio waveform theming moves from StreamChatThemeData.audioWaveformTheme to StreamTheme (via MaterialApp.theme.extensions).
// Before
StreamChatThemeData(
audioWaveformTheme: StreamAudioWaveformThemeData(waveColor: Colors.blue),
)
// After
MaterialApp(
theme: ThemeData(
extensions: [
StreamTheme(brightness: Brightness.light, /* audio waveform props in StreamTheme */),
],
),
)Icons & Headers
StreamSvgIcon Deprecated
StreamSvgIcon and StreamSvgIcons are now @Deprecated. Replace all usages with the standard Flutter Icon widget and the new StreamIcons token set accessed via context.streamIcons.
Before:
StreamSvgIcon(icon: StreamSvgIcons.reply)
StreamSvgIcon(icon: StreamSvgIcons.copy, color: Colors.red, size: 24)After:
Icon(context.streamIcons.reply)
Icon(context.streamIcons.copy, color: Colors.red, size: 24)context.streamIcons reads from the nearest StreamTheme in the widget tree.
Header Widget Changes
StreamChannelHeader, StreamChannelListHeader, StreamThreadHeader, and StreamGalleryHeader have been rebuilt on the new design-system StreamAppBar. The centerTitle, elevation, scrolledUnderElevation, bottomOpacity, bottom, and backgroundColor parameters are removed — the new bar always centres the title and draws a hairline border divider instead of an elevation shadow.
To override the background colour for a single header, use the new style: parameter:
// Before (removed):
StreamChannelHeader(elevation: 1, centerTitle: false)
// After:
StreamChannelHeader(
style: StreamAppBarStyle(backgroundColor: Colors.white),
)For full slot and theme migration details, see the migrations/redesign/headers_and_icons.md guide.
StreamChat.componentBuilders
StreamChat now accepts an optional componentBuilders parameter that automatically inserts a StreamComponentFactory into the widget tree:
Before:
StreamComponentFactory(
builders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
messageItem: (context, props) => MyMessage(props: props),
),
),
child: StreamChat(client: client, child: MyApp()),
)After:
StreamChat(
client: client,
componentBuilders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
messageItem: (context, props) => MyMessage(props: props),
),
),
child: MyApp(),
)StreamChatConfigurationData New Fields
Three new optional fields have been added:
| Field | Type | Default | Description |
|---|---|---|---|
attachmentBuilders | List<StreamAttachmentWidgetBuilder>? | null | Custom attachment widget builders, prepended to built-in builders |
reactionType | StreamReactionsType? | null | Controls visual style of the reactions display |
reactionPosition | StreamReactionsPosition? | null | Controls where reactions appear relative to the message bubble |
Note: The
imageCDNfield was also added — see the Image CDN section.
Localizations
New Required Abstract Members
If you have a custom Translations subclass, it will fail to compile unless you add implementations for the following new abstract members:
// Channel/message list empty states
@override
String get noConversationsYetText => 'No conversations yet';
@override
String get replyToStartThreadText => 'Reply to a message to start a thread';
@override
String get sendMessageToStartConversationText => 'Send a message to start the conversation';
// Message annotation labels
@override
String get savedForLaterLabel => 'Saved for later';
@override
String get repliedToThreadAnnotationLabel => 'Replied to a thread';
@override
String get alsoSentInChannelAnnotationLabel => 'Also sent in channel';
@override
String get viewLabel => 'View';
// Reminder labels
@override
String get reminderSetLabel => 'Reminder set';
@override
String reminderAtText(String time) => 'Today at $time';
// Channel list attachment previews
@override
String get fileAttachmentText => 'File';
@override
String get linkAttachmentText => 'Link';
@override
String filesAttachmentCountText(int count) => count == 1 ? 'File' : '$count files';
@override
String photosAttachmentCountText(int count) => count == 1 ? 'Photo' : '$count photos';
@override
String videosAttachmentCountText(int count) => count == 1 ? 'Video' : '$count videos';
// Attachment picker labels
@override
String get createPollPromptLabel => 'Create a poll and let everyone vote!';
@override
String get takePhotoAndShareLabel => 'Take a photo and share';
@override
String get takeVideoAndShareLabel => 'Take a video and share';
@override
String get openCameraLabel => 'Open camera';
@override
String get selectFilesToShareLabel => 'Select files to share';
@override
String get openFilesLabel => 'Open files';
// Reactions list / detail sheet
@override
String get emptyReactionsText => 'No reactions yet';
@override
String get loadingReactionsError => 'Error loading reactions';
@override
String get tapToRemoveReactionLabel => 'Tap to remove';
@override
String reactionsCountText(int count) =>
count == 1 ? '1 Reaction' : '$count Reactions';
// Confirmation dialogs
@override
String get confirmLabel => 'CONFIRM';
// Relative timestamps
@override
String get justNowLabel => 'Just now';
// Composer reply header
@override
String replyToUserLabel(String userName) => 'Reply to $userName';
// Poll creator toggle descriptions
@override
String get multipleAnswersDescription => 'Select more than one option';
@override
String maximumVotesPerPersonDescription([Range<int>? range]) {
final (:min, :max) = range ?? (min: 2, max: 10);
return 'Choose between $min\u2013$max options';
}
@override
String get anonymousPollDescription => 'Hide who voted';
@override
String get suggestAnOptionDescription => 'Let others add options';
@override
String get addACommentDescription => 'Allow others to add comments';
// Channel header subtitle for group channels
@override
String membersCountWithOnlineText({
required int memberCount,
required int onlineCount,
}) {
final members = membersCountText(memberCount);
if (onlineCount <= 0) return members;
return '$members, ${watchersCountText(onlineCount)}';
}
// Composer placeholder for user-target commands (`/mute`, `/unmute`, `/ban`, `/unban`)
@override
String get commandUsernameLabel => '@username';
// Poll results dialog footer
@override
String totalVoteCountLabel({int? count}) => switch (count) {
null || < 1 => '0 votes total',
1 => '1 vote total',
_ => '$count votes total',
};
// Generic "view all" CTA
@override
String get viewAllLabel => 'View all';
// Poll option votes dialog app bar title
@override
String get pollVotesLabel => 'Votes';
// Poll end-vote confirmation dialog body
@override
String get endVoteConfirmationMessage =>
'Do you want to end this poll now? Nobody will be able to vote in this poll anymore.';The values shown above are the English defaults from
DefaultTranslations. Provide your own translated strings in place of these.
Renamed Abstract Members
| Old member | New member | Notes |
|---|---|---|
String get questionsLabel | String questionLabel({bool isPlural = false}) | Pass isPlural: true for the plural "Questions" form. |
String get endVoteConfirmationText | String get endVoteConfirmationTitle | Renamed to reflect it's the dialog title, not body text. |
String get slowModeOnLabel | String slowModeOnLabel(int cooldownTimeOut) | Now takes the remaining cooldown seconds for a live countdown display. |
Before:
@override
String get questionsLabel => 'Questions';After:
@override
String questionLabel({bool isPlural = false}) {
if (isPlural) return 'Questions';
return 'Question';
}Changed Default String Values
The following strings changed their default English value. If you have not overridden them in a custom Translations subclass you do not need to do anything:
| Getter | Old default | New default |
|---|---|---|
threadReplyCountText(int) | '$count Thread Replies' | count == 1 ? '1 reply' : '$count replies' |
alsoSendAsDirectMessageLabel | 'Also send as direct message' | 'Also send in Channel' |
addMoreFilesLabel | 'Add more files' | 'Add more' |
emptyMessagesText | 'There are no messages currently' | 'No messages yet' |
writeAMessageLabel | 'Write a message' | 'Send a message' |
endVoteConfirmationTitle (was endVoteConfirmationText) | 'Are you sure you want to end the vote?' | 'End This Poll?' |
endVoteLabel | 'End Vote' | 'End Poll' |
StreamThreadHeader's default title now sources from the new threadLabel translation key (defaulting to 'Thread') rather than threadReplyLabel. Override threadLabel in your custom Translations if you need a different header.
Media Viewer
StreamFullScreenMedia → StreamMediaGalleryPreview
StreamFullScreenMedia and its platform variants (FullScreenMediaDesktop, StreamFullScreenMediaBuilder, FullScreenMediaWidget) have been replaced by a single StreamMediaGalleryPreview widget that works across all platforms.
Removed parameters: userName, sentAt, onReplyMessage, onShowMessage, attachmentActionsModalBuilder.
Renamed parameters: mediaAttachmentPackages → attachments, startIndex → initialIndex.
Before:
StreamFullScreenMedia(
mediaAttachmentPackages: [
for (final a in message.attachments)
StreamAttachmentPackage(attachment: a, message: message),
],
startIndex: 3,
userName: message.user!.name,
autoplayVideos: false,
onShowMessage: handleShowInChat,
)After:
StreamMediaGalleryPreview(
attachments: message.toMediaGalleryAttachments(
filter: (a) =>
a.type == AttachmentType.image ||
a.type == AttachmentType.video ||
a.type == AttachmentType.giphy,
),
initialIndex: 3,
autoplayVideos: false,
)StreamAttachmentPackage → StreamMediaGalleryAttachment
StreamAttachmentPackage has been renamed to StreamMediaGalleryAttachment. Use the new message.toMediaGalleryAttachments({filter}) extension to produce a list from a message.
StreamGalleryHeader → StreamMediaGalleryPreviewHeader
StreamGalleryHeader is renamed to StreamMediaGalleryPreviewHeader. The constructor now takes title and subtitle widget slots instead of userName, sentAt, message, and attachment properties.
StreamGalleryFooter → StreamMediaGalleryPreviewFooter
StreamGalleryFooter is renamed to StreamMediaGalleryPreviewFooter. The footer is automatically wired when using StreamMediaGalleryPreview — only relevant if you compose your own chrome.
Removed Themes
| Removed | Replacement |
|---|---|
StreamGalleryFooterThemeData | StreamBottomAppBarThemeData |
StreamChatThemeData.galleryFooterTheme / imageFooterTheme | Use StreamBottomAppBarThemeData |
StreamChatThemeData.galleryHeaderTheme | Use StreamAppBarThemeData |
StreamAvatarThemeData | Is no longer re-exported from stream_chat_flutter. The class is still alive in stream_core_flutter — import it directly when you need to theme avatars globally: import 'package:stream_core_flutter/stream_core_flutter.dart'; |
Removed Callbacks
onShowMessage and attachmentActionsModalBuilder have been removed from both StreamMessageItem (and its props) and StreamMessageListView. Replace the gallery preview via the component factory (mediaGalleryPreview:) and surface those actions in your own chrome if needed.
Attachments & Polls
Attachment Widget Changes
All attachment widgets now follow a Props + Component Factory pattern. The public constructor API is largely unchanged, but several parameters have been removed or consolidated.
Changes that apply to all attachment widgets:
shapeparameter removedconstraintschanged from required to optional
Per-widget changes:
| Widget | Additional Changes |
|---|---|
StreamImageAttachment | imageThumbnailSize/imageThumbnailResizeType/imageThumbnailCropType → single ImageResize? resize |
StreamFileAttachment | backgroundColor removed |
StreamUrlAttachment | Renamed to StreamLinkPreviewAttachment; messageTheme and hostDisplayName removed |
StreamUrlAttachment → StreamLinkPreviewAttachment
Before:
StreamUrlAttachment(
message: message,
urlAttachment: attachment,
messageTheme: theme.ownMessageTheme,
hostDisplayName: 'GitHub',
)After:
StreamLinkPreviewAttachment(
message: message,
urlAttachment: attachment,
)The corresponding builder was also renamed from UrlAttachmentBuilder to LinkPreviewAttachmentBuilder.
Attachment Builders
All builders have had their shape and padding parameters removed. If you subclass any attachment builder, update to use the new Props-based attachment constructors.
StreamPollInteractorThemeData
The theme has been fully redesigned with a structured theme using design system tokens:
| Removed Property | Replacement |
|---|---|
pollTitleStyle | titleTextStyle |
pollSubtitleStyle | subtitleTextStyle |
pollOptionTextStyle, pollOptionVoteCountTextStyle | optionStyle (StreamPollOptionStyle) |
pollOptionCheckbox* properties | optionStyle.checkboxStyle (StreamCheckboxStyle) |
pollOptionVotesProgressBar* properties | optionStyle.progressBarStyle (StreamProgressBarStyle) |
pollActionButtonStyle | primaryActionStyle / secondaryActionStyle (StreamButtonThemeStyle) |
pollActionDialog* properties | Removed |
Before:
StreamPollInteractorThemeData(
pollTitleStyle: TextStyle(fontWeight: FontWeight.bold),
pollActionButtonStyle: ButtonStyle(...),
pollOptionVotesProgressBarValueColor: Colors.blue,
)After:
StreamPollInteractorThemeData(
titleTextStyle: TextStyle(fontWeight: FontWeight.bold),
primaryActionStyle: StreamButtonThemeStyle.from(borderColor: Colors.blue),
optionStyle: StreamPollOptionStyle(
progressBarStyle: StreamProgressBarStyle(fillColor: Colors.blue),
),
)StreamVoiceRecordingAttachmentThemeData
The theme has been fully redesigned:
| Removed Property | Replacement |
|---|---|
backgroundColor | Removed (handled by attachment container) |
playIcon, pauseIcon, loadingIndicator | Removed (handled by controlButtonStyle) |
audioControlButtonStyle | controlButtonStyle (StreamButtonThemeStyle) |
speedControlButtonStyle | speedToggleStyle (StreamPlaybackSpeedToggleStyle) |
audioWaveformSliderTheme | waveformStyle (StreamAudioWaveformThemeData) |
New: activeDurationTextStyle for duration display while playing.
Before:
StreamVoiceRecordingAttachmentThemeData(
backgroundColor: Colors.grey,
audioControlButtonStyle: ButtonStyle(...),
durationTextStyle: TextStyle(...),
)After:
StreamVoiceRecordingAttachmentThemeData(
controlButtonStyle: StreamButtonThemeStyle.from(...),
durationTextStyle: TextStyle(...),
activeDurationTextStyle: TextStyle(...),
)Message State & Deletion
Message deletion now supports scoped deletion modes including delete-for-me functionality.
MessageState
Key Changes
MessageStatefactory constructors now acceptMessageDeleteScopeinstead ofbool hardparameter- Pattern matching callbacks in state classes now receive
MessageDeleteScope scopeinstead ofbool hard - New delete-for-me functionality with dedicated states and methods
Migration Steps
Before:
final deletingState = MessageState.deleting(hard: true);
final deletedState = MessageState.deleted(hard: false);
final failedState = MessageState.deletingFailed(hard: true);
message.state.whenOrNull(
deleting: (hard) => handleDeleting(hard),
deleted: (hard) => handleDeleted(hard),
deletingFailed: (hard) => handleDeletingFailed(hard),
);After:
final deletingState = MessageState.deleting(
scope: MessageDeleteScope.hardDeleteForAll,
);
final deletedState = MessageState.deleted(
scope: MessageDeleteScope.softDeleteForAll,
);
final failedState = MessageState.deletingFailed(
scope: MessageDeleteScope.deleteForMe(),
);
message.state.whenOrNull(
deleting: (scope) => handleDeleting(scope.hard),
deleted: (scope) => handleDeleted(scope.hard),
deletingFailed: (scope) => handleDeletingFailed(scope.hard),
);
// New delete-for-me functionality
channel.deleteMessageForMe(message); // Delete only for current user
client.deleteMessageForMe(messageId); // Delete only for current user
// Check delete-for-me states
if (message.state.isDeletingForMe) { /* ... */ }
if (message.state.isDeletedForMe) { /* ... */ }
if (message.state.isDeletingForMeFailed) { /* ... */ }Important:
- All
MessageStatefactory constructors now requireMessageDeleteScopeparameter- Pattern matching callbacks receive
MessageDeleteScopeinstead ofbool hard- Use
scope.hardto access the hard delete boolean value- New delete-for-me methods are available on both
ChannelandStreamChatClient
File Upload
The file uploader interface has been expanded with standalone upload and removal methods.
AttachmentFileUploader
Key Changes
AttachmentFileUploaderinterface now includes four new abstract methods:uploadImage,uploadFile,removeImage, andremoveFile.- Custom implementations must implement these new standalone upload/removal methods.
Migration Steps
Before:
class CustomAttachmentFileUploader implements AttachmentFileUploader {
@override
Future<SendImageResponse> sendImage(/* ... */) async { /* ... */ }
@override
Future<SendFileResponse> sendFile(/* ... */) async { /* ... */ }
@override
Future<EmptyResponse> deleteImage(/* ... */) async { /* ... */ }
@override
Future<EmptyResponse> deleteFile(/* ... */) async { /* ... */ }
}After:
class CustomAttachmentFileUploader implements AttachmentFileUploader {
@override
Future<SendImageResponse> sendImage(/* ... */) async { /* ... */ }
@override
Future<SendFileResponse> sendFile(/* ... */) async { /* ... */ }
@override
Future<EmptyResponse> deleteImage(/* ... */) async { /* ... */ }
@override
Future<EmptyResponse> deleteFile(/* ... */) async { /* ... */ }
// New required methods
@override
Future<UploadImageResponse> uploadImage(
AttachmentFile image, {
ProgressCallback? onSendProgress,
CancelToken? cancelToken,
}) async {
// Implementation for standalone image upload
}
@override
Future<UploadFileResponse> uploadFile(
AttachmentFile file, {
ProgressCallback? onSendProgress,
CancelToken? cancelToken,
}) async {
// Implementation for standalone file upload
}
@override
Future<EmptyResponse> removeImage(
String url, {
CancelToken? cancelToken,
}) async {
// Implementation for standalone image removal
}
@override
Future<EmptyResponse> removeFile(
String url, {
CancelToken? cancelToken,
}) async {
// Implementation for standalone file removal
}
}Important:
- Custom
AttachmentFileUploaderimplementations must now implement four additional methods- The new methods support standalone uploads/removals without requiring channel context
UploadImageResponseandUploadFileResponseare aliases forSendAttachmentResponse
onAttachmentTap
Key Changes
onAttachmentTapcallback signature has changed to support custom attachment handling with automatic fallback to default behavior.- Callback now receives
BuildContextas the first parameter. - Returns
FutureOr<bool>to indicate whether the attachment was handled. - Returning
trueskips default behavior,falseuses default handling (URLs, images, videos, giphys).
Migration Steps
Before:
StreamMessageItem(
message: message,
onAttachmentTap: (message, attachment) {
if (attachment.type == 'location') {
showLocationDialog(context, attachment);
}
// Other attachment types (images, videos, URLs) lost default behavior
},
)After:
StreamMessageItem(
message: message,
onAttachmentTap: (context, message, attachment) async {
if (attachment.type == 'location') {
await showLocationDialog(context, attachment);
return true; // Handled by custom logic
}
return false; // Use default behavior for images, videos, URLs, etc.
},
)Example: Handling multiple custom types
StreamMessageItem(
message: message,
onAttachmentTap: (context, message, attachment) async {
switch (attachment.type) {
case 'location':
await Navigator.push(
context,
MaterialPageRoute(builder: (_) => MapView(attachment)),
);
return true;
case 'product':
await showProductDialog(context, attachment);
return true;
default:
return false; // Images, videos, URLs use default viewer
}
},
)Important:
- The callback now requires
BuildContextas the first parameter- Must return
FutureOr<bool>-trueif handled,falsefor default behavior- Default behavior automatically handles URL previews, images, videos, and giphys
Deprecated API removals
Legacy theme classes removed
Some of the legacy theme classes have now been removed entirely. Use StreamColorScheme and StreamTextTheme (resolved through the StreamTheme ThemeExtension) instead.
| Removed | Replacement |
|---|---|
StreamColorTheme | StreamColorScheme (accessed via StreamTheme.of(context).colorScheme) |
StreamTextTheme | StreamTextTheme semantic theme on StreamTheme (e.g. bodyDefault) |
StreamChatThemeData.colorTheme | Read colors from StreamTheme.of(context).colorScheme |
StreamChatThemeData.textTheme | Read text styles from StreamTheme.of(context).textTheme |
StreamChatThemeData.brightness | Set brightness on ThemeData and use StreamTheme.light()/dark() |
StreamChatThemeData.primaryIconTheme | Configure via ThemeData.iconTheme or StreamIcons on StreamTheme |
StreamChatThemeData.defaultUserImage | Customize via StreamAvatarThemeData on StreamTheme |
StreamChatThemeData.placeholderUserImage | Customize via StreamAvatarThemeData on StreamTheme |
StreamChatThemeData.light() | StreamChatThemeData() (defaults derive from the ambient StreamTheme) |
StreamChatThemeData.dark() | StreamChatThemeData() (defaults derive from the ambient StreamTheme) |
StreamChatThemeData.fromTheme() | StreamChatThemeData() (the SDK no longer reads from Material ThemeData) |
StreamChatThemeData.fromColorAndTextTheme() | Build StreamTheme(colorScheme: ..., textTheme: ...) instead |
Before:
StreamChatThemeData(
brightness: Brightness.light,
colorTheme: StreamColorTheme.light(accentPrimary: Colors.indigo),
textTheme: StreamTextTheme.light(),
)After:
MaterialApp(
theme: ThemeData(
extensions: [
StreamTheme(
brightness: Brightness.light,
colorScheme: StreamColorScheme.light(
brand: StreamColorSwatch.fromColor(Colors.indigo),
),
),
],
),
// Chat-specific themes belong on StreamChatThemeData (passed to StreamChat).
home: StreamChat(
client: client,
themeData: StreamChatThemeData(/* per-component overrides */),
child: const MyHomePage(),
),
)StreamChat no longer rewrites the ambient Material ThemeData (the implicit overrides for primaryIconTheme and colorScheme.secondary are gone). Configure Material theming directly on MaterialApp.theme if you need it.
StreamChat parameter renames
The StreamChat constructor parameters have been shortened — the streamChat prefix was redundant:
| V9 parameter | V10 parameter |
|---|---|
StreamChat.streamChatThemeData | StreamChat.themeData |
StreamChat.streamChatConfigData | StreamChat.configData |
StreamChatState.streamChatConfigData | StreamChatState.configData |
StreamChatConfigurationData fields removed
| Removed | Replacement |
|---|---|
loadingIndicator | Provide a custom loading widget via StreamComponentFactory slots |
defaultUserImage | Customize avatars via StreamAvatarThemeData on StreamTheme |
placeholderUserImage | Customize avatars via StreamAvatarThemeData on StreamTheme |
If you were relying on defaultUserImage to render a gradient placeholder, the SDK already uses StreamGradientAvatar by default — no action is needed unless you had a custom widget there.
Unread Threads Banner
StreamUnreadThreadsBanner has been redesigned as a wrapper widget (similar to RefreshIndicator) instead of a standalone banner placed in a Column.
Key Changes
| Old API | New API |
|---|---|
Standalone widget in a Column | Wrapper (pass StreamThreadListView as child) |
| Always visible when unread threads > 0 | Controlled via enabled (default false) |
onTap (VoidCallback?) | onRefresh (Future<void> Function()?) |
minHeight parameter | Removed |
margin default: h: 8, v: 6 | margin default: EdgeInsets.zero |
padding default: h: 16 | padding default: EdgeInsets.all(spacing.sm) |
Migration Steps
Before:
Column(
children: [
ValueListenableBuilder(
valueListenable: controller.unseenThreadIds,
builder: (_, unreadThreads, __) => StreamUnreadThreadsBanner(
unreadThreads: unreadThreads,
onTap: () => controller
.refresh(resetValue: false)
.then((_) => controller.clearUnseenThreadIds()),
),
),
Expanded(
child: StreamThreadListView(controller: controller),
),
],
);After:
ValueListenableBuilder<Set<String>>(
valueListenable: controller.unseenThreadIds,
builder: (context, unseenThreadIds, child) => StreamUnreadThreadsBanner(
enabled: unseenThreadIds.isNotEmpty,
unreadThreads: unseenThreadIds,
onRefresh: () async {
await controller.refresh(resetValue: false);
controller.clearUnseenThreadIds();
},
child: child!,
),
child: StreamThreadListView(controller: controller),
);Message List
StreamMessageListView Configuration / Builders Split
StreamMessageListView had its parameter surface reorganized. Behavior flags moved to StreamMessageListViewConfiguration and custom builder callbacks moved to StreamMessageListViewBuilders.
Moved to StreamMessageListViewConfiguration: swipeToReply, markReadWhenAtTheBottom, showScrollToBottom, reverse, paginationLimit, scrollPhysics, and other behavior flags.
Moved to StreamMessageListViewBuilders: headerBuilder, footerBuilder, loadingBuilder, emptyBuilder, errorBuilder, messageListBuilder, parentMessageBuilder, dateDividerBuilder, floatingDateDividerBuilder, threadSeparatorBuilder, unreadMessagesSeparatorBuilder, scrollToBottomBuilder, paginationLoadingIndicatorBuilder, spacingWidgetBuilder, systemMessageBuilder, ephemeralMessageBuilder, moderatedMessageBuilder.
messageBuilder is unchanged — it stays at the root of StreamMessageListView.
Before:
StreamMessageListView(
swipeToReply: true,
paginationLimit: 20,
loadingBuilder: (context) => const MyLoader(),
emptyBuilder: (context) => const MyEmpty(),
dateDividerBuilder: (date) => MyDateDivider(date),
)After:
StreamMessageListView(
config: StreamMessageListViewConfiguration(
swipeToReply: true,
paginationLimit: 20,
),
builders: StreamMessageListViewBuilders(
loading: (context) => const MyLoader(),
empty: (context) => const MyEmpty(),
dateDivider: (date) => MyDateDivider(date),
),
)StreamMessageComposerInput Split
StreamMessageComposerInput has been split — both names now exist and serve different roles:
| Class | Role |
|---|---|
StreamMessageComposerInput | Outer container (the full input row including leading/trailing) |
StreamMessageComposerInputCenter | Center content only (text field, attachments preview, etc.) |
DefaultStreamMessageComposerInput | Default outer container implementation |
DefaultStreamMessageComposerInputCenter | Default center implementation |
MessageComposerInputProps | Props for the outer container |
MessageComposerInputCenterProps | Props for the center widget |
If you were previously using StreamMessageComposerInput to customise the text field area, switch to StreamMessageComposerInputCenter (and the messageComposerInputCenter builder key). The messageComposerInput builder key now controls the entire outer input row.
Important: This is a split, not a rename.
StreamMessageComposerInputstill exists; targeting it replaces the entire input row. To replace only the text field area, useStreamMessageComposerInputCenter.
Polls
Poll Dialogs → Sheets
All poll UI has moved from dialogs to Stream-styled modal bottom sheets:
| Old | New |
|---|---|
StreamPollOptionsDialog / showStreamPollOptionsDialog | StreamPollOptionsSheet / showStreamPollOptionsSheet |
StreamPollResultsDialog / showStreamPollResultsDialog | StreamPollResultsSheet / showStreamPollResultsSheet |
StreamPollOptionVotesDialog / showStreamPollOptionVotesDialog | StreamPollOptionVotesSheet / showStreamPollOptionVotesSheet |
StreamPollCommentsDialog / showStreamPollCommentsDialog | StreamPollCommentsSheet / showStreamPollCommentsSheet |
StreamPollCreatorDialog / StreamPollCreatorFullScreenDialog / showStreamPollCreatorDialog | StreamPollCreatorSheet / showStreamPollCreatorSheet |
StreamPollCreatorThemeData.primaryActionStyle and secondaryActionStyle are removed; use sheetHeaderStyle.trailingStyle / sheetHeaderStyle.leadingStyle instead.
Additional Localizations Changes
Translations: attachmentsUploadProgressText parameter rename
The named parameter remaining: on Translations.attachmentsUploadProgressText has been renamed to completed:.
Before:
translations.attachmentsUploadProgressText(remaining: 2, total: 5)After:
translations.attachmentsUploadProgressText(completed: 3, total: 5)Low-Level Client Changes
SortOption Constructor Rename
The unnamed positional constructor SortOption(field, direction) has been removed. Use the named constructors instead:
// Before
const [SortOption('last_message_at', direction: SortOption.DESC)]
const [SortOption('name', direction: SortOption.ASC)]
// After
const [SortOption.desc('last_message_at')]
const [SortOption.asc('name')]Important:
SortOption.desc()defaultsnullOrderingtoNullOrdering.nullsFirst;SortOption.asc()defaults toNullOrdering.nullsLast.
ClientState collections are now immutable
The channels, users, and activeLiveLocations collections on client.state no longer allow external mutation.
In addition, the following setters and methods are now @internal and must not be called from application code:
ClientState.channels=(setter)ClientState.addChannels(...)ClientState.removeChannel(...)ClientState.activeLiveLocations=(setter)
You will need to migrate any code that previously did:
// Old
client.state.channels[someCid] = channel;
client.state.addChannels({someCid: channel});
client.state.removeChannel(someCid);These calls will fail (read-only collection) or be inaccessible (@internal). Treat client.state.channels strictly as read-only state; updates flow through Channel.watch() / Channel.create() / client.queryChannels(...).
StreamChatCore: recoverStateOnReconnect and backgroundKeepAlive
StreamChatCore now sets client.recoverStateOnReconnect = false on mount. Channel refresh on reconnect is driven by the list controllers in the package, avoiding a duplicate queryChannels round-trip.
If you are watching a Channel outside any list controller (e.g. a deep link into a single channel screen), subscribe to client.on(EventType.connectionRecovered) and call channel.watch() yourself to refresh state on reconnect.
The default StreamChat.backgroundKeepAlive has also been reduced from 1 minute to 15 seconds.
Channel.isOneToOne and isGroup/isDistinct changes
Added: Channel.isOneToOne — returns true when the channel is isDistinct and has exactly two members.
Changed: Channel.isGroup is now memberCount > 2 || !isDistinct (was memberCount != 2). Two-member non-distinct channels now correctly report as groups. Migrate with !channel.isOneToOne where you previously used !isGroup || memberCount == 2.
Changed: Channel.isDistinct now uses the !members prefix (no trailing dash), matching the backend constant.
Deprecated: Message.syncWith in favor of Message.updateWith. The arguments are flipped: local.updateWith(remote) replaces remote.syncWith(local).
Removed APIs
Removed Widgets
The following widgets have been removed:
| Removed Widget | Notes |
|---|---|
AttachmentButton | Replaced by the attachment button inside StreamMessageComposer |
StreamQuotedMessageWidget | Use StreamQuotedMessage |
EditMessageSheet | Editing is handled inline by the composer |
StreamMessageSendButton | Part of the composer internals |
DesktopReactionsBuilder | Use ReactionDetailSheet |
StreamChannelGridView / StreamChannelGridTile | Removed — use StreamChannelListView |
StreamMessageSearchGridView | Removed |
AttachmentModalSheet | Removed |
ErrorAlertSheet | No longer publicly exported; still used internally by StreamMessageComposer |
StreamChannelInfoBottomSheet | Removed |
StreamMarkdownMessage | Use StreamMessageText (re-exported from stream_core_flutter) |
StreamAttachmentUploadStateBuilder.successBuilder | Removed (unreachable) |
StreamFileAttachmentThumbnail | Use StreamImageAttachmentThumbnail / StreamVideoAttachmentThumbnail or StreamFileTypeIcon.fromMimeType(...) |
Removed Themes
The following theme classes and StreamChatThemeData fields have been removed:
| Removed | Notes |
|---|---|
StreamMessageThemeData (and ownMessageTheme / otherMessageTheme accessors) | Bubble and text colors are now configured via StreamMessageItemThemeData |
StreamMessageInputThemeData (and messageInputTheme accessor) | Composer theming now uses the design-system primitives |
StreamChannelPreviewThemeData (and channelPreviewTheme accessor) | Replaced by StreamChannelListItemThemeData (see Channel List Item) |
StreamDraftListView and Related Classes Removed
StreamDraftListView, StreamDraftListTile, StreamDraftListTileTheme, StreamDraftListTileThemeData, and StreamChatThemeData.draftListTileTheme have been removed from the SDK.
Use StreamDraftListController with a generic PagedValueListView instead. See the sample app for a reference implementation.
Migration Checklist
Work through this list in order. Each item maps to a section above; jump to that section if you need details on the change.
Setup
- Bump
stream_chat_flutter(and any sibling packages) to^10.0.0.
Theming
- Replace the
StreamChatThemewrapper widget with aStreamThemeextension onMaterialApp.theme.extensions(anddarkTheme.extensionsfor dark mode). - Rename
StreamChatThemeData.channelPreviewTheme→channelListItemTheme;StreamChannelPreviewThemeData→StreamChannelListItemThemeData. - Rename
lastMessageAtStyle→timestampStyleon the channel-list item theme. - Remove all usages of
StreamMessageThemeData/ownMessageTheme/otherMessageTheme— these are gone. Bubble and text colors are now configured viaStreamMessageItemThemeData. - Remove all usages of
StreamMessageInputThemeData/messageInputTheme— composer theming now uses the design-system primitives. - Remove
StreamChatThemeData.audioWaveformTheme/audioWaveformSliderTheme; audio components now theme throughStreamTheme. - Remove
galleryHeaderTheme,galleryFooterTheme,imageFooterTheme, andStreamAvatarThemeDatafield references fromStreamChatThemeData.
Channel List
- Replace
StreamChannelListTilewithStreamChannelListItem. - Move slot customization to
StreamComponentFactory(viachannelListItem).
Message Item
- Update
messageBuildercallbacks to the newStreamMessageItemBuildersignature(context, message, defaultProps) => StreamMessageItem.fromProps(props: defaultProps.copyWith(...)). - Remove every
show*boolean parameter fromStreamMessageItemusages — they no longer exist. - Remove every removed builder callback (
userAvatarBuilder,textBuilder,bottomRowBuilderWithDefaultWidget, etc.); customize viaStreamComponentFactoryslots (messageLeading,messageHeader,messageFooter,messageItem) instead. - Drop
StreamMessageItem.onShowMessageandattachmentActionsModalBuilderfrom constructor calls.
Message Actions
- Replace
StreamMessageActionwithStreamContextMenuAction<MessageAction>(or your own value type). - Replace
customActions+onCustomActionTapwithactionsBuilder: (context, defaultActions) => [...defaultActions, yourAction]. - Remove
onActionTapfromStreamMessageActionsModalcall sites — dispatch now flows via the action'svalue/onTap. - Replace
showDialogwithshowStreamDialogwhen presenting Stream modals.
Reactions
- Update
sendReactioncalls to take aReactionobject instead of individual parameters. - Rename
StreamReactionPicker→StreamMessageReactionPicker. - Replace
reactionIconswithreactionIconResolverinStreamChatConfigurationData. - Replace
MessageReactionsModalwithReactionDetailSheet.show(...).
Avatars
- Replace any
constraints:parameter on avatar widgets with the newsize:enum (StreamAvatarSize.xs/sm/md/lg/xl/xxl). - Rename
showOnlineStatus→showOnlineIndicator. - Move
onTapcallbacks off avatar widgets onto a parentGestureDetectororInkWell. - Rename
StreamGroupAvatar→StreamUserAvatarGroup; changemembers:tousers:.
Message Composer
- Rename
StreamMessageInput→StreamMessageComposerin every usage. - Invert
hideSendAsDm→canAlsoSendToChannelFromThread(flip the boolean). - Rename
StreamMessageInputController→StreamMessageComposerController(andStreamRestorableMessageInputController→StreamRestorableMessageComposerController). - Rename the
messageInputController:parameter onStreamMessageComposertomessageComposerController:. - Replace
StreamMessageComposerController(message: existingMessage)withcontroller.editMessage(existingMessage)after construction. - Replace
controller.clear()(when used to exit edit mode) withcontroller.cancelEditMessage(). - Rename
editingOriginalMessage→messageBeingEdited. - If you customized only the text-field area via
StreamMessageComposerInput, switch toStreamMessageComposerInputCenter(builder keymessageComposerInputCenter). The originalmessageComposerInputkey now controls the whole outer input row.
Message List
- Move behavior flags (
swipeToReply,markReadWhenAtTheBottom,showScrollToBottom,reverse,paginationLimit, etc.) intoStreamMessageListView(config: StreamMessageListViewConfiguration(...)). - Move builder callbacks (
headerBuilder,footerBuilder,loadingBuilder,emptyBuilder,dateDividerBuilder, etc.) intoStreamMessageListView(builders: StreamMessageListViewBuilders(...)).
Attachments
- Update custom
StreamAttachmentWidgetBuildersubclasses whoseonAttachmentTapcallback signature is now(BuildContext, Message, Attachment) => FutureOr<bool>— returntrueif handled,falseto fall through to default behavior. - Replace
StreamUrlAttachmentwithStreamLinkPreviewAttachment;UrlAttachmentBuilderwithLinkPreviewAttachmentBuilder. - Remove
messageThemeandhostDisplayNamefrom link-preview usage. - Replace
imageThumbnailSize/imageThumbnailResizeType/imageThumbnailCropTypeonStreamImageAttachmentwith a singleImageResize? resizeparameter. - Remove
backgroundColorfromStreamFileAttachmentusage.
Attachment Picker
- Update picker-option construction to use
SystemAttachmentPickerOption(system pickers — camera, file dialogs) orTabbedAttachmentPickerOption(custom UI pickers — gallery, polls). - Handle
StreamAttachmentPickerResultreturn values from the picker — match onAttachmentsPicked,PollCreated,AttachmentPickerError, orCustomAttachmentPickerResult. - Replace any references to
showStreamAttachmentPickerModalBottomSheetandAttachmentPickerBottomSheet— the picker is now inline insideStreamMessageComposer. Customize viaattachmentPickerOptionsBuilder(ortabbedAttachmentPickerBuilder/systemAttachmentPickerBuilder). - Replace
customAttachmentPickerOptionswithattachmentPickerOptionsBuilder. - Replace
onCustomAttachmentPickerResultwithonAttachmentPickerResult(returnsFutureOr<bool>). - Catch typed errors: replace
ArgumentError('The size of the attachment is…')withAttachmentTooLargeError;ArgumentError('The maximum number of attachments is…')withAttachmentLimitReachedError.
Image CDN
- Replace the
getResizedImageUrlString extension withStreamImageCDN.resolveUrl(...). - Replace string
resize/croparguments withResizeMode/CropModeenums.
Unread Indicator & Unread Threads Banner
- Update
UnreadIndicatorButtoncallbacks:onTap→onJumpTap,onDismiss→onDismissTap. - Remove
StreamUnreadIndicatorstyling parameters (backgroundColor,textColor,textStyle) — theme viaStreamThemeinstead. - Convert
StreamUnreadThreadsBannerfrom a sibling-in-Columnwidget to a wrapper that takesStreamThreadListViewas itschild. - Replace the banner's
onTapwithonRefresh(returnsFuture<void>). - Add
enabled: true(or aValueListenableBuilderovercontroller.unseenThreadIds.isNotEmpty) to show the banner — it defaults to hidden. - Remove the
minHeightparameter fromStreamUnreadThreadsBannerif you were using it.
Icons & Headers
- Replace every
StreamSvgIcon(icon: StreamSvgIcons.*)usage withIcon(context.streamIcons.*)using the icon mapping table. - Remove
centerTitle,elevation,scrolledUnderElevation,bottomOpacity,bottom, andbackgroundColorfrom all header call sites — passstyle: StreamAppBarStyle(backgroundColor: ...)if you need a background override. - Optionally move app-wide
StreamComponentFactorywrapping intoStreamChat(componentBuilders: ...).
Localizations
- Find every
extends Translations/extends DefaultTranslationsin your codebase and implement the new required abstract members (see the Localizations section for the full list). - Rename
questionsLabeloverride →questionLabel({bool isPlural = false}); replacetranslations.questionsLabelcalls withtranslations.questionLabel(isPlural: true). - Rename
endVoteConfirmationTextoverride (and consumers) →endVoteConfirmationTitle. - Update
slowModeOnLabeloverride fromString gettoString slowModeOnLabel(int cooldownTimeOut). - Rename
attachmentsUploadProgressText(remaining: ...)calls toattachmentsUploadProgressText(completed: ...).
Media Viewer
- Replace
StreamFullScreenMedia/StreamFullScreenMediaBuilderwithStreamMediaGalleryPreview. - Replace
StreamAttachmentPackagewithStreamMediaGalleryAttachment(ormessage.toMediaGalleryAttachments(...)). - Rename
startIndex→initialIndex,mediaAttachmentPackages→attachmentson the preview widget. - Drop
userName,sentAt,onReplyMessage,onShowMessage,attachmentActionsModalBuilderfrom preview usage. - Replace
StreamGalleryHeaderwithStreamMediaGalleryPreviewHeaderand render sender/timestamp in thetitle/subtitleslots. - Replace
StreamGalleryFooterwithStreamMediaGalleryPreviewFooter. - Drop any references to
VideoPackage,DesktopVideoPackage,GalleryNavigationItem,FullScreenMediaWidget, andFullScreenMediaDesktop— they're removed.
Polls
- Replace each poll dialog (
StreamPollOptionsDialog,StreamPollResultsDialog,StreamPollOptionVotesDialog,StreamPollCommentsDialog,StreamPollCreatorDialog) and itsshowStreamPoll…Dialoghelper with the corresponding…Sheet/showStreamPoll…Sheetequivalent. - Migrate
StreamPollInteractorThemeDatato its new structured properties.
Voice Recording
- Migrate
StreamVoiceRecordingAttachmentThemeDatato its new design-token-based properties.
Message State & Deletion
- Update
MessageStatefactory constructors to take aMessageDeleteScopeparameter. - Update pattern-matching callbacks to handle
MessageDeleteScopeinstead ofbool hard. - Adopt the new
deleteMessageForMeAPIs where needed.
File Upload
- If you implement a custom
AttachmentFileUploader, add the four new abstract methods:uploadImage,uploadFile,removeImage,removeFile.
Low-Level Client (stream_chat)
- Replace
SortOption('field', direction: SortOption.DESC)withSortOption.desc('field')andSortOption('field', direction: SortOption.ASC)withSortOption.asc('field'). Note thenullOrderingdefaults differ (nullsFirstfor desc,nullsLastfor asc). - Remove any code that mutates
ClientState.channels/activeLiveLocationsdirectly — those setters (channels=,addChannels,removeChannel,activeLiveLocations=) are now@internal. Updates flow throughChannel.watch()/client.queryChannels(...). - If you watch a channel outside any list controller (e.g., a deep link), subscribe to
connectionRecoveredand callchannel.watch()yourself to refresh state on reconnect. - Replace
Channel.isGroupusages that assumed two-member non-distinct channels are 1:1 — usechannel.isOneToOnefor the DM check. - Replace
remote.syncWith(local)calls withlocal.updateWith(remote)(arguments are flipped).
Removed APIs
- Replace removed widgets with their successors (see the Removed Widgets table):
AttachmentButton,StreamQuotedMessageWidget→StreamQuotedMessage,EditMessageSheet,StreamMessageSendButton,DesktopReactionsBuilder→ReactionDetailSheet,StreamChannelGridView/StreamChannelGridTile→StreamChannelListView,StreamMessageSearchGridView,AttachmentModalSheet,StreamChannelInfoBottomSheet,StreamMarkdownMessage→StreamMessageText,StreamFileAttachmentThumbnail. - Remove
StreamDraftListView/StreamDraftListTileusage; build drafts UI on top ofStreamDraftListController+PagedValueListView.
After working through this list, run flutter analyze and the build; surviving issues are usually missed Translations members or a custom StreamAttachmentWidgetBuilder that still uses the old onAttachmentTap signature.
For additional support, visit our GitHub repository.
- Table of Contents
- Quick Reference
- Theming
- Channel List Item
- Message Item
- Message Actions
- Reactions
- Avatars
- Message Composer
- Attachment Picker
- Image CDN
- Unread Indicator
- Audio
- Icons & Headers
- Localizations
- Media Viewer
- Attachments & Polls
- Message State & Deletion
- File Upload
- onAttachmentTap
- Deprecated API removals
- Unread Threads Banner
- Message List
- Polls
- Additional Localizations Changes
- Low-Level Client Changes
- Removed APIs
- Migration Checklist