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

Feature AreaKey Changes
ThemingStreamChatTheme wrapper replaced by StreamTheme extension on MaterialApp.theme
Channel ListStreamChannelListTileStreamChannelListItem, theme class renames
Message ItemProps-based API via StreamMessageItemProps, removed show* booleans and builder callbacks
Message ActionsStreamContextMenuAction<T> replaces StreamMessageAction, actionsBuilder replaces customActions
ReactionsReaction object API, StreamMessageReactionPicker, reactionIconResolver, ReactionDetailSheet
AvatarsSize enums replace BoxConstraints, StreamGroupAvatarStreamUserAvatarGroup
Message ComposerStreamMessageInputStreamMessageComposer, hideSendAsDmcanAlsoSendToChannelFromThread (inverted), StreamMessageInputControllerStreamMessageComposerController
Attachment PickerSealed class hierarchy (AttachmentsPicked, PollCreated, AttachmentPickerError), builder pattern for options, typed errors
Image CDNgetResizedImageUrlStreamImageCDN.resolveUrl(), ResizeMode/CropMode enums
Unread IndicatorNamed constructors, updated UnreadIndicatorButton callbacks
AudioMoved to stream_core_flutter, theming via StreamTheme
Icons & HeadersStreamSvgIcon deprecated → Icon(context.streamIcons.*), header default changes
Message StateMessageDeleteScope replaces bool hard, delete-for-me support
File UploadFour new abstract methods on AttachmentFileUploader
onAttachmentTapNew FutureOr<bool> signature with BuildContext and fallback support
Unread Threads BannerWrapper 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:

StreamChatTheme(
  data: StreamChatThemeData(
    colorTheme: StreamColorTheme.light(
      accentPrimary: Colors.blue,
    ),
  ),
  child: MyApp(),
)

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

OldNew
StreamChatThemeData.channelPreviewThemeStreamChatThemeData.channelListItemTheme
StreamChannelPreviewThemeDataStreamChannelListItemThemeData
StreamChannelPreviewTheme.of(context)StreamChannelListItemTheme.of(context)
lastMessageAtStyletimestampStyle
lastMessageAtFormatter (theme prop)ChannelLastMessageDate(formatter: ...) widget param
tileColor / selectedTileColorbackgroundColor: 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 ParameterMigration Path
showReactionsStreamMessageItemThemeData visibility
showDeleteMessage / showEditMessageChannel permissions
showUsername / showTimestamp / showSendingIndicatorStreamMessageItemThemeData.metadataVisibility
showUserAvatarStreamMessageItemThemeData.avatarVisibility
showThreadReplyIndicatorStreamMessageItemThemeData.repliesVisibility
showErrorBadgeStreamMessageItemThemeData.errorBadgeVisibility
showAnnotationsStreamMessageItemThemeData.annotationVisibility
showReactionTailTail is shown automatically when the picker is visible

Removed Builder Callbacks

Old ParameterMigration Path
userAvatarBuilderComponent factory (replace DefaultStreamMessageItem)
textBuilderComponent factory (replace StreamMessageContent)
quotedMessageBuilderComponent factory (replace StreamMessageContent)
deletedMessageBuilderComponent factory (replace StreamMessageContent)
reactionPickerBuilderStreamChatConfigurationData.reactionIconResolver
customActionsactionsBuilder on StreamMessageItemProps
onCustomActionTaponTap per StreamContextMenuAction

Changed Callback Signatures

OldNew
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

OldNew
MessageBuilder = Widget Function(BuildContext, MessageDetails, List<Message>, StreamMessageItem)StreamMessageItemBuilder = Widget Function(BuildContext, Message, StreamMessageItemProps)
ParentMessageBuilderStreamMessageItemBuilder
StreamDeletedMessageStreamMessageDeleted

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:

StreamMessageActionStreamContextMenuAction
action: Tvalue: T?
title: Widget?label: Widget (now required)
leading: Widget?leading: Widget?
isDestructive: boolisDestructive: 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

  • sendReaction method now accepts a full Reaction object 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 sendReaction method now requires a Reaction object
  • Optional parameters like enforceUnique and skipPush remain as method parameters
  • You can now specify custom emoji codes for reactions using the emojiCode field

StreamMessageReactionPicker

Key Changes

  • StreamReactionPicker has been renamed to StreamMessageReactionPicker
  • onReactionPicked is optional

