Flutter数据查询管理插件query_stack的使用

Flutter数据查询管理插件query_stack的使用

Query Stack

一个简单但功能强大的状态管理库,使在您的Flutter应用中获取、缓存、同步和更新状态变得轻而易举。

受React Query启发

此包受到TanStack Query的启发。

特性

  • 集成依赖注入系统。
  • 使用简单。
  • 推崇SOLID设计模式和清洁架构。
  • 推崇DRY模式并创建领域插件(例如:一旦创建了身份验证插件,它在其他应用中就高度可重用)。

使用方法

Query Stack只有三个部分:依赖注入、流构建器和未来构建器。

依赖注入

Query Stack通过创建环境来实现依赖注入。

您可以创建一个指向开发资源的DebugEnvironment和一个用于真实使用的ProductionEnvironment,指向真实的服务器和API密钥。

您还可以为不同的口味创建继承自Environment的不同类。

通常,您会编写一个抽象的BaseEnvironment,该环境注册所有公共依赖项,并仅在具体的DebugEnvironmentProductionEnvironment中进行特殊化。

以下是实际环境的示例:

import 'package:flutter/foundation.dart';

import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:query_stack/query_stack.dart';
import 'package:query_stack_firebase_authentication/query_stack_firebase_authentication.dart';

import 'authentication/db_authentication_service.dart';
import 'companies/companies_service.dart';
import 'firebase_options.dart';

// 这个类是抽象的,因为它包含了调试和生产环境之间的共同注册
@immutable
abstract class BaseEnvironment extends Environment {
  const BaseEnvironment();

  // 这将被我的具体类覆盖,因为每个环境将指向不同的服务器URL
  String get serverBaseUrl => throw UnimplementedException();

  // 当您的应用运行时调用此方法
  //
  // 在这里,您将配置当有人请求特定类型时返回哪个类(这称为服务定位器)
  //
  // 由于您有一个`get`参数,您可以将其他服务注入您正在注册的服务中(因为一个依赖于另一个,这被称为依赖倒置原则)
  //
  // 只需注意循环依赖!
  @override
  void registerDependencies(RegisterDependenciesDelegate when, PlatformInfo platformInfo) {
    // 这个`AuthenticationService`是由`query_stack_firebase_authentication_service`提供的Query Stack插件
    //
    // 您可以创建适用于所有应用的插件并重复使用它们
    when<AuthenticationService>(
      // 由于我在数据库中持久化了我的认证用户,我可以继承`AuthenticationService`并进行特殊化
      (get) => DBAuthenticationService(
        appleRedirectUrl: platformInfo.nativePlatform.when(
          onAndroid: () => "use this url on android",
          onWeb: () => "use this url on web",
          orElse: () => null,
        ),
        appleServiceId: "apple sign in service id",
        googleClientId: platformInfo.nativePlatform.when(
          onAndroid: () => "use this google client id on android",
          oniOS: () => "use this google client id on ios",
          onWeb: () => "use this google client id on web",
          orElse: () => throw UnsupportedError("${platformInfo.nativePlatform} is not supported"),
        ),
      ),
    );

    // 这是我的应用的一个服务,使用`AuthenticationService`来获取已认证的用户ID
    //
    // 注意我请求的是`<AuthenticationService>`,但上面注册了一个`DBAuthenticationService`。这将在这种上下文中工作,因为`CompaniesService`知道如何处理`AuthenticationService`,并且继承该类不会改变其行为(这被称为开闭原则和里氏替换原则)
    when<CompaniesService>(
      (get) => CompaniesService(
        authenticationService: get<AuthenticationService>(),
        // 由于我的服务将调用一些远程API,
        // 我需要知道使用哪个服务器,而且这个基本URL在调试和生产环境之间不同
        serverBaseURL: serverBaseURL,
      ),
    );
  }

