Customizing Widgets

The Flutter SDK provides several patterns for customizing and replacing built-in components. Depending on the scope of your change, you can use builder callbacks on individual widgets, StreamComponentFactory for app-wide overrides, or StreamTheme / StreamChatThemeData for visual styling.

Props and copyWith

Most SDK widgets accept a props argument that is a typed object containing all the data the default implementation uses. You can use this to customize the widget by updating the properties, or creating a fully custom widget based on this data.

  • Per-widget builder callbacks — when you provide a messageBuilder or itemBuilder, the callback receives defaultProps already populated with all the data for that item. See Per-widget builder callbacks below for the full list.
  • StreamComponentFactory builders — each slot receives a typed props argument with all the data the default implementation uses.

To customize a widget without reimplementing it from scratch, call copyWith() to override only the fields you need. Then pass the result to the component's .fromProps() factory constructor to rebuild the default widget with your overrides applied:

StreamMessageListView(
  messageBuilder: (context, message, defaultProps) {
    // defaultProps is the fully-populated StreamMessageItemProps for this message.
    // copyWith overrides only the fields you care about; everything else stays as-is.
    return StreamMessageItem.fromProps(
      // fromProps rebuilds the default widget using the modified props.
      props: defaultProps.copyWith(
        swipeToReply: true,
        maxWidth: 300,
      ),
    );
  },
)

StreamComponentFactory

StreamComponentFactory is the primary extension point for replacing entire component implementations. It uses the StreamComponentBuilders API to register custom builders for each slot.

StreamComponentFactory is an InheritedWidget, so its overrides apply to its subtree only — exactly like Flutter's Theme. Place it once at the root of your app to apply custom builders globally, or nest it deeper in the tree to scope overrides to a single screen or section. When multiple StreamComponentFactory ancestors are present, the nearest one wins for any given slot.

StreamComponentBuilders accepts named slots for both chat-specific components (via streamChatComponentBuilders) and core design-system components such as button, avatar, textInput, and loadingSpinner:

StreamComponentFactory(
  builders: StreamComponentBuilders(
    // Core component — replace every StreamButton in the subtree.
    button: (context, props) {
      return FilledButton(
        onPressed: props.onPressed,
        style: FilledButton.styleFrom(
          backgroundColor: props.style == StreamButtonStyle.destructive
              ? Colors.red
              : Theme.of(context).colorScheme.primary,
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            if (props.iconLeft != null) ...[
              Icon(props.iconLeft, size: 18),
              const SizedBox(width: 6),
            ],
            if (props.child != null) props.child!,
          ],
        ),
      );
    },
    // Chat-specific components — passed via the extensions list.
    extensions: streamChatComponentBuilders(
      channelListItem: (context, props) => MyCustomChannelListItem(props: props),
      messageItem: (context, props) => MyCustomMessageWidget(props: props),
    ),
  ),
  child: MyApp(),
)

Each builder receives a typed props object containing all the data the default implementation uses. StreamButtonProps, for example, exposes child, onPressed, style, type, size, iconLeft, iconRight, isSelected, isFloating, themeStyle.

Spread multiple extension sets together when composing overrides from different sources:

extensions: [
  ...streamChatComponentBuilders(
    channelListItem: (context, props) => MyChannelListItem(props: props),
  ),
  ...myOtherBuilders(),
]

The nearest StreamComponentFactory ancestor wins, so you can scope an override to a single screen without affecting the rest of the app.

What is a builder key?

A builder key is the slot name you pass to StreamComponentBuilders or streamChatComponentBuilders(...). Each key maps to a specific component type. Your builder receives a typed Props object containing the data the default implementation uses. You return the widget you want rendered in its place.

Full signature pattern:

// Core component (direct named parameter on StreamComponentBuilders)
StreamComponentBuilders(
  button: (BuildContext context, StreamButtonProps props) {
    return FilledButton(onPressed: props.onPressed, child: props.child);
  },
)

// Chat-specific component (via the extensions list)
StreamComponentBuilders(
  extensions: streamChatComponentBuilders(
    messageItem: (BuildContext context, StreamMessageItemProps props) {
      // Use props.copyWith() to adjust defaults, or return a completely custom widget.
      return DefaultStreamMessageItem(
        props: props.copyWith(
          actionsBuilder: (context, defaultActions) => [
            ...defaultActions,
            StreamContextMenuAction(
              leading: const Icon(Icons.star),
              label: const Text('Favourite'),
              onTap: () => _favourite(props.message),
            ),
          ],
        ),
      );
    },
  ),
)

See the Props and copyWith section above for details on working with props classes.

Core components (stream_core_flutter)

These are design-system primitives shared across Stream SDKs. Pass them directly as named parameters on StreamComponentBuilders.

