class CustomChannelListPage extends StatefulWidget {
const CustomChannelListPage({super.key, required this.client});
final StreamChatClient client;
@override
State<CustomChannelListPage> createState() => _CustomChannelListPageState();
}
class _CustomChannelListPageState extends State<CustomChannelListPage> {
late final StreamChannelListController _controller;
@override
void initState() {
super.initState();
_controller = StreamChannelListController(
client: widget.client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
channelStateSort: const [SortOption<ChannelState>.desc('last_message_at')],
limit: 20,
);
_controller.doInitialLoad();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Messages')),
body: PagedValueListenableBuilder<int, Channel>(
valueListenable: _controller,
builder: (context, value, child) {
return value.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error) => Center(child: Text(error.message)),
data: (channels, nextPageKey, error) {
return ListView.builder(
itemCount: channels.length + (nextPageKey != null ? 1 : 0),
itemBuilder: (context, index) {
if (index == channels.length) {
_controller.fetchNextPage();
return const Center(child: CircularProgressIndicator());
}
final channel = channels[index];
return _ChannelListItem(
channel: channel,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
),
),
);
},
);
},
);
},
),
);
}
}
class _ChannelListItem extends StatelessWidget {
const _ChannelListItem({required this.channel, required this.onTap});
final Channel channel;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final lastMessage = channel.state?.messages.last;
return ListTile(
leading: StreamChannelAvatar(channel: channel),
title: StreamChannelName(channel: channel),
subtitle: lastMessage != null ? Text(lastMessage.text ?? '') : null,
trailing: channel.state?.unreadCount != null &&
channel.state!.unreadCount > 0
? Badge(label: Text('${channel.state!.unreadCount}'))
: null,
onTap: onTap,
);
}
}Custom Channel List
This recipe shows how to build a channel list with a fully custom item layout using the low-level StreamChannelListController and a standard ListView.
When to use this approach
Use a custom channel list when the itemBuilder parameter on StreamChannelListView is not flexible enough for your design. The controller handles all data fetching, pagination, and real-time updates, while you own the list rendering.
Implementation
Key components
| Component | Purpose |
|---|---|
StreamChannelListController | Fetches, paginates, and streams channel updates |
PagedValueListenableBuilder | Rebuilds the list when the controller emits new data |
StreamChannelAvatar | Renders the channel's avatar (1:1 or group) |
StreamChannelName | Renders the channel's display name |