  // 注册的所有服务都继承`BaseService`,该服务具有一个`void initialize()`和一个`Future<void> initializeAsync()`。
  // 如果您的服务需要初始化,您可以覆盖这些方法(您可以覆盖其中一个或两个,`initializeAsync`将等待,因此如果您的初始化有异步方法,这就是您的选择)
  //
  // 在此方法之后,所有服务的`initialize`和`initializeAsync`将被调用(如果您没有覆盖它们,它们将是空的且没有效果)
  @override
  Future<void> initializeAsync(GetDelegate get) async {
    await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
    FirebaseAnalytics.instance.logAppOpen().ignore();
  }
}

// 由于我的开发和生产环境相同,我只需要更改特定设置(在我的情况下,指向本地或远程API服务器,取决于环境)
@immutable
class DevelopmentEnvironment extends BaseEnvironment {
  const DevelopmentEnvironment();

  @override
  String get serverBaseUrl => "http://localhost:8888";
}

@immutable
class ProductionEnvironment extends BaseEnvironment {
  const ProductionEnvironment();

  @override
  String get serverBaseUrl => "https://my-real-api-server.com";  
}

完成后,只需在主函数中使用您的环境:

Future<void> main() async {
  await Environment.use(
    kDebugMode 
      ? const DevelopmentEnvironment() 
      : const ProductionEnvironment(),
  );

  runApp(const MainApp());
}

在这里,我根据kDebugMode决定使用哪个环境,kDebugMode是一个Flutter常量,告诉我在调试或发布模式下。

您可以以任何方式选择您的环境,包括使用Flutter Flavors。

流构建器

流构建器基本上是一个带有易于使用功能的StreamBuilder

一个现实世界的例子是根据当前已认证的用户更改显示的主页小部件。例如,将MaterialApp.home属性设置为:

return AuthenticationQuery(
  loginConfiguration: BaseLoginConfiguration(
    header: const AppHeader(),
    privacyPolicyText: "Política de Privacidade",
    privacyPolicyURL: "https://meucronogramacapilar.code.art.br/Privacy.html",
    termsOfUseText: "Termos de Uso",
    termsOfUseURL: "https://meucronogramacapilar.code.art.br/Terms.html",
    signInWithAppleText: "Entrar com Apple",
    signInWithGoogleText: "Entrar com Google",
    footerTextColor: Colors.white,
    progressIndicatorColor: Colors.white,
    backgroundColor: theme.primaryColor,
    onDebug: () => Navigator.of(context).push(
      MaterialPageRoute<void>(builder: (_) => DriftDbViewer(Database.instance)),
    ),
  ),
  builder: (_, principal) => Text(
    principal == null 
      ? "Not authenticated"
      : "${principal!.displayName} authenticated"
  ),
);

这个AuthenticationQueryquery_stack_firebase_authentication包中的一个widget,会自动为您的应用提供登录页面。

它的源代码是:

class AuthenticationQuery extends StatelessWidget {
  const AuthenticationQuery({required this.loginConfiguration, required this.builder, super.key});

  final BaseLoginConfiguration loginConfiguration;
  final Widget Function(BuildContext context, Principal principal) builder;

  @override
  Widget build(BuildContext context) {
    return QueryStreamBuilder<Principal>(
      stream: AuthenticationService.current.currentPrincipalStream,
      emptyBuilder: (_) => BaseLoginPage(loginConfiguration: loginConfiguration),
      waitingBuilder: (_) => WaitingPage(header: loginConfiguration.header),
      errorBuilder: (_, __) => BaseLoginPage(loginConfiguration: loginConfiguration),
      initialData: AuthenticationService.current.currentPrincipalStream.hasValue ? AuthenticationService.current.currentPrincipalStream.value : null,
      onError: _onError,
      dataBuilder: builder,
    );
  }

  void _onError(BuildContext context, Object error) {
    showOkAlertDialog(
      context: context,
      title: "Unexpected Error",
      message: error.toString(),
    );
  }
}

魔法在于QueryStreamBuilder<Principal>

