⚡FQuery 是一个功能强大的 Flutter 异步状态管理解决方案。它可以缓存、更新和完全管理您的 Flutter 应用中的异步数据。
它可以用于管理服务器状态(REST API、GraphQL 等)、本地数据库(如 SQLite),或者任何异步内容,只需给它一个 Future 即可。
? 特点
- 完全可定制
- 无样板代码,易于使用
- 数据获取逻辑无关
- 自动缓存
- 垃圾回收
- 自动重新获取陈旧数据
- 状态数据失效
- 手动更新
- 依赖查询
- 并行查询
❔问题定义
我问您一个简单的问题:您在 Flutter 应用中如何管理服务器状态? 大多数开发者会回答他们使用 Riverpod、Bloc、FutureBuilder 或其他通用状态管理解决方案。这通常会导致编写大量样板代码,并反复重复数据获取、缓存和其他逻辑。
问题在于,现有的状态管理解决方案非常通用,适用于您应用中的任何全局状态因此称为“通用”,但在用于异步状态(如服务器状态)时效果不佳,因为服务器状态的差异太大。服务器状态是——
- 异步状态,需要异步 API 来获取和更新)
- 存储在远程位置,并且可能在您不知情的情况下,从世界任何地方被更改,仅仅这一点就意味着很多,保持数据同步并确保数据不是陈旧的
FQuery 如何解决这个问题?
FQuery 由 flutter_hooks 提供支持。它与 swr 和 react-query 非常相似。它为您提供易于使用的 hooks。只需通过提供 Future 来告诉它从哪里获取数据,其余的都是自动的。它可以完全配置以满足您的需求,您可以配置所有内容。
? 示例
这是一个使用 useQuery hook 的非常简单的 widget
class Posts extends HookWidget {
const Posts({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final posts = useQuery(['posts'], getPosts);
return Builder(
builder: (context) {
if (posts.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (posts.isError) {
return Center(child: Text(posts.error!.toString()));
}
return ListView.builder(
itemCount: posts.data!.length,
itemBuilder: (context, index) {
final post = posts.data![index];
return ListTile(
title: Text(post.title),
);
},
);
},
);
}
}
?? 用法
您需要先安装 flutter_hooks 才能开始使用此库。您需要将整个应用包装在 QueryClientProvider 中,然后就可以使用了。
void main() {
runApp(
QueryClientProvider(
queryClient: queryClient,
child: CupertinoApp(
Queries(查询)
要在您的 widgets 中查询数据,您需要通过继承 HookWidget 或 StatefulHookWidget(用于有状态 widgets)来扩展 widget。这些类是从 flutter_hooks 包导出的。
查询实例是对缓存中异步数据的订阅。每个查询都需要——
- 一个查询键,它唯一地标识了存储在缓存中的查询。
- 一个
Future,它要么解析,要么抛出错误
相同的查询键可以在多个 useQuery hook 实例中使用,并且数据将在整个应用中共享。
Future<List<Post>> getPosts() async {
final res = await Dio().get('https://jsonplaceholder.typicode.com/posts');
return (res.data as List)
.map((e) => Post.fromJson(e as Map<String, dynamic>))
.toList();
}
class Posts extends HookWidget {
const Posts({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final posts = useQuery(['posts'], getPosts);
useQuery hook 返回的值是 UseQueryResult 的实例,并包含与该查询相关的所有信息。在渲染结果时,Builder widget 会非常方便。
// The query has no data to display
if (posts.isLoading) {
return const Center(child: CircularProgressIndicator());
}
// An error has occurred
if (posts.isError) {
return Center(child: Text(posts.error!.toString()));
}
// Success, data is ready to display
return ListView.builder(
itemCount: posts.data!.length,
itemBuilder: (context, index) {
final post = posts.data![index];
return ListTile(
title: Text(post.title),
);
},
);
查询配置
查询是完全可定制的,以满足您的需求,这些配置可以作为命名参数传递给 useQuery hook
// These are default configurations
final posts = useQuery(
['posts'],
getPosts,
enabled: true,
cacheDuration: const Duration(minutes: 5),
refetchInterval: null // The query will not refetch by default,
refetchOnMount: RefetchOnMount.stale,
staleDuration: const Duration(seconds: 10),
);
enabled– 指定 widget 渲染时是否自动调用查询获取器函数,可用于*依赖查询*cacheDuration– 指定未使用的/不活动的缓存数据在内存中保留的时间,缓存数据将在该时间后被垃圾回收。当在多个查询实例中指定不同的值时,将使用最长的时间。refetchInterval– 指定所有查询重新获取数据的间隔时间,将其设置为null(默认值)将关闭重新获取refetchOnMount– 指定 widget 首次构建时查询实例的行为,以及数据是否已可用。RefetchOnMount.always– widget 构建时将始终重新获取。RefetchOnMount.stale– 如果数据已过时(参见staleDuration),则会获取数据。RefetchOnMount.never– 永远不会重新获取
staleDuration– 指定数据变为陈旧的持续时间。此值适用于每个查询实例
查询失效
此技术可用于手动将缓存数据标记为陈旧,甚至可能重新获取它们。当您知道数据已更改时,这尤其有用。QueryClient(见下文)有一个 invalidateQueries() 方法,允许您执行此操作。您可以使用 useQueryClient hook 来获取 QueryClient 的实例,该实例与 QueryClientProvider 一起传递。
final queryClient = useQueryClient();
// Invalidate every query with a key that starts with `post`
queryClient.invalidateQueries(['posts']);
// Both queries will be invalidated
final posts = useQuery(['posts'], getPosts);
final post = useQuery(['posts', 1], getPosts);
// Use `exact: true` to exactly match the query
queryClient.invalidateQueries(['posts'], exact: true);
// Only this will invalidate
final posts = useQuery(['posts'], getPosts);
当查询失效时,将发生两件事
- 它将其标记为陈旧,这将覆盖传递给
useQuery的任何staleDuration配置。 - 如果查询正在 widget 中使用,它将被重新获取;否则,它将在稍后由 widget 使用时被重新获取。
手动更新
您可能已经知道数据是如何更改的,并且不想重新获取所有数据。您可以使用 QueryClient 上的 setQueryData() 方法手动设置它。它接受一个查询键和一个更新函数。如果查询数据尚不存在于缓存中(这就是为什么 previous 是可空的),它将被创建。
final queryClient = useQueryClient();
// The `Type` of returned data must match the `Type` of data
// stored in the cache, otherwise an error will be thrown
queryClient.setQueryData<List<Post>>(['posts'], (previous) {
return previous?.map((post) {
return post.copyWith(
title: "lorem ipsum"
);
}).toList() ?? <Post>[]
})
QueryClient
QueryClient 用于与查询缓存进行交互。它通过 QueryClientProvider 在整个应用中可用。它可以配置以更改查询的默认配置。
final queryClient = QueryClient(
defaultQueryOptions: DefaultQueryOptions(
cacheDuration: Duration(minutes: 20),
refetchInterval: Duration(minutes: 5),
refetchOnMount: RefetchOnMount.always,
staleDuration: Duration(minutes: 3),
),
);
void main() {
runApp(
QueryClientProvider(
queryClient: queryClient,
child: CupertinoApp(
错误和建议
欢迎在 GitHub 仓库 提出 issue 或建议。