这个简短的教程展示了如何在Flutter中用RxDart实现搜索。
我们将使用GitHub搜索API,但同样的概念也适用于任何其他搜索REST APIs。
我们的目标是拥有良好的搜索用户体验,同时不给服务器带来过多的负载,也不影响客户端的带宽和电池寿命。
GitHub搜索示例应用程序
让我们开始看看一个简单的工作应用。
这使用了 SearchDelegate类来显示符合输入搜索查询的用户列表。
为了建立这个应用,我们需要一些组件。
- 一个
GitHubSearchAPIWrapper类,从 GitHub REST API 中提取数据 - 一个
GitHubSearchResult模型类,包含 API 响应数据 - 一个
GitHubSearchDelegate类,显示带有结果网格的搜索用户界面 - 一个
GitHubSearchService类,包含所有用于连接 API 封装器和用户界面的逻辑。
下面是所有东西是如何连接的。
这是一个简短的教程,所以我们将只关注GitHubSearchService 类。
但你可以在GitHub上查看完整的源代码,了解其余所有细节。
GitHubSearchService
让我们从代表这个类的起点的一些代码开始。
class GitHubSearchService {
GitHubSearchService({@required this.apiWrapper});
final GitHubSearchAPIWrapper apiWrapper;
// Input stream (search terms)
final _searchTerms = BehaviorSubject<String>();
void searchUser(String query) => _searchTerms.add(query);
// Output stream (search results)
final _results = BehaviorSubject<GitHubSearchResult>();
Stream<GitHubSearchResult> get results => _results.stream;
void dispose() {
_results.close();
_searchTerms.close();
}
}
我们有一个搜索词的输入流(当我们在搜索框上输入时产生),和一个结果数据的输出流(在用户界面上显示为项目的网格)。
这个类需要一个GitHubSearchAPIWrapper 作为构造函数参数。
我们需要研究如何在输入发生变化时向输出流中添加数值。
将输入映射到输出:第一次尝试
作为第一次尝试,让我们给_searchTerms 流添加一个监听器。
GitHubSearchService({@required this.apiWrapper}) {
_searchTerms.listen((query) async {
print('searching: $query');
// get new result from the api
final result = await apiWrapper.searchUser(query);
print('received result for: $query');
// add to the output stream
_results.add(result);
});
}
每次搜索查询发生变化时,这段代码就会调用API并将结果添加到输出流中。
虽然这种方法看起来合乎逻辑,但它在实践中并没有很好地发挥作用。
为什么呢?因为不能保证结果是按照搜索词的顺序回来的。
当我们有一个不可靠的连接时,这是一个问题,会导致不按顺序的结果和糟糕的用户体验。
// example log
searching: b
searching: bi
searching: biz
searching: bizz
searching: bizz8
searching: bizz84
received result for: b
received result for: bi
received result for: bizz
received result for: bizz8
received result for: bizz84
received result for: biz
asyncMap
让我们尝试使用asyncMap 操作符来代替。
GitHubSearchService({@required this.apiWrapper}) {
_results = _searchTerms.asyncMap((query) async {
print('searching: $query');
return await apiWrapper.searchUser(query);
}); // discard previous events
}
// To make this work, `_results` is now decleared as a `Stream`
Stream<GitHubSearchResult> _results;
asyncMap 保证输出的事件与输入的顺序相同。
这个解决方案解决了失序的问题,但它有一个主要的缺点。
如果其中一个输出延迟到达,那么所有后续的输出也会延迟。
相反,我们应该在搜索查询发生变化时立即丢弃任何飞行中的请求。
这可以通过switchMap 操作符来实现。
switchMap
switchMap 为我们做了正确的事情,可以像这样使用。
GitHubSearchService({@required this.apiWrapper}) {
_results = _searchTerms.switchMap((query) async* {
print('searching: $query');
yield await apiWrapper.searchUser(query);
}); // discard previous events
}
注意,在这种情况下,我们使用的是带有async* 语法的流生成器。
有了这个设置,一旦有新的搜索词进来,我们就可以丢弃任何飞行中的请求。
但在GitHub Search API中,仍有一个问题特别明显。
如果我们过快地提交了太多的查询,就会出现 "rate limit exceeded "的错误。
{
"message": "API rate limit exceeded for 82.37.171.3. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
"documentation_url": "https://developer.github.com/v3/#rate-limiting"
}
去抖动
Debounce通过将所有输入事件 "搁置 "一定时间来减轻服务器的压力。
我们可以通过添加一行来轻松地对输入流进行去抖。
_results = _searchTerms
.debounce((_) => TimerStream(true, Duration(milliseconds: 500)))
.switchMap((query) async* {
print('searching: $query');
yield await apiWrapper.searchUser(query);
}); // discard previous events
我们可以根据自己的喜好控制去抖的时间(500ms是一个很好的默认值)。
就这样,我们拥有了它!一个高效的搜索实现,感觉很敏捷,不会让服务器或客户端负担过重。
荣誉
本教程在很大程度上受到Brian Egan和Filip Hracek在ReactiveConf 2018上的演讲的启发。
完整的源代码可用于GitHub搜索实例--欢迎在你自己的应用程序中使用它作为参考。
编码愉快!