[译][官方文档] Flutter/Dart 状态管理库 RiverPod (四)- Provider 开发者使用 RiverPod - 对比

原文链接:Provider vs Riverpod | Riverpod

pub:riverpod | Dart Package (flutter-io.cn)

译时版本: 2.4.5


之前翻译过 RiverPod 的官方文档,现在随着版本更新,官方文档又多了很多新内容,所以再补充翻译一下。

之前翻译过的内容,现在官方文档有中文了。
Flutter状态管理库Riverpod官方文档翻译汇总 - 掘金 (juejin.cn)


Provider vs Riverpod

该文章简要描述了 Provider 和 Riverpod 的差异点和相似点。

定义 provider

两个包的主要的差异是定义 "providers" 的方式。

使用 Provider ,provider 都是组件,所以会放置在组件树内部,经典用法是在 MultiProvider 里:

dart 复制代码
class Counter extends ChangeNotifier {
 ...
}

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<Counter>(create: (context) => Counter()),
      ],
      child: MyApp(),
    )
  );
}

使用 Riverpod ,provider 不是 组件。它们反而是普通的 Dart 对象。

类似地,provider 是在组件树外部定义的,并声明为全局的 final 变量。

而且,要正常使用 Riverpod ,需要在整个应用上上添加 ProviderScope 组件。 这样,将 Provider 示例改写为使用 Riverpod 会是下面这个样子:

dart 复制代码
// Provider 现在是顶级的变量
final counterProvider = ChangeNotifierProvider<Counter>((ref) => Counter());

void main() {
  runApp(
    // 该组件使 Riverpod 在整个工程可用
    ProviderScope(
      child: MyApp(),
    ),
  );
}

能注意到 provider 只需要简单修改几行。

信息:

由于 Riverpod 的 provider 是普通的 Dart 对象,所以不是 Flutter 开发,也能使用 Riverpod 。

例如,Riverpod 可用于编写命令行应用。

读取 provider : BuildContext

使用 Provider ,读取 provider 的一个方式是使用组件的 BuildContext

例如,如果 provider 定义如下:

dart 复制代码
Provider<Model>(...);

Provider 读取的代码会如下:

dart 复制代码
class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Model model = context.watch<Model>();

  }
}

用 Riverpod 读取的话就会是:

dart 复制代码
final modelProvider = Provider<Model>(...);

class Example extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    Model model = ref.watch(modelProvider);

  }
}

注意改写方式:

  • Riverpod 的代码片段继承了 ConsumerWidget 而不是 StatelessWidget 。不同的组件类型是向 build 函数添加了一个变量: WidgetRef
  • 代替 BuildContext.watch ,在 Riverpod 中是用 WidgetRef.watchWidgetRef 可从 ConsumerWidget 获取。
  • Riverpod 不依赖泛型。它依赖用 provider 定义创建的变量。

也要注意二者的用语很相似。Provider 和 Riverpod 都使用关键字 "watch" 描述 "当值改变时该组件需要重绘"。

信息:

Riverpod 使用 Provider 相似的术语用于读取 provider 。

  • BuildContext.watch -> WidgetRef.watch -> BuildContext.read -> WidgetRef.read
  • BuildContext.select -> WidgetRef.watch(myProvider.select)

context.watch vs context.read 的规则对于 RiverPod 也是适用的: 在 build 方法中,使用 watch。在点击处理器或其它事件中,使用 read 。 需要过滤值并重绘时,使用 select

读取 provider :Consumer(消费者)

Provider 带有一个名为 ConsumerConsumer2 之类的变量)的可选组件用于读取 provider 。

通过允许更多颗粒级的组件树重绘,Consumer 有助于性能优化。

  • 只在状态改变时更新关联的组件:

照这样,如果 provider 定义如下:

dart 复制代码
Provider<Model>(...);

Provider 可如下使用 Consumer 读取这个 provider :

dart 复制代码
Consumer<Model>(
  builder: (BuildContext context, Model model, Widget? child) {

  }
)

Riverpod 也有相同的准则。 Riverpod 也有一个名为 Consumer 的组件用于完全相同的处理。

