widget
Fuery 的 widget 在 widget 树中渲染查询和变更,因此显示服务端数据的界面可以保持为 StatelessWidget。按来源和用途选择 widget:
| 重建 UI | 副作用 | 两者兼有 | 状态的一部分 | |
|---|---|---|---|---|
| 查询 | QueryBuilder |
QueryListener |
QueryConsumer |
QuerySelector |
| 无限查询 | InfiniteQueryBuilder |
InfiniteQueryListener |
InfiniteQueryConsumer |
InfiniteQuerySelector |
| 变更 | MutationBuilder |
MutationListener |
MutationConsumer |
MutationSelector |
| 多个查询 | QueriesBuilder |
QueriesSelector |
||
| 变更的每次执行 | MutationStateBuilder |
MutationStateListener |
MutationStateSelector |
查询、无限查询和变更 widget 接收一个定义:Query、InfiniteQuery 或 Mutation。在任何地方构建它,build 中也可以。widget 挂载期间,会为它保持一个观察者:
QueryBuilder( query: todoQuery(id), builder: (context, state) => Text(state.data?.title ?? '…'),)- 挂载时订阅。查询没有数据时会获取;默认情况下,数据过期时也会获取。设置了
enabled: false的查询在挂载时不获取。卸载时取消订阅。 - widget 用另一个键的定义重建时,观察者会跟随它。新键的缓存数据在同一帧中显示。
- 观察者使用最近的
FueryProvider的客户端;没有FueryProvider时使用Fuery.client。 - 结果带有各种操作:
state.refetch();无限查询的state.fetchNextPage()和state.fetchPreviousPage();变更的state.mutate(...)、state.mutateAsync(...)和state.reset()。
MutationBuilder 只显示它自己开始的执行。MutationState 系列 widget 按 mutationKey 找到变更的执行,显示来自任何地方的执行。参见显示变更的每次执行。用 addTodo.mutate('Buy milk', context.queryClient) 从定义执行变更的按钮不需要 MutationBuilder。参见执行变更。
构建器和监听器何时运行
Section titled “构建器和监听器何时运行”buildWhen(previous, current)比较上次构建所用的结果和新结果。listenWhen(previous, current)比较上一个结果和新结果。- 监听器在变化之后的一个微任务中运行,从不在构建期间运行。
- 对于监听器挂载时查询已有的结果,不会调用监听器。
- 消费者的监听器在显示这次变化的重建之前运行。
- 抛出异常的监听器不会阻止重建。Fuery 把它的错误报告给
onUncaughtError。
只重建发生变化的部分
Section titled “只重建发生变化的部分”buildWhen 跳过构建器不显示的那些变化引起的重建。这个构建器只在查询重新获取时显示一个进度条:
QueryBuilder( query: todosQuery, buildWhen: (previous, current) => previous.isRefetching != current.isRefetching, builder: (context, state) => state.isRefetching ? const LinearProgressIndicator() : const SizedBox(),)选择状态的一部分
Section titled “选择状态的一部分”选择器根据结果中的一个值构建,并且只在这个值变化时重建:
QuerySelector( query: todosQuery, selector: (state) => state.data?.where((todo) => todo.done).length ?? 0, builder: (context, doneCount) => Text('$doneCount done'),)- Fuery 按内容比较列表、Map 和 Set,其他值都用
==比较。每次调用都返回新列表的选择器,只在列表项变化时才让构建器重建。 - 父 widget 重建时,选择器会再次运行,因此它可以读取父 widget 中的值。
- 构建器需要整个结果时,使用
buildWhen。构建器需要从结果派生出的一个值时,使用选择器。
MutationStateSelector 对变更的执行做同样的事。这个示例统计来自任何界面、正在进行中的保存:
MutationStateSelector( mutation: saveTodo, selector: (runs) => runs.where((run) => run.isPending).length, builder: (context, saving) => Text(saving > 0 ? 'Saving $saving…' : 'All changes saved'),)用监听器处理导航、snackbar 和其他一次性副作用:
QueryListener( query: todosQuery, listenWhen: (previous, current) => current.isRefetchError, listener: (context, state) => ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Could not refresh: ${state.error}'))), child: const TodoScreen(),)对于变更:
MutationStateListener接收变更的每次执行,无论它来自哪个界面。参见告诉用户变更失败。- 要在界面自己的调用成功后关闭界面,用
await等待mutateAsync,然后检查context.mounted。参见在一次调用成功后采取行动。 MutationListener只接收它拿到的观察者的执行。传入定义时,它什么也接收不到,并在 debug 构建中打印一条警告。参见 MutationListener 从不运行。- 传入定义的
MutationConsumer接收它自己的构建器开始的执行。
state.refetch() 返回一个 Future<QueryResult>,它在获取结束时完成。RefreshIndicator 等待这个 Future:
QueryBuilder( query: todosQuery, builder: (context, state) => switch (state) { QueryResult(:final data?) => RefreshIndicator( onRefresh: () => state.refetch(), child: ListView( children: [for (final todo in data) TodoTile(todo)], ), ), QueryResult(:final error?) => Center(child: Text('$error')), _ => const Center(child: CircularProgressIndicator()), },)refetch()在结果中报告失败的获取,而不是抛出异常,因此指示器总会关闭。传入throwOnError: true可以让它抛出异常。- 查询已有数据时,
refetch()会取消进行中的获取,并开始一次新的获取。传入cancelRefetch: false则改为等待进行中的获取。没有数据的查询总是等待它。 - 只包裹数据分支。这个手势需要可滚动的 widget,而
pending分支和错误分支中没有。
要刷新一个界面的所有查询,调用客户端:
RefreshIndicator( onRefresh: () => context.queryClient.invalidateQueries(queryKey: ['todos']), child: const TodoList(),)invalidateQueries把['todos']下的所有查询标记为过期,并重新获取有观察者(例如已挂载的 widget)的查询。refetchQueries重新获取,但不把任何查询标记为过期。它的type默认为QueryTypeFilter.all,没有观察者的缓存条目(CachedQuery)也会重新获取。传入type: QueryTypeFilter.active,只重新获取有观察者的查询。- 两者都在所有匹配的获取结束时完成,并且只在
throwOnError: true时抛出异常。两者都不等待设备离线时暂停的获取,因此指示器不会卡住。
查询过滤器列出了两者接受的过滤器。
给错误分支加一个调用 state.refetch() 的按钮:
QueryBuilder( query: todosQuery, builder: (context, state) => switch (state) { QueryResult(:final data?) => TodoList(data), QueryResult(:final error?) => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('$error'), FilledButton( onPressed: state.isFetching ? null : () => state.refetch(), child: const Text('Try again'), ), ], ), ), _ => const Center(child: CircularProgressIndicator()), },)state.isFetching为 true 时禁用按钮,避免点击叠加在正在运行的获取上。- 先匹配数据分支。重新获取失败时会保留数据,因此列表仍留在屏幕上,并且
isRefetchError为 true。用QueryListener和 snackbar 报告这次失败,而不是替换整个界面。 - 默认情况下,查询在构建错误分支之前重试 3 次,依次等待 1 秒、2 秒和 4 秒。重试哪些错误介绍如何缩小重试范围。
同时显示多个查询
Section titled “同时显示多个查询”QueriesBuilder 根据一组同一数据类型的查询的结果构建,例如每个 id 一个查询:
QueriesBuilder( queries: [for (final id in cartIds) productQuery(id)], builder: (context, results) { if (results.any((result) => !result.hasData)) { return const CircularProgressIndicator(); } final total = results.fold(0.0, (sum, result) => sum + result.data!.price); return Text('Total: $total'); },)- 结果的顺序与查询的顺序一致。
- 在
build中构建这个列表。只要查询的键还在列表中,这个查询就保留它的观察者,即使列表重新排序也是如此。 - 离开列表的键会丢弃它的观察者。
- 同时变化的多个结果只引起一次重建。
- 构建
QueriesBuilder的 widget 每次重建,都会更新列表中的每个查询,即使 id 没有变化。查询有数百个时,把经常变化的状态(例如文本框的状态)放到另一个 widget 中。这样,构建QueriesBuilder的 widget 只在 id 变化时重建。
QueriesSelector 根据由这些结果合成的一个值构建,并且只在这个值变化时重建:
QueriesSelector( queries: [for (final id in ids) todoQuery(id)], selector: (results) => results.where((result) => result.data?.done ?? false).length, builder: (context, doneCount) => Text('$doneCount done'),)列表的每一项相互独立时,给每一项单独的 QueryBuilder,例如在 ListView.builder 中。这样,每个列表项只因自己的查询而重建。对于数据类型不同的查询,把一个 QueryBuilder 嵌套在另一个里面。
显示是否有获取正在进行
Section titled “显示是否有获取正在进行”跟随应用中每个查询的进度条读取的是客户端,而不是 widget。参见观察缓存。
对于变更,带 MutationFilters 的 MutationStateSelector 显示是否有变更正在执行:
MutationStateSelector( mutation: const MutationFilters(), selector: (runs) => runs.any((run) => run.isPending), builder: (context, saving) => saving ? const Text('Saving…') : const SizedBox(),)大多数界面传入定义。传入相同键的定义的 widget 已经共享缓存条目和请求。参见使用查询。
查询、无限查询和变更 widget,以及 QueriesBuilder 和 QueriesSelector,也接收 observe() 返回的观察者。它们原样使用这个观察者,包括它的选项和创建它时使用的客户端。只在以下情况传入观察者:cubit 和 widget 共享同一个句柄,或者多个 widget 只显示一个变更观察者的执行。
在 build 之外只创建一次观察者,例如在 cubit 或 State 字段中。每次调用 observe() 都会创建一个新的观察者,它会重新订阅并再次获取。共享一个观察者展示了 State 字段、客户端,以及 dispose 中的 reset()。
查询观察者无须释放。只有你持有的变更观察者需要在 dispose 中处理。
- 传入查询定义的 widget 在挂载时创建观察者,在卸载时销毁它。
observe()返回的观察者在有了第一个监听器时订阅它的查询。最后一个监听器(例如使用它的最后一个 widget)离开时,观察者取消它的过期计时器和重新获取计时器,并与查询分离。- 之后用同一个观察者挂载的 widget 会再次订阅它,因此持有查询观察者的
State字段无须在dispose中做任何事。 - 监听
stream的 cubit 在close()中取消它的订阅,这同样会取消观察者的订阅。参见在 cubit 中。
widget 消失后,你持有的变更观察者仍会运行它最近一次调用的 MutateOptions 回调。在 dispose 中对它调用 reset(),或者在使用 State 或它的 BuildContext 的回调中检查 mounted。以定义形式传入变更的 widget 在卸载时会重置它自己的观察者。
最后一个观察者离开后,缓存条目会在垃圾回收时间(gcTime,默认值:5 分钟)内保留,因此回到的界面会立即显示它的数据。
QueryObserver.destroy() 一次移除所有监听器。与最后一次取消订阅一样,它取消观察者的计时器,并让观察者与查询分离。widget 树中没有任何东西需要它。长期存在的对象必须停止某个观察者,却无法访问它的监听器时,调用它。
构建自己的 widget 或适配器
Section titled “构建自己的 widget 或适配器”要使用 hook,请用 fuery_hooks。要编写自己的 widget,或者为其他状态管理库编写适配器,参见构建适配器。它展示了如何用 fuery_core 的公开 API 渲染查询,这些 widget 使用的也是同一套 API。
在示例应用中
Section titled “在示例应用中”示例在信息流中用 buildWhen 显示重新获取指示器,实现了带重试按钮的下拉刷新,并用 MutationStateListener 显示 snackbar。它的 README 列出了每个界面展示的内容。