Builder keyProps classDescription
appBarStreamAppBarPropsApp bar
avatarStreamAvatarPropsSingle avatar image
avatarGroupStreamAvatarGroupPropsOverlapping group of avatars
avatarStackStreamAvatarStackPropsStacked avatar layout
badgeCountStreamBadgeCountPropsNumeric unread/count badge
badgeNotificationStreamBadgeNotificationPropsNotification dot badge
bottomAppBarStreamBottomAppBarPropsBottom app bar
buttonStreamButtonPropsButton
checkboxStreamCheckboxPropsCheckbox
commandChipStreamCommandChipPropsSlash-command suggestion chip
contextMenuActionStreamContextMenuActionPropsItem inside a context menu
emojiStreamEmojiPropsEmoji renderer
emojiButtonStreamEmojiButtonPropsButton that opens the emoji picker
emojiChipStreamEmojiChipPropsSingle emoji chip (reaction)
emojiChipBarStreamEmojiChipBarPropsRow of emoji chips
errorBadgeStreamErrorBadgePropsError badge overlay
fileTypeIconStreamFileTypeIconPropsIcon representing a file type
imageSourceBadgeStreamImageSourceBadgePropsBadge showing the image source
jumpToUnreadButtonStreamJumpToUnreadButtonPropsFloating button to jump to first unread
listTileStreamListTilePropsGeneric list tile
loadingSpinnerStreamLoadingSpinnerPropsLoading spinner
mediaViewerStreamMediaViewerPropsFull-screen media viewer
messageAnnotationStreamMessageAnnotationPropsSystem/annotation message row
messageBubbleStreamMessageBubblePropsMessage bubble container
messageComposerAttachmentStreamMessageComposerAttachmentPropsGeneric attachment chip in the composer
messageComposerEditMessageAttachmentStreamMessageComposerEditMessageAttachmentPropsEdit-message preview in the composer
messageComposerFileAttachmentStreamMessageComposerFileAttachmentPropsFile attachment preview in the composer
messageComposerLinkPreviewAttachmentStreamMessageComposerLinkPreviewAttachmentPropsLink preview in the composer
messageComposerMediaAttachmentStreamMessageComposerMediaAttachmentPropsImage/video preview in the composer
messageComposerReplyAttachmentStreamMessageComposerReplyAttachmentPropsReply preview in the composer
messageComposerUnsupportedAttachmentStreamMessageComposerUnsupportedAttachmentPropsUnsupported attachment placeholder in the composer
messageContentStreamMessageContentPropsMessage content layout (bubble + metadata)
messageMetadataStreamMessageMetadataPropsTimestamp and delivery status row
messageRepliesStreamMessageRepliesPropsThread reply count link
messageTextStreamMessageTextPropsMessage body (markdown renderer)
networkImageStreamNetworkImagePropsNetwork image with loading/error states
onlineIndicatorStreamOnlineIndicatorPropsOnline presence dot
playbackSpeedToggleStreamPlaybackSpeedTogglePropsAudio playback speed button
progressBarStreamProgressBarPropsAudio/upload progress bar
reactionPickerStreamReactionPickerPropsReaction picker overlay
reactionsStreamReactionsPropsReaction row beneath a message
retryBadgeStreamRetryBadgePropsFailed-send retry badge
sheetHeaderStreamSheetHeaderPropsBottom sheet header/handle
skeletonLoadingStreamSkeletonLoadingPropsShimmer skeleton loading placeholder
stepperStreamStepperPropsNumeric increment/decrement stepper
textInputStreamTextInputPropsText input field
toggleSwitchStreamSwitchPropsToggle switch

Chat-specific components (stream_chat_flutter)

These are chat domain components registered via streamChatComponentBuilders(...) passed to the extensions list.

Channel and thread list

Builder keyProps classDescription
channelListItemStreamChannelListItemPropsRow in the channel list
threadListItemStreamThreadListTilePropsRow in the thread list

Example — custom channel list item:

StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      channelListItem: (context, props) {
        return StreamChannelListTile(
          avatar: StreamChannelAvatar(channel: props.channel),
          title: Text(props.channel.name ?? ''),
          subtitle: Text(props.channel.lastMessageAt?.toString() ?? ''),
          onTap: props.onTap,
          onLongPress: props.onLongPress,
          selected: props.selected,
        );
      },
    ),
  ),
  child: MyApp(),
)

Message list

