美文网首页
Riverpod源码分析(一)

Riverpod源码分析(一)

作者: _Jun | 来源:发表于2022-12-29 14:13 被阅读0次

    前沿

    说到 Flutter 的状态管理框架,我们耳熟能详的有 ProviderRiverpodBloc 以及大名鼎鼎的 Getx 等,可谓是数不胜数。但要说其中哪一个更优秀,那可能 一千个人有一千个哈姆雷特。今天我们就来介绍其中的 Riverpod,它也是 Provider 的开发团队 dash-overflow.net 针对 Provider 存在的缺点,重新开发设计的一个状态管理框架,由此可见其价值。

    一、什么是状态管理?

    在 Flutter 中,状态管理是指在应用中管理修改数据的过程。简单来说,就是更新页面中的数据。

    Flutter 的状态管理方式有很多种,我们可以根据自己的需要选择合适的方式来管理应用的状态。一些常见的状态管理方式包括:

    • 局部状态:局部状态是指只影响单个组件的状态。可以使用 Flutter 的 setState 方法来更新局部状态。
    • 全局状态:全局状态是指影响整个应用的状态。可以使用 Flutter 的 Provider 插件来管理全局状态。
    • 共享状态:共享状态是指影响多个组件的状态。可以使用 Flutter 的 InheritedWidget 来共享状态。

    二、认识Riverpod

    1. Rivepod插件介绍

    Riverpod(即 Provider 的变位词)适用于 Flutter / Dart 的响应式缓存框架。它可以自动获取、缓存、组合和重新计算网络请求,同时能还为你处理错误。

    Riverpod 通过提供一种新的、独特的方式来编写业务逻辑,灵感来自 Flutter 的 Widget。Riverpod 在许多方面都与 Widget 类似,但在状态方面不同。

    使用这种新方法,那些复杂的特性大部分都是默认完成,我们只需要关注 UI 即可。

    例如下面的代码片段是使用 Riverpod 实现的 Pub.dev 应用的简化代码:

    // 从pub.dev中获取package的列表
    @riverpod
    Future<List<Package>> fetchPackages(
      FetchPackagesRef ref, {
      required int page,
      String search = '',
    }) async {
      final dio = Dio();
      // 取一个API。这里我们使用的是package:dio,但我们也可以使用其他任何东西
      final response = await dio.get(
        'https://pub.dartlang.org/api/search?page=$page&q=${Uri.encodeQueryComponent(search)}',
      );
    
      // 将JSON响应解码为Dart类
      final json = response.data as List;
      return json.map(Package.fromJson).toList();
    }
    

    这段代码是“输入搜索”所需的全部业务逻辑,通过 @riverpod 注解的方式生成 Riverpod 代码。

    2. 开始使用

    1. pubspec.yaml 引入 package
    name: my_app_name
    environment:
      sdk: ">=2.17.0 <3.0.0"
      flutter: ">=3.0.0"
    
    dependencies:
      flutter:
        sdk: flutter
      flutter_riverpod: ^2.1.1
      riverpod_annotation: ^1.0.6
    
    dev_dependencies:
      build_runner:
      riverpod_generator: ^1.0.6
    
    1. 使用例子:计数器
    void main() {
      runApp(
        // 添加ProviderScope可以使Riverpod适用于整个项目
        const ProviderScope(child: MyApp()),
      );
    }
    
    /// provider是全局声明的,并指定如何创建一个状态
    final counterProvider = StateProvider((ref) => 0);
    
    class Home extends ConsumerWidget {
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        return Scaffold(
          body: Center(
            // Consumer是一个允许读取providers的widget
            child: Consumer(
              builder: (context, ref, _) {
                final count = ref.watch(counterProvider);
                return Text('$count');
              },
            ),
          ),
          floatingActionButton: FloatingActionButton(
            // read方法用于更新provider的值
            onPressed: () => ref.read(counterProvider.notifier).state++,
            child: const Icon(Icons.add),
          ),
        );
      }
    }
    

    3. Riverpod和provider之间的关系

    Riverpod 是在寻找解决 Provider 所面临的各种技术限制的方案时诞生的。最初,Riverpod 被认为是 Provider 中解决这一问题的一个主要版本。但开发团队最终还是决定不这么做,因为这将是一个相当大的突破性变化,而 Provider 是最常用的 Flutter 包之一。

    尽管如此,Riverpod 和 Provider 在概念上还是相当相似的。两个包都扮演类似的角色。两者都试图:

    • 缓存并处理一些有状态的对象
    • 提供一种在测试期间模拟这些对象的方法
    • 为 Widgets 提供了一种简单的方式来监听这些对象。

    Riverpod 修复了 Provider 的各种基本问题,例如但不限于:

    • 显著简化了 “Providers” 的组合。Riverpod 没有使用繁琐且容易出错的 ProxyProvider 工具,而是使用了简单且强大的实用工具,例如 ref.watchref.listen
    • 允许多个 “provider” 公开同一类型的值。这消除了在使用 int or String 等类型值时定义自定义类的需要。
    • 无需在测试中重新定义 providers。在 Riverpod 中,provider 默认情况下可以使用内部测试。
    • 通过提供处理对象的替代方法( autoDispose )来减少对“作用域”处理对象的过度依赖。尽管功能强大,但确定提供者的作用域是相当高级的,而且很难做到正确。

    4. 缺点

    Riverpod 唯一的缺点是它需要改变 Widget类型 才能工作:

    • 在 Riverpod 中,你应该继承 ConsumerWidget ,而不是继承 StatelessWidget
    • 在 Riverpod 中,你应该继承 ConsumerStatefulWidget ,而不是继承 StatefulWidget

    三、Providers

    1. Provider

    provier 是所有 providers 中的最基础的类。provider 通用用于:

    • 缓存计算
    • 向其他 providers 公开一个值(例如Respository / HttpClient)
    • 为测试或 widget 提供覆盖值的方法
    • 在非必要情况下减少 providers / Widget 的 rebuild 次数

    使用示例:

    假设我们的应用中有一个 StateNotifierProvider 来操作待办事项列表:

    @riverpod
    class Todos extends _$Todos {
      @override
      List<Todo> build() {
        return [];
      }
    
      void addTodo(Todo todo) {
        state = [...state, todo];
      }
    }
    
    

    我们可以使用 Providertodos 的筛选列表,仅显示已完成的 todos:

    @riverpod
    List<Todo> completedTodos(CompletedTodosRef ref) {
      final todos = ref.watch(todosProvider);
    
      // 我们只返回完成的todo
      return todos.where((todo) => todo.isCompleted).toList();
    }
    

    我们的 UI 现在可以通过监听来显示已完成的待办事项列表 completedTodosProvider

    Consumer(builder: (context, ref, child) {
      final completedTodos = ref.watch(completedTodosProvider);
      // TODO 使用ListView/GridView/...显示已完成列表
    });
    

    2. StateNotifierProvider

    StateNotifierProvider 是一个用于监听和公开 StateNotifier 的 providers(来自 Riverpod 重新导出的包 state_notifierStateNotifierProviderStateNotifier 一起是 Riverpod 推荐的用于管理状态的解决方案。 它通常用于:

    • 公开一个不可变的状态,它可以在对自定义事件做出反应后随时间改变。
    • 将修改某些状态的逻辑(也称为“业务逻辑”)集中在一个地方,随着时间的推移提高可维护性。

    用法示例:

    我们可以 StateNotifierProvider 用来实现一个待办事项列表,例如 addTodo让 UI 修改与用户交互的待办事项列表:

    // StateNotifier的状态应该是不可变的。
    // 也可以使用像frozen这样的包来帮助实现。
    @immutable
    class Todo {
      const Todo({required this.id, required this.description, required this.completed});
    
      // 类中的所有属性都应该是final。
      final String id;
      final String description;
      final bool completed;
    
      // 由于Todo是不可变的,我们实现了一个方法,允许clone
      // 内容略有不同的Todo。
      Todo copyWith({String? id, String? description, bool? completed}) {
        return Todo(
          id: id ?? this.id,
          description: description ?? this.description,
          completed: completed ?? this.completed,
        );
      }
    }
    
    // 传递给StateNotifierProvider的StateNotifier类
    // 这个类不应该在其"state"属性之外暴露状态,这意味着
    // 没有公共getter属性
    // 这个类的public方法将允许UI修改状态
    class TodosNotifier extends StateNotifier<List<Todo>> {
      // 将待办事项列表初始化为一个空列表
      TodosNotifier(): super([]);
    
      // 允许UI添加待办事项。
      void addTodo(Todo todo) {
        // 因为我们的状态是不可变的,所以不允许执行' state.add(todo) '
        // 相反,我们应该创建一个新的待办事项列表,其中包含以前的项目和新的项目
        // 在这里使用Dart的展开运算符是有帮助的
        state = [...state, todo];
        // 调用"state =" 不需要调用“notifyListeners”或类似的方法。
        // 会在必要时自动rebuild UI
      }
    
      // 允许删除待办事项
      void removeTodo(String todoId) {
        // 同样,我们的状态是不可变的。所以我们创建了一个新的列表,而不是改变现有的列表。
        state = [
          for (final todo in state)
            if (todo.id != todoId) todo,
        ];
      }
    
      // 把待办事项标记为已完成
      void toggle(String todoId) {
        state = [
          for (final todo in state)
            // 只标记已完成的匹配todo
            if (todo.id == todoId)
              // 同样,因为我们的状态是不可变的,所以需要创建todo的副本                  
                // 使用之前实现的copyWith方法来帮助实现
              todo.copyWith(completed: !todo.completed)
            else
              // other todos are not modified
              todo,
        ];
      }
    }
    
    // 最后,我们使用StateNotifierProvider来允许UI与我们的TodosNotifier类交互
    final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
      return TodosNotifier();
    });
    

    现在我们已经定义了一个 StateNotifierProvider,我们可以使用它与 UI 中的待办事项列表进行交互:

    class TodoListView extends ConsumerWidget {
      const TodoListView({Key? key}): super(key: key);
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        // 当待办事项列表更改时重新构建Widget
        List<Todo> todos = ref.watch(todosProvider);
    
        // 在一个可滚动的列表视图中呈现待办事项
        return ListView(
          children: [
            for (final todo in todos)
              CheckboxListTile(
                value: todo.completed,
                // 当点击待办事项时,更改其完成状态
                onChanged: (value) => ref.read(todosProvider.notifier).toggle(todo.id),
                title: Text(todo.description),
              ),
          ],
        );
      }
    }
    

    3. FutureProvider

    FutureProvider 等价于 Provier 但用于异步代码

    FutureProvider 通常用于:

    • 执行和缓存异步操作(例如网络请求)
    • 很好地处理异步操作的错误/加载状态
    • 将多个异步值组合成另一个值

    使用示例:读取配置

    /// 定义FutureProvider
    @riverpod
    Future<Configuration> fetchConfigration(FetchConfigrationRef ref) async {
      final content = json.decode(
        await rootBundle.loadString('assets/configurations.json'),
      ) as Map<String, Object?>;
    
      return Configuration.fromJson(content);
    }
    
    /// 更新UI
    Widget build(BuildContext context, WidgetRef ref) {
      final config = ref.watch(fetchConfigrationProvider);
    
      return config.when(
        loading: () => const CircularProgressIndicator(),
        error: (err, stack) => Text('Error: $err'),
        data: (config) {
          return Text(config.host);
        },
      );
    }
    

    4. StreamProvider

    StreamProvider类似于 FutureProvider 但用于 Stream 而不是 Future

    StreamProvider通常用于:

    • 听 Firebase 或网络套接字
    • 每隔几秒重建另一个供应商

    使用 StreamProviderStreamBuilder 有很多好处:

    • 它允许其他 providers 使用 ref.watch 收听流
    • 得益于 AsyncValue,它确保正确处理加载和错误情况
    • 它消除了区分广播流和普通流的需要
    • 它缓存流发出的最新值,确保如果在发出事件后添加侦听器,侦听器仍然可以立即访问最新的事件
    • 它允许通过覆盖 StreamProvider

    5. StateProvider

    StateProvider 是一个提供者,它公开了一种修改其状态的方法。它是 StateNotifierProvider 的简化版,旨在避免为非常简单的用例编写 StateNotifier

    StateProvider 存在的主要目的是允许 通过用户界面修改简单变量。 StateProvider 的使用通常是以下情景之一:

    • 枚举,例如过滤器类型
    • 字符串,通常是文本字段的原始内容
    • 布尔值,用于复选框
    • 数字,用于分页或年龄表单字段

    如果出现以下情况,则不应使用 StateProvider

    • 你的状态需要验证逻辑
    • 您的状态是一个复杂的对象(例如自定义类、列表/地图……)
    • 修改状态的逻辑比简单的 count++.

    使用示例:使用下拉菜单更改过滤器类型

    为了简单起见,我们将获得的产品列表将直接构建在应用中,如下所示:

    class Product {
      Product({required this.name, required this.price});
    
      final String name;
      final double price;
    }
    
    final _products = [
      Product(name: 'iPhone', price: 999),
      Product(name: 'cookie', price: 2),
      Product(name: 'ps5', price: 500),
    ];
    
    final productsProvider = Provider<List<Product>>((ref) {
      return _products;
    });
    

    然后,用户界面可以通过执行以下操作来显示产品列表:

    Widget build(BuildContext context, WidgetRef ref) {
      final products = ref.watch(productsProvider);
      return Scaffold(
        body: ListView.builder(
          itemCount: products.length,
          itemBuilder: (context, index) {
            final product = products[index];
            return ListTile(
              title: Text(product.name),
              subtitle: Text('${product.price} \$'),
            );
          },
        ),
      );
    }
    

    我们可以添加一个下拉列表,这将允许按价格或按名称过滤我们的产品:

    // 表示筛选器类型的枚举
    enum ProductSortType {
      name,
      price,
    }
    
    Widget build(BuildContext context, WidgetRef ref) {
      final products = ref.watch(productsProvider);
      return Scaffold(
        appBar: AppBar(
          title: const Text('Products'),
          actions: [
            DropdownButton<ProductSortType>(
              value: ProductSortType.price,
              onChanged: (value) {},
              items: const [
                DropdownMenuItem(
                  value: ProductSortType.name,
                  child: Icon(Icons.sort_by_alpha),
                ),
                DropdownMenuItem(
                  value: ProductSortType.price,
                  child: Icon(Icons.sort),
                ),
              ],
            ),
          ],
        ),
        body: ListView.builder(
          // ... 
        ),
      );
    }
    

    现在我们有了一个下拉列表,让我们创建一个下拉列表 StateProvider 并将其状态与我们的 providers 同步。

    // 创建StateProvider
    final productSortTypeProvider = StateProvider<ProductSortType>(
      // 我们返回默认的排序类型,这里是name。
      (ref) => ProductSortType.name,
    );
    
    // 通过以下方式将此提供程序与我们的下拉列表连接起来
    DropdownButton<ProductSortType>(
      // 当排序类型改变时,这将重新构建下拉列表以更新显示的图标。
      value: ref.watch(productSortTypeProvider),
      // 当用户与下拉菜单交互时,我们更新提供者状态。
      onChanged: (value) =>
          ref.read(productSortTypeProvider.notifier).state = value!,
      items: [
        // ...
      ],
    ),
    
    // 更新productsProvider以对产品列表进行排序
    final productsProvider = Provider<List<Product>>((ref) {
      final sortType = ref.watch(productSortTypeProvider);
      switch (sortType) {
        case ProductSortType.name:
          return _products.sorted((a, b) => a.name.compareTo(b.name));
        case ProductSortType.price:
          return _products.sorted((a, b) => a.price.compareTo(b.price));
      }
    });
    

    就这样!此更改足以让用户界面在排序类型更改时自动重新呈现产品列表。

    6. ChangeNotifierProvider

    ChangeNotifierProvider(仅限 flutter_riverpod / hooks_riverpod)是一个 provider,用于从 Flutter 本身监听和公开 ChangeNotifier

    Riverpod ChangeNotifierProvider 不鼓励使用,主要用于:

    • package:provider从使用其时的轻松过渡 ChangeNotifierProvider
    • 支持可变状态,即使不可变状态是首选

    使用可变状态而不是不可变状态有时会更有效。缺点是,它可能更难维护并且可能会破坏各种功能。 例如,provider.select 如果你的状态是可变的,那么使用来优化你的 Widget 的重建可能不起作用,因为 select 会认为该值没有改变。 因此,使用不可变数据结构有时会更快。因此,针对您的用例制定基准非常重要,以确保您通过使用 ChangeNotifierProvider

    使用示例:

    ChangeNotifierProvider 用来实现一个待办事项列表。这样做将允许我们公开方法,例如 addTodo 让 UI 修改用户交互的待办事项列表:

    class Todo {
      Todo({
        required this.id,
        required this.description,
        required this.completed,
      });
    
      String id;
      String description;
      bool completed;
    }
    
    class TodosNotifier extends ChangeNotifier {
      final todos = <Todo>[];
    
      // 允许UI添加待办事项
      void addTodo(Todo todo) {
        todos.add(todo);
        notifyListeners();
      }
    
      // 允许删除待办事项
      void removeTodo(String todoId) {
        todos.remove(todos.firstWhere((element) => element.id == todoId));
        notifyListeners();
      }
    
      // 把待办事项标记为已完成
      void toggle(String todoId) {
        for (final todo in todos) {
          if (todo.id == todoId) {
            todo.completed = !todo.completed;
            notifyListeners();
          }
        }
      }
    }
    
    // 最后,使用StateNotifierProvider来允许UI与我们的TodosNotifier类交互。
    final todosProvider = ChangeNotifierProvider<TodosNotifier>((ref) {
      return TodosNotifier();
    });
    

    使用它与 UI 中的待办事项列表进行交互:

    class TodoListView extends ConsumerWidget {
      const TodoListView({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        // 当待办事项列表更改时重新构建widget
        List<Todo> todos = ref.watch(todosProvider).todos;
    
        // 在一个可滚动的列表视图中呈现待办事项
        return ListView(
          children: [
            for (final todo in todos)
              CheckboxListTile(
                value: todo.completed,
                // 当点击待办事项时,更改其完成状态
                onChanged: (value) =>
                    ref.read(todosProvider.notifier).toggle(todo.id),
                title: Text(todo.description),
              ),
          ],
        );
      }
    }
    

    四、关于Riverpod的代码生成

    代码生成就是使用工具为我们生成代码的思想。

    在 Dart 中,它的缺点是需要额外的步骤来“编译” 应用。虽然这个问题可能会在不久的将来得到解决,因为 Dart 团队正在研究这个问题的潜在解决方案。

    在 Riverpod 的上下文中,代码生成是关于稍微改变定义 “providers” 的语法。例如:

    final fetchUserProvider = FutureProvider.autoDispose.family<User, String>((ref, userId) async {
      final json = await http.get('api/user/$userId');
      return User.fromJson(json);
    });
    

    使用代码生成,我们会写:

    @riverpod
    Future<User> fetchUser(FetchUserRef ref, {required int userId}) async {
      final json = await http.get('api/user/$userId');
      return User.fromJson(json);
    }
    

    使用 Riverpod 时,代码生成是完全可选的。完全可以在没有的情况下使用 Riverpod。 同时,Riverpod 拥抱代码生成并推荐使用它。

    五、关于Riverpod的钩子

    Hooks” 是独立于 Riverpod 的通用实用 package: flutter_hooks。 尽管 flutter_hooks 是一个完全独立的包并且与 Riverpod 没有任何关系(至少没有直接关系),但通常将 Riverpodflutter_hooks 配对在一起。毕竟,Riverpod 和 flutter_hooks 是由同一个团队维护的。

    钩子是完全可选的。你可以不必使用钩子,尤其是在你开始使用 Flutter 时。它们是强大的工具,但不是很像 Flutter。因此,首先从普通的 Flutter / Riverpod 开始可能是有意义的,一旦你有了更多的经验,再回到钩子上。

    什么是钩子?

    钩子是在小部件内部使用的函数。它们被设计为 StatefulWidget 的替代品,以使逻辑更具可重用性和可组合性。

    如果 Riverpod 的提供者用于“全局” 应用,则挂钩用于本地 widget。钩子通常用于处理有状态的 UI 对象,例如 TextEditingController、 AnimationController。 它们还可以作为“构建器”模式的替代品,用不涉及“嵌套”的替代品替代,诸如 FutureBuilder / TweenAnimatedBuilder 之类的 Widget 显着提高可读性。

    通常,钩子有助于:

    • 形式
    • 动画
    • 响应用户事件
    • ...

    使用示例:

    class FadeIn extends HookWidget {
      const FadeIn({Key? key, required this.child}) : super(key: key);
    
      final Widget child;
    
      @override
      Widget build(BuildContext context) {
        // 创建一个AnimationController卸载Widget时,控制器将自动被处置。
        final animationController = useAnimationController(
          duration: const Duration(seconds: 2),
        );
    
        // useEffect相当于initState + didUpdateWidget + dispose
        // 传递给useEffect的回调在钩子第一次出现时执行
        // 被调用时执行,然后在作为第二个参数传递的列表发生变化时执行
        // 因为我们在这里传递了一个空的const列表,这严格地等同于' initState '。
        useEffect(() {
          // 在widget第一次呈现时启动动画。
          animationController.forward();
          // 我们可以选择在这里返回一些“dispose”逻辑
          return null;
        }, const []);
    
        // 告诉Flutter在动画更新时重新构建此Widget。
        // 这相当于AnimatedBuilder
        useAnimation(animationController);
    
        return Opacity(
          opacity: animationController.value,
          child: child,
        );
      }
    }
    

    总结

    今天先介绍了状态管理的一些基础概念,然后介绍了 Riverpod 和 Provider 之间的联系和差异,最后着重介绍了 Riverpod 的一些用法和 6Provider,以及 Riverpod 的代码生成钩子。下一篇我们将开始介绍Riverpod的特性和实现原理。

    作者:Fitem
    链接:https://juejin.cn/post/7182225820767748153

    相关文章

      网友评论

          本文标题:Riverpod源码分析(一)

          本文链接:https://www.haomeiwen.com/subject/ktetcdtx.html