Users State & Filtering

StreamUserListController manages fetching, filtering, and pagination for a list of Stream Chat users. It is required by StreamUserListView and StreamUserGridView. See the pub.dev documentation for the full API reference.

Background

The StreamUserListController is a controller class that allows you to control a list of users. StreamUserListController is a required parameter of the StreamUserListView widget. Check the StreamUserListView documentation to read more about that.

Basic Example

Building a custom user list is a very common task. Here is an example of how to use the StreamUserListController to build a simple list with pagination.

First of all we should create an instance of the StreamUserListController and provide it with the StreamChatClient instance. You can also add a Filter, a list of SortOptions and other pagination-related parameters.

class UserListPageState extends State<UserListPage> {
  /// Controller used for loading more data and controlling pagination in
  /// [StreamUserListController].
  late final userListController = StreamUserListController(
    client: StreamChatCore.of(context).client,
  );

Make sure you call userListController.doInitialLoad() to load the initial data and userListController.dispose() when the controller is no longer required.

@override
void initState() {
  userListController.doInitialLoad();
  super.initState();
}

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

The StreamUserListController is basically a PagedValueNotifier that notifies you when the list of users has changed. You can use a PagedValueListenableBuilder to build your UI depending on the latest users.

@override
Widget build(BuildContext context) => Scaffold(
      body: PagedValueListenableBuilder<int, User>(
        valueListenable: userListController,
        builder: (context, value, child) {
          return value.when(
            (users, nextPageKey, error) => LazyLoadScrollView(
              onEndOfPage: () async {
                if (nextPageKey != null) {
                  userListController.loadMore(nextPageKey);
                }
              },
              child: ListView.builder(
                /// We're using the users 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)
                    ? users.length + 1
                    : users.length,
                itemBuilder: (BuildContext context, int index) {
                  if (index == users.length) {
                    if (error != null) {
                      return TextButton(
                          onPressed: () {
                            userListController.retry();
                          },
                          child: Text(error.message),
                        );
                    }
                    return const CircularProgressIndicator();
                  }

                  final _item = users[index];
                  return ListTile(
                    title: Text(_item.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: userListController.search,
)

search() matches the text against both the user name and id 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 filter rather 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.
userListController.searchWithFilter(
  Filter.and([
    Filter.equal('role', 'admin'),
    Filter.autoComplete('name', query),
  ]),
);

searchWithFilter() accepts null to match all users. 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:

userListController.clearResults();