class MemberListPageState extends State<MemberListPage> {
/// Controller used for loading more data and controlling pagination in
/// [StreamMemberListController].
late final memberListController = StreamMemberListController(
channel: StreamChannel.of(context).channel,
);Members State
StreamMemberListController manages fetching, filtering, and pagination for a list of channel members. It is required by StreamMemberListView and StreamMemberGridView, and supports real-time updates as members join or leave. See the pub.dev documentation for the full API reference.
Background
The StreamMemberListController is a controller class that allows you to control a list of users.
StreamMemberListController is a required parameter of the StreamMemberListView widget.
Check the StreamMemberListView documentation to read more about that.
Basic Example
Building a custom member list is a very common task. Here is an example of how to use the StreamMemberListController to build a simple list with pagination.
First, create an instance of the StreamMemberListController and provide it with the StreamChatClient instance.
You can also add a Filter, a list of SortOptions, and other pagination-related parameters.
Make sure you call memberListController.doInitialLoad() to load the initial data and memberListController.dispose() when the controller is no longer required.
@override
void initState() {
memberListController.doInitialLoad();
super.initState();
}
@override
void dispose() {
memberListController.dispose();
super.dispose();
}The StreamMemberListController is basically a PagedValueNotifier that notifies you when the list of members has changed.
You can use a PagedValueListenableBuilder to build your UI depending on the latest members.
@override
Widget build(BuildContext context) => Scaffold(
body: PagedValueListenableBuilder<int, Member>(
valueListenable: memberListController,
builder: (context, value, child) {
return value.when(
(members, nextPageKey, error) => LazyLoadScrollView(
onEndOfPage: () async {
if (nextPageKey != null) {
memberListController.loadMore(nextPageKey);
}
},
child: ListView.builder(
/// We're using the members length when there are no more
/// pages to load and there are no errors with pagination.
/// In case we need to show a loading indicator or and error
/// tile we're increasing the count by 1.
itemCount: (nextPageKey != null || error != null)
? members.length + 1
: members.length,
itemBuilder: (BuildContext context, int index) {
if (index == members.length) {
if (error != null) {
return TextButton(
onPressed: () {
memberListController.retry();
},
child: Text(error.message),
);
}
return const CircularProgressIndicator();
}
final _item = members[index];
return ListTile(
title: Text(_item.user?.name ?? ''),
);
},
),
),
loading: () => const Center(
child: SizedBox(
height: 100,
width: 100,
child: CircularProgressIndicator(),
),
),
error: (e) => Center(
child: Text(
'Oh no, something went wrong. '
'Please check your config. $e',
),
),
);
},
),
);In this case, we're using the LazyLoadScrollView widget to load more data when the user scrolls to the bottom of the list.
Searching
Call search() with the text from your search field. The controller debounces the reload for you and drops superseded results, so you don't need to throttle the input yourself:
TextField(
onChanged: memberListController.search,
)search() matches the text against the member name as an autocomplete filter. Two things are worth knowing about how it interacts with the controller's own filter:
- The search filter replaces the base
filterrather than narrowing it. Passing a blank query restores the base filter. - The debounce is keyed to the query length — short, less selective queries wait a little longer before firing, which keeps a burst of keystrokes from turning into a burst of requests.
Because rapidly superseded searches are discarded, a slow response for "al" can no longer land after the results for "alice".
To search on other fields, or to keep the base filter applied while searching, build the Filter yourself and pass it to searchWithFilter():
// Narrow the base filter instead of replacing it.
memberListController.searchWithFilter(
Filter.and([
Filter.equal('channel_role', 'channel_moderator'),
Filter.autoComplete('name', query),
]),
);searchWithFilter() accepts null to match all members. It debounces the same way when the filter carries a text-search operator (Filter.autoComplete or Filter.query), including when that operator is nested inside a compound filter. A filter with no search text — an exact-match lookup, say — reloads immediately instead of waiting.
To cancel a pending search and empty the list, for example when the user clears the field:
memberListController.clearResults();