StreamChannelListView

StreamChannelListView is the primary widget for displaying a scrollable, paginated list of channels. It pairs with StreamChannelListController to fetch, filter, and sort channels in real time, and handles loading, empty, and error states out of the box. See the pub.dev documentation for the full API reference.

Background

Channels are fundamental elements of Stream Chat and constitute shared spaces which allow users to message each other.

1:1 conversations and groups are both examples of channels, albeit with some (distinct/non-distinct) differences. Displaying the list of channels that a user is a part of is a pattern present in most messaging apps.

The StreamChannelListView widget allows displaying a list of channels to a user. By default, this is NOT ONLY the channels that the user is a part of. This section goes into setting up and using a StreamChannelListView widget.

Make sure to check the StreamChannelListController documentation for more information on how to use the controller to manipulate the StreamChannelListView.

Basic Example

Here is a basic example of the StreamChannelListView widget. It consists of the main widget itself, a StreamChannelListController to control the list of channels and a callback to handle the tap of a channel.

class ChannelListPage extends StatefulWidget {
  const ChannelListPage({
    super.key,
    required this.client,
  });

  final StreamChatClient client;

  @override
  State<ChannelListPage> createState() => _ChannelListPageState();
}

class _ChannelListPageState extends State<ChannelListPage> {
  late final _controller = StreamChannelListController(
    client: widget.client,
    filter: Filter.in_(
      'members',
      [StreamChat.of(context).currentUser!.id],
    ),
    channelStateSort: const [SortOption<ChannelState>.desc('last_message_at')],
  );

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        body: RefreshIndicator(
          onRefresh: _controller.refresh,
          child: StreamChannelListView(
            controller: _controller,
            onChannelTap: (channel) => Navigator.push(
              context,
              MaterialPageRoute(
                builder: (_) => StreamChannel(
                  channel: channel,
                  child: const ChannelPage(),
                ),
              ),
            ),
          ),
        ),
      );
}

Channel List Item (StreamChannelListItem)

Each row in the list is rendered by StreamChannelListItem. It resolves the channel's avatar, name, last-message preview, unread count, and timestamp. It also shows draft previews when a channel has a saved draft.

ParameterTypeDescription
channelChannelThe channel to display
onTapGestureTapCallback?Called when the row is tapped
onLongPressGestureLongPressCallback?Called on long-press
selectedboolHighlights the row (e.g. in a multi-select flow)
StreamChannelListItem(
  channel: channel,
  onTap: () => openChannel(channel),
)

Customizing the Channel List Item

The primary way to customize channel list items for a single list is via the itemBuilder parameter on StreamChannelListView. Use it to replace the default row widget entirely — you have access to the full channel list, the current index, and the default tile widget:

StreamChannelListView(
  controller: _controller,
  itemBuilder: (context, channels, index, defaultTile) {
    final channel = channels[index];
    return StreamChannelListItem(
      channel: channel,
      onTap: () => openChannel(channel),
      leading: StreamChannelAvatar(channel: channel),
      title: Text(channel.name ?? ''),
      subtitle: Text(channel.lastMessageAt?.toString() ?? ''),
    );
  },
),

For app-wide customization (across the entire widget tree rather than a single list), use StreamComponentFactory with the channelListItem slot:

StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      channelListItem: (context, props) => 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(),
)

StreamChannelListTile is the low-level presentational component that renders pre-resolved slot values without any channel-specific logic.

Theming Channel List Items

Use StreamChannelListItemThemeData to style list items globally:

StreamChatTheme(
  data: StreamChatThemeData(
    channelListItemTheme: StreamChannelListItemThemeData(
      titleStyle: const TextStyle(fontWeight: FontWeight.bold),
      subtitleStyle: const TextStyle(color: Colors.grey),
      timestampStyle: const TextStyle(fontSize: 12),
    ),
  ),
  child: MyApp(),
)

To scope a theme override to a subtree, wrap with StreamChannelListItemTheme directly:

StreamChannelListItemTheme(
  data: StreamChannelListItemThemeData(
    titleStyle: const TextStyle(color: Colors.blue),
  ),
  child: StreamChannelListView(controller: _controller),
)

Mute and Pin Attribute Icons

The position of the mute and pin attribute icons is controlled via StreamChannelListItemThemeData.attributePosition. Set it globally through StreamChatThemeData.channelListItemTheme:

StreamChatTheme(
  data: StreamChatThemeData(
    channelListItemTheme: StreamChannelListItemThemeData(
      attributePosition: AttributePosition.trailingBottom,
    ),
  ),
  child: MyApp(),
)
AttributePositionBehavior
.inlineTitleIcon shown next to the channel name (default)
.trailingBottomIcon shown in the bottom-right trailing area

Swipe Actions

Swipe actions let users perform quick operations on a channel row by swiping. Common examples include viewing channel info and muting or unmuting a channel.

Slidable demo

Adding swipe actions with a package

For swipe-to-reveal actions on each channel row, combine StreamChannelListView's itemBuilder with the flutter_slidable package — it handles the gesture and animation, while itemBuilder lets you wrap each tile.

Add the dependency:

dependencies:
  flutter_slidable: ^3.1.1

Example: info and mute actions

The following example adds two swipe actions to each channel row: one to open the channel info sheet, and one to toggle mute on the channel.

import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

SlidableAutoCloseBehavior(
  child: StreamChannelListView(
    controller: _controller,
    itemBuilder: (context, channels, index, defaultTile) {
      final channel = channels[index];
      final colorScheme = StreamTheme.of(context).colorScheme;
      return StreamBuilder<bool>(
        stream: channel.isMutedStream,
        initialData: channel.isMuted,
        builder: (context, snapshot) {
          final isMuted = snapshot.data ?? false;
          return Slidable(
            groupTag: 'channels-actions',
            endActionPane: ActionPane(
              extentRatio: 0.4,
              motion: const BehindMotion(),
              children: [
                CustomSlidableAction(
                  backgroundColor: colorScheme.backgroundSurfaceSubtle,
                  onPressed: (_) {
                    showStreamSheet<void>(
                      context: context,
                      builder: (sheetContext, scrollController) => Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          StreamSheetHeader(title: Text(channel.name ?? '')),
                          // ... your channel info content
                        ],
                      ),
                    );
                  },
                  child: const Icon(Icons.more_horiz),
                ),
                CustomSlidableAction(
                  backgroundColor: colorScheme.accentPrimary,
                  foregroundColor: Colors.white,
                  onPressed: (_) async {
                    if (isMuted) {
                      await channel.unmute();
                    } else {
                      await channel.mute();
                    }
                  },
                  child: Icon(
                    isMuted ? Icons.volume_up : Icons.volume_off,
                    color: Colors.white,
                  ),
                ),
              ],
            ),
            child: defaultTile,
          );
        },
      );
    },
  ),
)

SlidableAutoCloseBehavior automatically closes any open action pane when the user taps elsewhere or scrolls the list. groupTag ensures only one row is open at a time.

Available channel actions

ActionMethod
Mutechannel.mute()
Unmutechannel.unmute()
Hidechannel.hide()
Deletechannel.delete()
Mark as readchannel.markRead()