Migration Steps

Before:

StreamReactionPicker(message: message)

After:

StreamMessageReactionPicker(
  message: message,
  onReactionPicked: onReactionPicked,
)

Note: StreamReactionPicker still exists in stream_core_flutter, but as a primitive that takes items: List<StreamReactionPickerItem>, onReactionPicked, and onAddReactionTap. The chat-domain wrapper for v9 callers is the new StreamMessageReactionPicker (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 an onReactionPicked callback.


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:

  • showOnlineStatus is renamed to showOnlineIndicator
  • onTap has been removed from avatar widgets — wrap with GestureDetector or InkWell instead

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)

OldNew
hideSendAsDm: truecanAlsoSendToChannelFromThread: false
hideSendAsDm: falsecanAlsoSendToChannelFromThread: 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:

OldNew
StreamMessageInputControllerStreamMessageComposerController
StreamRestorableMessageInputControllerStreamRestorableMessageComposerController
StreamMessageComposerController.editingOriginalMessagemessageBeingEdited
StreamMessageComposer.messageInputController parammessageComposerController

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; use cancelEditMessage() 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

  • AttachmentPickerType enum replaced with sealed class hierarchy
  • Now supports extensible custom types like contact and location pickers
  • Use built-in types like AttachmentPickerType.images or define your own via CustomAttachmentPickerType

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

  • StreamAttachmentPickerOption replaced with two sealed classes:
    • SystemAttachmentPickerOption for system pickers (camera, files)
    • TabbedAttachmentPickerOption for 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 SystemAttachmentPickerOption for system pickers (camera, file dialogs)
  • Use TabbedAttachmentPickerOption for 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

  • customAttachmentPickerOptions has been removed. Use attachmentPickerOptionsBuilder instead.
  • 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

  • onCustomAttachmentPickerResult has been removed. Use onAttachmentPickerResult which returns FutureOr<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:

  • onAttachmentPickerResult replaces onCustomAttachmentPickerResult and must return a boolean
  • Return true from onAttachmentPickerResult to skip default handling
  • Return false to allow the default handler to process the result

StreamAttachmentPickerController

Key Changes

  • Replaced ArgumentError('The size of the attachment is...') with AttachmentTooLargeError.
  • Replaced ArgumentError('The maximum number of attachments is...') with AttachmentLimitReachedError.

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 ArgumentError catches with the specific typed errors
  • AttachmentTooLargeError provides fileSize and maxSize properties
  • AttachmentLimitReachedError provides maxCount property

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 CachedNetworkImage

For 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 threads

UnreadIndicatorButton

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:

FieldTypeDefaultDescription
attachmentBuildersList<StreamAttachmentWidgetBuilder>?nullCustom attachment widget builders, prepended to built-in builders
reactionTypeStreamReactionsType?nullControls visual style of the reactions display
reactionPositionStreamReactionsPosition?nullControls where reactions appear relative to the message bubble

Note: The imageCDN field 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 memberNew memberNotes
String get questionsLabelString questionLabel({bool isPlural = false})Pass isPlural: true for the plural "Questions" form.
String get endVoteConfirmationTextString get endVoteConfirmationTitleRenamed to reflect it's the dialog title, not body text.
String get slowModeOnLabelString 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:

GetterOld defaultNew 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: mediaAttachmentPackagesattachments, startIndexinitialIndex.

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

RemovedReplacement
StreamGalleryFooterThemeDataStreamBottomAppBarThemeData
StreamChatThemeData.galleryFooterTheme / imageFooterThemeUse StreamBottomAppBarThemeData
StreamChatThemeData.galleryHeaderThemeUse StreamAppBarThemeData
StreamAvatarThemeDataIs 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:

  • shape parameter removed
  • constraints changed from required to optional

Per-widget changes:

