StreamChannelListView(
controller: _controller,
onChannelTap: (channel) => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
),
),
)Channel
StreamChannel is an InheritedWidget that provides a Channel instance to its subtree. Every screen that shows channel content (message list, header, composer) must be wrapped in a StreamChannel.
Find the pub.dev documentation here
Setting up a channel screen
The typical pattern is to wrap the destination page with StreamChannel when navigating from a channel list:
Inside ChannelPage, all Stream widgets automatically read the channel from StreamChannel.of(context):
class ChannelPage extends StatelessWidget {
const ChannelPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: [
const Expanded(child: StreamMessageListView()),
const StreamMessageComposer(),
],
),
);
}
}Accessing the channel in custom widgets
Use StreamChannel.of(context) to read the current channel anywhere inside the subtree:
final channel = StreamChannel.of(context).channel;
final state = channel.state; // ChannelClientStateChannel.state (a ChannelClientState) is where reactive data lives, exposing unread counts, pinned messages, typing users, and more.
Initial message loading
By default, StreamChannel loads the most recent messages when it mounts. Pass initialMessageId to load the channel starting at a specific message (for example, when navigating from a push notification):
StreamChannel(
channel: channel,
initialMessageId: notification.messageId,
child: const ChannelPage(),
)Default constructor vs StreamChannel.value
StreamChannel ships two constructors with different semantics:
StreamChannel(...)— the default. Initializes the channel and positions the loaded message window on mount (jumping toinitialMessageId, the last-read marker, or the latest message). Use this on the main channel-page route.StreamChannel.value(...)— provides the sameChannelto the subtree without repositioning the loaded window. Use this when wrapping a sub-route or overlay just for context access — for example a thread page, channel info screen, long-press modal, attachment viewer, or any nested route that should not re-run channel positioning and overwrite what the parent route already loaded.
// Main channel route — positions the window.
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
),
);
// Sub-route that needs the same channel context but should not reposition.
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamChannel.value(
channel: channel,
child: const ChannelInfoPage(),
),
),
);Composing a channel screen
Build your channel screen by combining StreamChannelHeader, StreamMessageListView, and StreamMessageComposer inside a StreamChannel ancestor. Each widget is independent, so you can lay them out however your design requires and swap individual parts via the typed builders and componentBuilders slots described on the linked component pages.
Threads
Thread pages should also be wrapped in a dedicated StreamChannel (or use StreamMessageListView's built-in thread support via the parentMessage parameter). The composition mirrors the regular channel page — StreamThreadHeader instead of StreamChannelHeader, and StreamMessageListView(parentMessage: parent) to scope the list:
class ThreadPage extends StatefulWidget {
const ThreadPage({super.key, required this.parent});
final Message parent;
@override
State<ThreadPage> createState() => _ThreadPageState();
}
class _ThreadPageState extends State<ThreadPage> {
late final StreamMessageComposerController _composerController;
@override
void initState() {
super.initState();
// Seeding the controller with parentId puts the composer in thread mode —
// outgoing messages are posted as replies to widget.parent.
_composerController = StreamMessageComposerController(
message: Message(parentId: widget.parent.id),
);
}
@override
void dispose() {
_composerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: StreamThreadHeader(parent: widget.parent),
body: Column(
children: [
Expanded(child: StreamMessageListView(parentMessage: widget.parent)),
StreamMessageComposer(messageComposerController: _composerController),
],
),
);
}
}See the Thread List documentation for details on listing threads.