QueryStreamBuilder将监听一个流(在这种情况下,是AuthenticationServicecurrentPrincipalStream,从null(无用户认证到一个实例的Principal(代表已认证用户))。

每当流发生变化时,会调用某些构建器:

  • emptyBuilder将在流内容为空或空的Iterator(空列表、映射或集合)时被调用。
  • waitingBuilder将在流处于等待状态(首次初始化)时被调用。
  • errorBuilder将在流出现错误时被调用。
  • dataBuilder将在流有有效值时被调用。

在这个例子中,“空”意味着没有用户认证(所以我们将构建一个BaseLoginPage),而“数据”意味着已认证的用户(所以我们将在应用中构建任何想要的内容,并传递当前已认证的用户)。

换句话说:这是一个StreamBuilder,您不必自己处理空值、等待状态和异常。

未来构建器

未来构建器是特殊的FutureBuilders,自动为您处理一些巧妙的事情。

例如,假设您需要根据用户是否已配置某些设置来构建不同的页面(在我的示例中,已认证的用户必须在进入应用之前配置一些公司信息,所以我使用远程API来检查用户是否已经配置了一些公司信息,或者他现在需要设置)。

为此,我可以使用QueryFutureBuilder<bool>来询问我的服务器是否有公司的设置(我称之为第一次访问)。或者我可以将其包装在一个专门为此目的的widget中:

// 通过使用自定义widget而不是通用的`QueryFutureBuilder<T>`,
// 我不需要手动处理查询键,我可以通过`FirstAccessQuery.of(context)`访问此数据的类型化的特殊版本
@immutable
class FirstAccessQuery extends StatelessWidget {
  const FirstAccessQuery({required this.builder, super.key});

  // 此构建器将使用我的查询响应调用
  final Widget Function(BuildContext context, bool firstAccessComplete) builder;

  // 每个查询必须有一个键,这样以后可以访问它们以刷新(例如:在设置第一个公司后,我可以手动触发此widget的重新加载)
  static final String queryKey = "${FirstAccessQuery}";

  // 我只是包装一个`QueryFutureBuilder<Response>`,以避免在未来重复自己并将所有相关内容保持在一处
  @override
  Widget build(BuildContext context) {
    return QueryFutureBuilder<bool>(
      queryKey: queryKey,
      // 这是调用我的本地数据库或远程API的方法,并返回`true`如果此用户有任何公司注册或`false`如果不注册,我现在需要这样做
      future: () => CompaniesService.current.getFirstAccessIsComplete(),
      dataBuilder: builder,
    );
  }

  // 一些通用的`maybeOf`和`of`的`InheritedModels`(这些是`InheritedWidget`的特殊案例)
  static Query<bool>? maybeOf(BuildContext context) {
    return InheritedModel.inheritFrom<Query<bool>>(context, aspect: queryKey);
  }

  static Query<bool> of(BuildContext context) {
    final result = maybeOf(context);

    assert(result != null, "Unable to find an instance of FirstAccessQuery in the widget tree");

    return result!;
  }
}

现在,我可以基于用户的先前设置决定要做什么:

...
return FirstAccessQuery(
  builder: (context, hasFirstAccess) {
    if(hasFirstAccess) {
      return const HomePage();
    }

    return const SetupCompanyPage();
  },
);
...

SetupCompanyPage()上,我可以通过调用来触发刷新:

...
  final query = FirstAccessQuery.of(context);

  await query.refreshFn();
...

这将使FirstAccessQuery() widget重新执行future函数,再次调用我的API。

此外,QueryFutureBuilder<T>也可以在以下情况下重新执行其功能:

  • 导航到包含QueryFutureBuilder<T>的页面时,如果它是陈旧的(陈旧是在上次成功响应后经过的时间段,在此期间不会自动重新获取。如果您设置为5分钟,则所有自动重新获取将只在上次成功响应后的5分钟后才击中您的API)
  • 您配置了一个自动重新获取计时器使用refetchInterval
  • 您的应用从前台切换到后台,然后再回到前台

此外,QueryFutureBuilder<T>将尝试在maxAttempts属性配置的次数内重新执行未来的操作(配置的次数)后再抛出错误。

QueryFutureBuilder<T>仅在keepPreviousDatefalse时才会设置等待状态(构建一个waitingBuilder,这样您可以显示旋转进度条等,以便在获取数据时显示),否则,直到获取完成时不会改变任何内容,然后dataBuilder将使用新数据执行,您的屏幕将刷新。

对于简单的值(如默认的Flutter计数器示例),您可以定义一个具有内部_count变量的可变服务,该变量由函数处理,这些函数触发一个流:

class CounterService extends BaseService {
  CounterService(this.initialValue);

  static CounterService get current => Environment.get<CounterService>();

  final int? initialValue;

  // 使用`rxDart`包的`BehaviorSubject`:
  final _streamController = BehaviorSubject<int>(initialValue ?? 0); 

  Stream<int> get counterStream => _streamController.stream;

  void addToCounter() {
    final currentValue = _streamController.value;

    _streamController.add(currentValue + 1);
  }
}

然后使用QueryStreamBuilder

...
return QueryStreamBuilder<int>(
      stream: CounterService.current.counterStream,
      initialData: 1,
      dataBuilder: (context, value) => Text("Counter: ${value}"),
    )
...

更多关于Flutter数据查询管理插件query_stack的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter数据查询管理插件query_stack的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


query_stack 是一个用于 Flutter 应用的状态管理和数据查询的插件。它旨在简化数据获取、缓存和状态管理的流程,特别适合处理需要频繁查询和管理数据的应用场景。

1. 安装 query_stack

首先,你需要在 pubspec.yaml 文件中添加 query_stack 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  query_stack: ^0.2.0  # 请根据最新版本号进行替换

然后运行 flutter pub get 来安装依赖。

2. 基本用法

query_stack 提供了一个 QueryStack 类,用于管理数据查询和状态。你可以通过它来定义查询、缓存和更新数据。

2.1 创建一个 QueryStack 实例

import 'package:query_stack/query_stack.dart';

final queryStack = QueryStack();

2.2 定义一个查询

你可以使用 queryStack.query 方法来定义查询。该方法接受一个 QueryKey 和一个 QueryFn 函数。

final userQuery = queryStack.query(
  'user',  // QueryKey
  () async {
    // 这里可以是一个网络请求或其他异步操作
    return await fetchUserData();
  },
);

2.3 获取查询结果

你可以通过 userQuery.watch 方法来监听查询结果的变化。

class UserProfile extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return userQuery.watch(
      builder: (context, data, error) {
        if (error != null) {
          return Center(child: Text('Error: $error'));
        }
        if (data == null) {
          return Center(child: CircularProgressIndicator());
        }
        return Text('User: ${data.name}');
      },
    );
  }
}