Builder keyProps classDescription
messageItemStreamMessageItemPropsIndividual message item
messageLeadingStreamMessageLeadingPropsLeading slot of the message row (avatar area for other users' messages)
messageHeaderStreamMessageHeaderPropsHeader row above the bubble (sender name, thread-parent label, etc.)
messageFooterStreamMessageFooterPropsFooter row below the bubble (timestamp, delivery state, edited indicator)

Example — add a custom context-menu action to every message:

StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      messageItem: (context, props) {
        return DefaultStreamMessageItem(
          props: props.copyWith(
            actionsBuilder: (context, defaultActions) => [
              ...defaultActions,
              StreamContextMenuAction(
                leading: const Icon(Icons.star),
                label: const Text('Favourite'),
                onTap: () => _favourite(props.message),
              ),
            ],
          ),
        );
      },
    ),
  ),
  child: MyApp(),
)

Message composer

Builder keyProps classDescription
messageComposerMessageComposerPropsFull composer widget
messageComposerLeadingMessageComposerLeadingPropsLeft side of the composer bar
messageComposerTrailingMessageComposerTrailingPropsRight side of the composer bar
messageComposerInputMessageComposerInputPropsInput field area
messageComposerInputCenterMessageComposerInputCenterPropsCenter section of the input field
messageComposerInputLeadingMessageComposerInputLeadingPropsLeading icon(s) inside the input field
messageComposerInputHeaderMessageComposerInputHeaderPropsHeader row above the input field
messageComposerInputTrailingMessageComposerInputTrailingPropsTrailing icon(s) inside the input field
messageComposerAttachmentListStreamMessageComposerAttachmentListPropsHorizontal list of attachment previews
messageComposerAttachmentStreamMessageComposerAttachmentPropsIndividual attachment preview chip

Message composer

Example — move the send button outside the text input and add an emoji picker inside it:

StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      // Remove send button from inside the input field
      messageComposerInputTrailing: (context, props) => const SizedBox.shrink(),
      // Render send button outside (to the right of the whole composer bar)
      messageComposerTrailing: (context, props) =>
          DefaultStreamMessageComposerInputTrailing(props: props),
      // Add an emoji button inside the input on the left
      messageComposerInputLeading: (context, props) => StreamButton.icon(
        icon: Icon(context.streamIcons.emoji),
        type: StreamButtonType.ghost,
        style: StreamButtonStyle.secondary,
        size: StreamButtonSize.small,
        onPressed: () { /* open emoji picker */ },
      ),
    ),
  ),
  child: StreamMessageComposer(),
)

Attachments

Builder keyProps classDescription
imageAttachmentStreamImageAttachmentPropsSingle image attachment
videoAttachmentStreamVideoAttachmentPropsVideo attachment with playback
giphyAttachmentStreamGiphyAttachmentPropsGiphy/GIF attachment
galleryAttachmentStreamGalleryAttachmentPropsMulti-image gallery attachment
fileAttachmentStreamFileAttachmentPropsGeneric file attachment
linkPreviewAttachmentStreamLinkPreviewAttachmentPropsLink preview card
voiceRecordingAttachmentStreamVoiceRecordingAttachmentPropsVoice recording playback attachment
pollAttachmentStreamPollAttachmentPropsPoll attachment
quotedMessageStreamQuotedMessagePropsQuoted/replied message preview
unsupportedAttachmentStreamUnsupportedAttachmentPropsFallback for unrecognised attachment types
mediaGalleryStreamMediaGalleryPropsFull-screen media gallery
mediaGalleryPreviewStreamMediaGalleryPreviewPropsMedia gallery preview thumbnail strip

Note: The factory slots above replace the pre-built attachment widget for that type everywhere in the app. If you need to add a new custom attachment type or replace how built-in types render per-message, use StreamAttachmentWidgetBuilder instead — see Custom Attachment Builders.

Per-widget builder callbacks

Many high-level widgets expose itemBuilder or messageBuilder callbacks for customizing individual items without replacing the whole component:

StreamChannelListView(
  controller: _controller,
  itemBuilder: (context, channels, index, defaultTile) {
    final channel = channels[index];
    return ListTile(
      title: Text(channel.name ?? ''),
      onTap: () => openChannel(channel),
    );
  },
)
StreamMessageListView(
  messageBuilder: (context, message, defaultProps) {
    return StreamMessageItem.fromProps(
      props: defaultProps.copyWith(
        actionsBuilder: (context, defaultActions) => [
          ...defaultActions,
          StreamContextMenuAction(
            leading: const Icon(Icons.star),
            label: const Text('Favourite'),
            onTap: () => _favourite(message),
          ),
        ],
      ),
    );
  },
)

Theming vs. component replacement

Use theming for visual changes (colors, typography, spacing) and component replacement for structural or behavioral changes.

GoalApproach
Change colors, fonts, sizingStreamTheme / StreamChatThemeData
Hide or reorder parts of a componentcopyWith() on the props class
Replace a component entirelyStreamComponentFactory builder
Customize a single list itemitemBuilder / messageBuilder callback

See the Theming page for details on visual customization.