⚡FQuery 是一个功能强大的 Flutter 异步状态管理解决方案。它可以缓存、更新和完全管理您的 Flutter 应用中的异步数据。

它可以用于管理服务器状态(REST API、GraphQL 等)、本地数据库(如 SQLite),或者任何异步内容,只需给它一个 Future 即可。

? 特点

  • 完全可定制
  • 无样板代码,易于使用
  • 数据获取逻辑无关
  • 自动缓存
  • 垃圾回收
  • 自动重新获取陈旧数据
  • 状态数据失效
  • 手动更新
  • 依赖查询
  • 并行查询

❔问题定义

我问您一个简单的问题:您在 Flutter 应用中如何管理服务器状态? 大多数开发者会回答他们使用 Riverpod、Bloc、FutureBuilder 或其他通用状态管理解决方案。这通常会导致编写大量样板代码,并反复重复数据获取、缓存和其他逻辑。

问题在于,现有的状态管理解决方案非常通用,适用于您应用中的任何全局状态因此称为“通用”,但在用于异步状态(如服务器状态)时效果不佳,因为服务器状态的差异太大。服务器状态是——

  • 异步状态,需要异步 API 来获取和更新)
  • 存储在远程位置,并且可能在您不知情的情况下,从世界任何地方被更改仅仅这一点就意味着很多,保持数据同步并确保数据不是陈旧的

FQuery 如何解决这个问题?

FQuery 由 flutter_hooks 提供支持。它与 swrreact-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 中查询数据,您需要通过继承 HookWidgetStatefulHookWidget(用于有状态 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 或建议。

GitHub

查看 Github