class SearchListPageState extends State<SearchListPage> {
/// Controller used for loading more data and controlling pagination in
/// [StreamMessageSearchListController].
late final messageSearchListController = StreamMessageSearchListController(
client: StreamChatCore.of(context).client,
filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]),
);Message Search State
StreamMessageSearchListController manages search queries, pagination, and result state for cross-channel message search. It is required by StreamMessageSearchListView and can also be used to build fully custom search UIs. See the pub.dev documentation for the full API reference.
Background
The StreamMessageSearchListController is a controller class that allows you to control a list of searched messages.
StreamMessageSearchListController is a required parameter of the StreamMessageSearchListView widget.
Check the StreamMessageSearchListView documentation to read more about that.
Basic Example
Building a custom message search feature is a common task. Here is an example of how to use the StreamMessageSearchListController to build a simple search list with pagination.
First of all we should create an instance of the StreamMessageSearchListController and provide it with the StreamChatClient instance.
We can then add a filter to only get the channels that the current user is a part of.
You can also add a list of SortOptions and other pagination-related parameters.
Make sure you call messageSearchListController.doInitialLoad() to load the initial data and messageSearchListController.dispose() when the controller is no longer required.
@override
void initState() {
messageSearchListController.doInitialLoad();
super.initState();
}
@override
void dispose() {
messageSearchListController.dispose();
super.dispose();
}The StreamMessageSearchListController is basically a PagedValueNotifier that notifies you when the list of responses has changed.
You can use a PagedValueListenableBuilder to build your UI depending on the latest responses.
@override
Widget build(BuildContext context) => Scaffold(
body: Column(
children: [
TextField(
/// `search` debounces the reload and drops superseded results,
/// so the field does not need to throttle its input.
onChanged: messageSearchListController.search,
),
Expanded(
child: PagedValueListenableBuilder<String, GetMessageResponse>(
valueListenable: messageSearchListController,
builder: (context, value, child) {
return value.when(
(responses, nextPageKey, error) => LazyLoadScrollView(
onEndOfPage: () async {
if (nextPageKey != null) {
messageSearchListController.loadMore(nextPageKey);
}
},
child: ListView.builder(
/// We're using the responses 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)
? responses.length + 1
: responses.length,
itemBuilder: (BuildContext context, int index) {
if (index == responses.length) {
if (error != null) {
return TextButton(
onPressed: () {
messageSearchListController.retry();
},
child: Text(error.message),
);
}
return const CircularProgressIndicator();
}
final _item = responses[index];
return ListTile(
title: Text(_item.channel?.name ?? ''),
subtitle: Text(_item.message.text ?? ''),
);
},
),
),
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
As shown above, wire your search field straight to search():
TextField(
onChanged: messageSearchListController.search,
)search() matches the text against the message text as an autocomplete filter, and handles the two things you would otherwise write yourself:
- Debouncing, 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.
- Dropping superseded results, so a slow response for
"me"can no longer land after the results for"meeting".
A blank query clears the results rather than querying, because the backend rejects an empty message search.
For anything beyond a prefix match on text, build the message Filter yourself and pass it to searchWithFilter(). For example, to use full-text query matching instead of the prefix-oriented autoComplete:
messageSearchListController.searchWithFilter(
Filter.query('text', query),
);The filter becomes the active message filter and clears any active search query. See Understanding Filters for the fields a message search accepts. 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 reloads immediately instead of waiting.
To cancel a pending search and empty the results, for example when the user clears the field:
messageSearchListController.clearResults();Note:
search()andsearchWithFilter()replace the older pattern of settingsearchQuery(ormessageFilter) and then callingdoInitialLoad()yourself. That pattern still works, but it does no debouncing and does not discard superseded responses, so a fast typist can see results flicker between queries.