如果定义 provider 如下:

dart 复制代码
final modelProvider = Provider<Model>(...);

然后使用 Consumer 可以如下这样:

dart 复制代码
Consumer(
  builder: (BuildContext context, WidgetRef ref, Widget? child) {
    Model model = ref.watch(modelProvider);

  }
)

注意 Consumer 是如何给我们一个 WidgetRef 对象。 这和在前面看到的 ConsumerWidget 关联的部分是相同的对象。

Riverpod 中没有 ConsumerN

注意 pkg:Provider 中的 Consumer2Consumer3 等是如何在 RiverPode 中不需要或者没有的。

使用 Riverpod ,如果需要从多个 provider 中读取值,只需写多个 ref.watch 语句,如下:

dart 复制代码
Consumer(
  builder: (context, ref, child) {
    Model1 model = ref.watch(model1Provider);
    Model2 model = ref.watch(model2Provider);
    Model3 model = ref.watch(model3Provider);
    // ...
  }
)

相对于 pkg:Provider 的 ConsumerN API,上面的方案更轻量易懂。

绑定 providers :无状态对象的 ProxyProvider

使用 Provider ,绑定 provider 的官方方式是使用 ProxyProvider 组件(或者 ProxyProvider2 之类的变量)。

例如,我们可能定义如下:

dart 复制代码
class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// ...

ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),

这样有两个选项。绑定 UserIdNotifier 创建新的"无状态" provider(典型的是重写 == 的不可变值)。如下:

dart 复制代码
ProxyProvider<UserIdNotifier, String>(
  update: (context, userIdNotifier, _) {
    return 'The user ID of the the user is ${userIdNotifier.userId}';
  }
)

该 provider 会在 UserIdNotifier.userId 发生改变时自动返回新的 String

可以在 RiverPod 中用相同的方式,只是语法有所不同。

首先,在 RiverPod 中,UserIdNotifier 的定义会如下:

dart 复制代码
class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// ...

final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
  (ref) => UserIdNotifier(),
);

然后生成基于 userId 的 String ,可如下:

dart 复制代码
final labelProvider = Provider<String>((ref) {
  UserIdNotifier userIdNotifier = ref.watch(userIdNotifierProvider);
  return 'The user ID of the the user is ${userIdNotifier.userId}';
});

注意 ref.watch(userIdNotifierProvider) 代码行的作用。

该代码行告诉 Riverpod 获取 userIdNotifierProvider 的内容,并且无论何时值发生改变,labelProvider 都会重新计算。这样,无论何时 userId 发生改变时,labelProvider 生成的 String 都会自动更新。

ref.watch 应该似曾相识。 该模式在前面解释 如何读取组件内部的 provider 时说明过。 实际上 provider 现在也能用使用组件时相同的方式监听其它的 provider 。

绑定 provider :有状态对象的 ProxyProvider

绑定 provider 时,另外一个场景是暴露有状态的对象,如 ChangeNotifier 实例。

对于这种情况,可以使用 ChangeNotifierProxyProvider (或 ChangeNotifierProxyProvider2 之类的变量)

例如,可能定义如下:

dart 复制代码
class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// ...

ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),

然后定义一个新的基于 UserIdNotifier.userIdChangeNotifier 。例如可如下:

dart 复制代码
class UserNotifier extends ChangeNotifier {
  String? _userId;

  void setUserId(String? userId) {
    if (userId != _userId) {
      print('The user ID changed from $_userId to $userId');
      _userId = userId;
    }
  }
}

// ...

ChangeNotifierProxyProvider<UserIdNotifier, UserNotifier>(
  create: (context) => UserNotifier(),
  update: (context, userIdNotifier, userNotifier) {
    return userNotifier!
      ..setUserId(userIdNotifier.userId);
  },
);

新的 provider 创建了 UserNotifier (永远不会重新构造)的单例,并在 user ID 发生改变时打印字符串。

在 provider 中实现相同处理的方式有所不同。 首先,在 RiverPod 中,UserIdNotifier 的定义会是:

dart 复制代码
class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// ...

final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
  (ref) => UserIdNotifier(),
),