WidgetAdditional Changes
StreamImageAttachmentimageThumbnailSize/imageThumbnailResizeType/imageThumbnailCropType → single ImageResize? resize
StreamFileAttachmentbackgroundColor removed
StreamUrlAttachmentRenamed 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 PropertyReplacement
pollTitleStyletitleTextStyle
pollSubtitleStylesubtitleTextStyle
pollOptionTextStyle, pollOptionVoteCountTextStyleoptionStyle (StreamPollOptionStyle)
pollOptionCheckbox* propertiesoptionStyle.checkboxStyle (StreamCheckboxStyle)
pollOptionVotesProgressBar* propertiesoptionStyle.progressBarStyle (StreamProgressBarStyle)
pollActionButtonStyleprimaryActionStyle / secondaryActionStyle (StreamButtonThemeStyle)
pollActionDialog* propertiesRemoved

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 PropertyReplacement
backgroundColorRemoved (handled by attachment container)
playIcon, pauseIcon, loadingIndicatorRemoved (handled by controlButtonStyle)
audioControlButtonStylecontrolButtonStyle (StreamButtonThemeStyle)
speedControlButtonStylespeedToggleStyle (StreamPlaybackSpeedToggleStyle)
audioWaveformSliderThemewaveformStyle (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

  • MessageState factory constructors now accept MessageDeleteScope instead of bool hard parameter
  • Pattern matching callbacks in state classes now receive MessageDeleteScope scope instead of bool 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 MessageState factory constructors now require MessageDeleteScope parameter
  • Pattern matching callbacks receive MessageDeleteScope instead of bool hard
  • Use scope.hard to access the hard delete boolean value
  • New delete-for-me methods are available on both Channel and StreamChatClient

File Upload

The file uploader interface has been expanded with standalone upload and removal methods.


AttachmentFileUploader

Key Changes

  • AttachmentFileUploader interface now includes four new abstract methods: uploadImage, uploadFile, removeImage, and removeFile.
  • 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 AttachmentFileUploader implementations must now implement four additional methods
  • The new methods support standalone uploads/removals without requiring channel context
  • UploadImageResponse and UploadFileResponse are aliases for SendAttachmentResponse

onAttachmentTap

Key Changes

  • onAttachmentTap callback signature has changed to support custom attachment handling with automatic fallback to default behavior.
  • Callback now receives BuildContext as the first parameter.
  • Returns FutureOr<bool> to indicate whether the attachment was handled.
  • Returning true skips default behavior, false uses 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 BuildContext as the first parameter
  • Must return FutureOr<bool> - true if handled, false for 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.

RemovedReplacement
StreamColorThemeStreamColorScheme (accessed via StreamTheme.of(context).colorScheme)
StreamTextThemeStreamTextTheme semantic theme on StreamTheme (e.g. bodyDefault)
StreamChatThemeData.colorThemeRead colors from StreamTheme.of(context).colorScheme
StreamChatThemeData.textThemeRead text styles from StreamTheme.of(context).textTheme
StreamChatThemeData.brightnessSet brightness on ThemeData and use StreamTheme.light()/dark()
StreamChatThemeData.primaryIconThemeConfigure via ThemeData.iconTheme or StreamIcons on StreamTheme
StreamChatThemeData.defaultUserImageCustomize via StreamAvatarThemeData on StreamTheme
StreamChatThemeData.placeholderUserImageCustomize 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 parameterV10 parameter
StreamChat.streamChatThemeDataStreamChat.themeData
StreamChat.streamChatConfigDataStreamChat.configData
StreamChatState.streamChatConfigDataStreamChatState.configData

StreamChatConfigurationData fields removed

RemovedReplacement
loadingIndicatorProvide a custom loading widget via StreamComponentFactory slots
defaultUserImageCustomize avatars via StreamAvatarThemeData on StreamTheme
placeholderUserImageCustomize 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 APINew API
Standalone widget in a ColumnWrapper (pass StreamThreadListView as child)
Always visible when unread threads > 0Controlled via enabled (default false)
onTap (VoidCallback?)onRefresh (Future<void> Function()?)
minHeight parameterRemoved
margin default: h: 8, v: 6margin default: EdgeInsets.zero
padding default: h: 16padding 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:

ClassRole
StreamMessageComposerInputOuter container (the full input row including leading/trailing)
StreamMessageComposerInputCenterCenter content only (text field, attachments preview, etc.)
DefaultStreamMessageComposerInputDefault outer container implementation
DefaultStreamMessageComposerInputCenterDefault center implementation
MessageComposerInputPropsProps for the outer container
MessageComposerInputCenterPropsProps 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. StreamMessageComposerInput still exists; targeting it replaces the entire input row. To replace only the text field area, use StreamMessageComposerInputCenter.


Polls

Poll Dialogs → Sheets

All poll UI has moved from dialogs to Stream-styled modal bottom sheets:

OldNew
StreamPollOptionsDialog / showStreamPollOptionsDialogStreamPollOptionsSheet / showStreamPollOptionsSheet
StreamPollResultsDialog / showStreamPollResultsDialogStreamPollResultsSheet / showStreamPollResultsSheet
StreamPollOptionVotesDialog / showStreamPollOptionVotesDialogStreamPollOptionVotesSheet / showStreamPollOptionVotesSheet
StreamPollCommentsDialog / showStreamPollCommentsDialogStreamPollCommentsSheet / showStreamPollCommentsSheet
StreamPollCreatorDialog / StreamPollCreatorFullScreenDialog / showStreamPollCreatorDialogStreamPollCreatorSheet / 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() defaults nullOrdering to NullOrdering.nullsFirst; SortOption.asc() defaults to NullOrdering.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 WidgetNotes
AttachmentButtonReplaced by the attachment button inside StreamMessageComposer
StreamQuotedMessageWidgetUse StreamQuotedMessage
EditMessageSheetEditing is handled inline by the composer
StreamMessageSendButtonPart of the composer internals
DesktopReactionsBuilderUse ReactionDetailSheet
StreamChannelGridView / StreamChannelGridTileRemoved — use StreamChannelListView
StreamMessageSearchGridViewRemoved
AttachmentModalSheetRemoved
ErrorAlertSheetNo longer publicly exported; still used internally by StreamMessageComposer
StreamChannelInfoBottomSheetRemoved
StreamMarkdownMessageUse StreamMessageText (re-exported from stream_core_flutter)
StreamAttachmentUploadStateBuilder.successBuilderRemoved (unreachable)
StreamFileAttachmentThumbnailUse StreamImageAttachmentThumbnail / StreamVideoAttachmentThumbnail or StreamFileTypeIcon.fromMimeType(...)

Removed Themes

The following theme classes and StreamChatThemeData fields have been removed:

RemovedNotes
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, 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 StreamChatTheme wrapper widget with a StreamTheme extension on MaterialApp.theme.extensions (and darkTheme.extensions for dark mode).
  • Rename StreamChatThemeData.channelPreviewThemechannelListItemTheme; StreamChannelPreviewThemeDataStreamChannelListItemThemeData.
  • Rename lastMessageAtStyletimestampStyle on the channel-list item theme.
  • Remove all usages of StreamMessageThemeData / ownMessageTheme / otherMessageTheme — these are gone. Bubble and text colors are now configured via StreamMessageItemThemeData.
  • Remove all usages of StreamMessageInputThemeData / messageInputTheme — composer theming now uses the design-system primitives.
  • Remove StreamChatThemeData.audioWaveformTheme / audioWaveformSliderTheme; audio components now theme through StreamTheme.
  • Remove galleryHeaderTheme, galleryFooterTheme, imageFooterTheme, and StreamAvatarThemeData field references from StreamChatThemeData.

Channel List

  • Replace StreamChannelListTile with StreamChannelListItem.
  • Move slot customization to StreamComponentFactory (via channelListItem).

Message Item

  • Update messageBuilder callbacks to the new StreamMessageItemBuilder signature (context, message, defaultProps) => StreamMessageItem.fromProps(props: defaultProps.copyWith(...)).
  • Remove every show* boolean parameter from StreamMessageItem usages — they no longer exist.
  • Remove every removed builder callback (userAvatarBuilder, textBuilder, bottomRowBuilderWithDefaultWidget, etc.); customize via StreamComponentFactory slots (messageLeading, messageHeader, messageFooter, messageItem) instead.
  • Drop StreamMessageItem.onShowMessage and attachmentActionsModalBuilder from constructor calls.

Message Actions

  • Replace StreamMessageAction with StreamContextMenuAction<MessageAction> (or your own value type).
  • Replace customActions + onCustomActionTap with actionsBuilder: (context, defaultActions) => [...defaultActions, yourAction].
  • Remove onActionTap from StreamMessageActionsModal call sites — dispatch now flows via the action's value / onTap.
  • Replace showDialog with showStreamDialog when presenting Stream modals.

Reactions

  • Update sendReaction calls to take a Reaction object instead of individual parameters.
  • Rename StreamReactionPickerStreamMessageReactionPicker.
  • Replace reactionIcons with reactionIconResolver in StreamChatConfigurationData.
  • Replace MessageReactionsModal with ReactionDetailSheet.show(...).

Avatars

  • Replace any constraints: parameter on avatar widgets with the new size: enum (StreamAvatarSize.xs/sm/md/lg/xl/xxl).
  • Rename showOnlineStatusshowOnlineIndicator.
  • Move onTap callbacks off avatar widgets onto a parent GestureDetector or InkWell.
  • Rename StreamGroupAvatarStreamUserAvatarGroup; change members: to users:.

Message Composer

  • Rename StreamMessageInputStreamMessageComposer in every usage.
  • Invert hideSendAsDmcanAlsoSendToChannelFromThread (flip the boolean).
  • Rename StreamMessageInputControllerStreamMessageComposerController (and StreamRestorableMessageInputControllerStreamRestorableMessageComposerController).
  • Rename the messageInputController: parameter on StreamMessageComposer to messageComposerController:.
  • Replace StreamMessageComposerController(message: existingMessage) with controller.editMessage(existingMessage) after construction.
  • Replace controller.clear() (when used to exit edit mode) with controller.cancelEditMessage().
  • Rename editingOriginalMessagemessageBeingEdited.
  • If you customized only the text-field area via StreamMessageComposerInput, switch to StreamMessageComposerInputCenter (builder key messageComposerInputCenter). The original messageComposerInput key now controls the whole outer input row.

Message List

  • Move behavior flags (swipeToReply, markReadWhenAtTheBottom, showScrollToBottom, reverse, paginationLimit, etc.) into StreamMessageListView(config: StreamMessageListViewConfiguration(...)).
  • Move builder callbacks (headerBuilder, footerBuilder, loadingBuilder, emptyBuilder, dateDividerBuilder, etc.) into StreamMessageListView(builders: StreamMessageListViewBuilders(...)).

Attachments

  • Update custom StreamAttachmentWidgetBuilder subclasses whose onAttachmentTap callback signature is now (BuildContext, Message, Attachment) => FutureOr<bool> — return true if handled, false to fall through to default behavior.
  • Replace StreamUrlAttachment with StreamLinkPreviewAttachment; UrlAttachmentBuilder with LinkPreviewAttachmentBuilder.
  • Remove messageTheme and hostDisplayName from link-preview usage.
  • Replace imageThumbnailSize / imageThumbnailResizeType / imageThumbnailCropType on StreamImageAttachment with a single ImageResize? resize parameter.
  • Remove backgroundColor from StreamFileAttachment usage.

Attachment Picker

  • Update picker-option construction to use SystemAttachmentPickerOption (system pickers — camera, file dialogs) or TabbedAttachmentPickerOption (custom UI pickers — gallery, polls).
  • Handle StreamAttachmentPickerResult return values from the picker — match on AttachmentsPicked, PollCreated, AttachmentPickerError, or CustomAttachmentPickerResult.
  • Replace any references to showStreamAttachmentPickerModalBottomSheet and AttachmentPickerBottomSheet — the picker is now inline inside StreamMessageComposer. Customize via attachmentPickerOptionsBuilder (or tabbedAttachmentPickerBuilder / systemAttachmentPickerBuilder).
  • Replace customAttachmentPickerOptions with attachmentPickerOptionsBuilder.
  • Replace onCustomAttachmentPickerResult with onAttachmentPickerResult (returns FutureOr<bool>).
  • Catch typed errors: replace ArgumentError('The size of the attachment is…') with AttachmentTooLargeError; ArgumentError('The maximum number of attachments is…') with AttachmentLimitReachedError.

Image CDN

  • Replace the getResizedImageUrl String extension with StreamImageCDN.resolveUrl(...).
  • Replace string resize / crop arguments with ResizeMode / CropMode enums.

Unread Indicator & Unread Threads Banner

  • Update UnreadIndicatorButton callbacks: onTaponJumpTap, onDismissonDismissTap.
  • Remove StreamUnreadIndicator styling parameters (backgroundColor, textColor, textStyle) — theme via StreamTheme instead.
  • Convert StreamUnreadThreadsBanner from a sibling-in-Column widget to a wrapper that takes StreamThreadListView as its child.
  • Replace the banner's onTap with onRefresh (returns Future<void>).
  • Add enabled: true (or a ValueListenableBuilder over controller.unseenThreadIds.isNotEmpty) to show the banner — it defaults to hidden.
  • Remove the minHeight parameter from StreamUnreadThreadsBanner if you were using it.

Icons & Headers

  • Replace every StreamSvgIcon(icon: StreamSvgIcons.*) usage with Icon(context.streamIcons.*) using the icon mapping table.
  • Remove centerTitle, elevation, scrolledUnderElevation, bottomOpacity, bottom, and backgroundColor from all header call sites — pass style: StreamAppBarStyle(backgroundColor: ...) if you need a background override.
  • Optionally move app-wide StreamComponentFactory wrapping into StreamChat(componentBuilders: ...).

Localizations

  • Find every extends Translations / extends DefaultTranslations in your codebase and implement the new required abstract members (see the Localizations section for the full list).
  • Rename questionsLabel override → questionLabel({bool isPlural = false}); replace translations.questionsLabel calls with translations.questionLabel(isPlural: true).
  • Rename endVoteConfirmationText override (and consumers) → endVoteConfirmationTitle.
  • Update slowModeOnLabel override from String get to String slowModeOnLabel(int cooldownTimeOut).
  • Rename attachmentsUploadProgressText(remaining: ...) calls to attachmentsUploadProgressText(completed: ...).

Media Viewer

  • Replace StreamFullScreenMedia / StreamFullScreenMediaBuilder with StreamMediaGalleryPreview.
  • Replace StreamAttachmentPackage with StreamMediaGalleryAttachment (or message.toMediaGalleryAttachments(...)).
  • Rename startIndexinitialIndex, mediaAttachmentPackagesattachments on the preview widget.
  • Drop userName, sentAt, onReplyMessage, onShowMessage, attachmentActionsModalBuilder from preview usage.
  • Replace StreamGalleryHeader with StreamMediaGalleryPreviewHeader and render sender/timestamp in the title / subtitle slots.
  • Replace StreamGalleryFooter with StreamMediaGalleryPreviewFooter.
  • Drop any references to VideoPackage, DesktopVideoPackage, GalleryNavigationItem, FullScreenMediaWidget, and FullScreenMediaDesktop — they're removed.

Polls

  • Replace each poll dialog (StreamPollOptionsDialog, StreamPollResultsDialog, StreamPollOptionVotesDialog, StreamPollCommentsDialog, StreamPollCreatorDialog) and its showStreamPoll…Dialog helper with the corresponding …Sheet / showStreamPoll…Sheet equivalent.
  • Migrate StreamPollInteractorThemeData to its new structured properties.

Voice Recording

  • Migrate StreamVoiceRecordingAttachmentThemeData to its new design-token-based properties.

Message State & Deletion

  • Update MessageState factory constructors to take a MessageDeleteScope parameter.
  • Update pattern-matching callbacks to handle MessageDeleteScope instead of bool hard.
  • Adopt the new deleteMessageForMe APIs 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) with SortOption.desc('field') and SortOption('field', direction: SortOption.ASC) with SortOption.asc('field'). Note the nullOrdering defaults differ (nullsFirst for desc, nullsLast for asc).
  • Remove any code that mutates ClientState.channels / activeLiveLocations directly — those setters (channels=, addChannels, removeChannel, activeLiveLocations=) are now @internal. Updates flow through Channel.watch() / client.queryChannels(...).
  • If you watch a channel outside any list controller (e.g., a deep link), subscribe to connectionRecovered and call channel.watch() yourself to refresh state on reconnect.
  • Replace Channel.isGroup usages that assumed two-member non-distinct channels are 1:1 — use channel.isOneToOne for the DM check.
  • Replace remote.syncWith(local) calls with local.updateWith(remote) (arguments are flipped).

Removed APIs

  • Replace removed widgets with their successors (see the Removed Widgets table): AttachmentButton, StreamQuotedMessageWidgetStreamQuotedMessage, EditMessageSheet, StreamMessageSendButton, DesktopReactionsBuilderReactionDetailSheet, StreamChannelGridView / StreamChannelGridTileStreamChannelListView, StreamMessageSearchGridView, AttachmentModalSheet, StreamChannelInfoBottomSheet, StreamMarkdownMessageStreamMessageText, StreamFileAttachmentThumbnail.
  • Remove StreamDraftListView / StreamDraftListTile usage; build drafts UI on top of StreamDraftListController + 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.