Riverpod源码分析(一)

前沿

说到 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

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。

在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集
在这里插入图片描述
二、源码解析合集

在这里插入图片描述
三、开源框架合集

在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值