然后,前面 ChangeNotifierProxyProvider 需要改写为如下:

dart 复制代码
class UserNotifier extends ChangeNotifier {
  String? _userId;

  void setUserId(String? userId) {
    if (userId != _userId) {
      print('The user ID changed from $_userId to $userId');
      _userId = userId;
    }
  }
}

// ...

final userNotifierProvider = ChangeNotifierProvider<UserNotifier>((ref) {
  final userNotifier = UserNotifier();
  ref.listen<UserIdNotifier>(
    userIdNotifierProvider,
    (previous, next) {
      if (previous?.userId != next.userId) {
        userNotifier.setUserId(next.userId);
      }
    },
  );

  return userNotifier;
});

该代码片段的核心是 ref.listen 代码行。

ref.listen 函数是允许监听 provider 的工具函数,当 provider 发生改变时,执行函数。

该函数的 previousnext 参数对应着 provider 改变前的最终值和改变后的新值。

作用域 Provider vs .family + .autoDispose

在 pkg:Provider 中,作用域用于两种场合:

  • 离开页面时清除状态
  • 每个页面有各自的自定义状态

使用作用域清除状态不太完美。

问题是作用域在大型应用上并不能很好运用。

例如,状态通常会在某个页面创建,但是可能会在导航跳转到另一个页面后清除。

这样无法为不同的页面操作多个活跃的缓存。

类似地,如果状态需要和组件树的其它部分共享,那"为每个页面自定义状态"的方式很快就会难以驾驭, 例如需要操作模态窗口或者多个步骤的窗体。

Riverpod 采用了不同的方式:首先,不鼓励使用作用域的 provider ;

第二, .family.autoDispose 完全可以代替(作用域)。

在 RiverPod 中,标记为 .autoDispose 的 provider 不再使用时会自动清除它们的状态。

当最后移除 provider 的组件不再装载时,RiverPod 会检测到并清除该 provider。

可以尝试在一个 provider 中使用两个生命周期方法来测试该行为:

dart 复制代码
ref.onCancel((){
  print("No one listens to me anymore!");
});
ref.onDispose((){
  print("If I've been defined as `.autoDispose`, I just got disposed!");
});

这能从本质上解决 "清除状态" 的问题。

当然也能将一个 Provider 标记为 .family(并同时标记为 .autoDispose)。

这样可以给 provider 传递参数,可以在内部生成多个 provider 并进行追踪。

换句话说,传递参数时,会为每个唯一的参数创建唯一的状态

dart 复制代码
@riverpod
int random(RandomRef ref, {required int seed, required int max}) {
  return Random(seed).nextInt(max);
}

这解决了 "每个页面都自定义状态" 的问题。实际上,还有另外一个优点:状态不再局限于某个特定的页面。

并且,如果如果不同的页面尝试访问相同的状态,这样的页面也可以只是重新使用参数就能做到。

在很多方面,向 provider 传递参数等同于传递 Map 的键值。 如果键是相同的,获取的值就是相同的。如果是不同的键,就会获取不同的状态。


相关推荐
AiFlutter15 小时前
Flutter之Package教程
flutter
Mingyueyixi19 小时前
Flutter Spacer引发的The ParentDataWidget Expanded(flex: 1) 惨案
前端·flutter
crasowas1 天前
Flutter问题记录 - 适配Xcode 16和iOS 18
flutter·ios·xcode
老田低代码2 天前
Dart自从引入null check后写Flutter App总有一种难受的感觉
前端·flutter
AiFlutter3 天前
Flutter Web首次加载时添加动画
前端·flutter
ZemanZhang4 天前
Flutter启动无法运行热重载
flutter
AiFlutter4 天前
Flutter-底部选择弹窗(showModalBottomSheet)
flutter
帅次5 天前
Android Studio:驱动高效开发的全方位智能平台
android·ide·flutter·kotlin·gradle·android studio·android jetpack
程序者王大川5 天前
【前端】Flutter vs uni-app:性能对比分析
前端·flutter·uni-app·安卓·全栈·性能分析·原生
yang2952423615 天前
使用 Vue.js 将数据对象的值放入另一个数据对象中
前端·vue.js·flutter