Custom Message List

This recipe shows how to replace only the message bubble layout while keeping all of StreamMessageListView's built-in behavior (pagination, reactions, threads, typing indicators).

Approach: messageBuilder with copyWith

Use the messageBuilder callback on StreamMessageListView to intercept each message and customize its props before rendering:

StreamMessageListView(
  messageBuilder: (context, message, defaultProps) {
    return StreamMessageItem.fromProps(
      props: defaultProps.copyWith(
        // Enable swipe-to-reply gesture
        swipeToReply: true,
        // Custom padding for the message bubble
        padding: const EdgeInsets.symmetric(
          horizontal: 16,
          vertical: 8,
        ),
      ),
    );
  },
)

Approach: fully custom message widget

For a completely different layout, return your own widget from messageBuilder. Read the message from defaultProps.message and determine ownership by comparing the sender's ID against the current user:

StreamMessageListView(
  messageBuilder: (context, message, defaultProps) {
    final currentUserId = StreamChat.of(context).currentUser?.id;
    final isOwn = message.user?.id == currentUserId;
    return Align(
      alignment: isOwn ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: isOwn ? Colors.blue : Colors.grey.shade200,
          borderRadius: BorderRadius.circular(16),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            if (!isOwn)
              Text(
                message.user?.name ?? '',
                style: const TextStyle(fontWeight: FontWeight.bold),
              ),
            Text(
              message.text ?? '',
              style: TextStyle(
                color: isOwn ? Colors.white : Colors.black87,
              ),
            ),
          ],
        ),
      ),
    );
  },
)

Approach: app-wide via StreamComponentFactory

To apply a custom message layout throughout the entire app without modifying each StreamMessageListView, use StreamComponentFactory:

StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      messageItem: (context, props) => MyCustomMessage(props: props),
    ),
  ),
  child: MyApp(),
)

Tips

  • Keep system and ephemeral messages (typing indicators, deleted messages) handled by defaultProps to avoid breaking built-in behavior.
  • To check if a message belongs to the current user, compare message.user?.id == StreamChat.of(context).currentUser?.id.
  • Reactions, threads, and editing are driven by actionsBuilder on StreamMessageItemProps. Copy the default actions and add or remove items rather than replacing the list entirely.