2.4 手动刷新查询

你可以通过调用 userQuery.refresh 来手动刷新查询。

void refreshUserData() {
  userQuery.refresh();
}

3. 高级用法

3.1 缓存策略

query_stack 提供了缓存管理功能。你可以通过 cacheDuration 参数来设置缓存的有效期。

final userQuery = queryStack.query(
  'user',
  () async => await fetchUserData(),
  cacheDuration: Duration(minutes: 5),  // 缓存 5 分钟
);

3.2 依赖查询

你可以使用 queryStack.dependentQuery 来定义依赖于其他查询的查询。

final userPostsQuery = queryStack.dependentQuery(
  'userPosts',
  dependsOn: [userQuery],
  () async {
    final user = userQuery.data;
    return await fetchUserPosts(user.id);
  },
);

3.3 查询状态管理

query_stack 提供了 QueryState 来管理查询的状态,如加载中、成功、失败等。

final userQuery = queryStack.query(
  'user',
  () async => await fetchUserData(),
);

class UserProfile extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return userQuery.watch(
      builder: (context, data, error) {
        if (userQuery.isLoading) {
          return Center(child: CircularProgressIndicator());
        }
        if (error != null) {
          return Center(child: Text('Error: $error'));
        }
        return Text('User: ${data.name}');
      },
    );
  }
}
回到顶部