微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

重走Flutter状态管理之Riverpod最终篇

作者:xuyisheng
转载地址:https://juejin.cn/post/7105937480892809252

我们在掌握了如何读取状态值,并知道如何根据不同场景选择不同类型的Provider,以及如何对Provider进行搭配使用之后,再来了解一下它的一些其它特性,看看它们是如何帮助我们更好的进行状态管理的。

Provider Modifiers

所有的Provider都有一个内置的方法来为你的不同Provider添加额外的功能

它们可以为 ref 对象添加新的功能,或者稍微改变Provider的consume方式。Modifiers可以在所有Provider上使用,其语法类似于命名的构造函数

final myAutodisposeProvider = StateProvider.autodispose<int>((ref) => 0);
final myFamilyProvider = Provider.family<String,int>((ref,id) => '$id');

目前,有两个Modifiers可用。

  • .autodispose,这将使Provider在不再被监听时自动销毁其状态
  • .family,它允许使用一个外部参数创建一个Provider

一个Provider可以同时使用多个Modifiers。

final userProvider = FutureProvider.autodispose.family<User,userId) async {
  return fetchUser(userId);
});

.family

.family修饰符有一个目的:根据外部参数创建一个独特的Provider。family的一些常见用例是下面这些。

  • 将FutureProvider与.family结合起来,从其ID中获取一个Message对象
  • 将当前的Locale传递给Provider,这样我们就可以处理国际化

family的工作方式是通过向Provider添加一个额外的参数。然后,这个参数可以在我们的Provider中自由使用,从而创建一些状态。

例如,我们可以将family与FutureProvider结合起来,从其ID中获取一个Message。

final messagesFamily = FutureProvider.family<Message,String>((ref,id) async {
  return dio.get('http://my_api.dev/messages/$id');
});

当使用我们的 messagesFamily Provider时,语法会略有不同。

像下面这样的通常语法将不再起作用。

Widget build(BuildContext context,WidgetRef ref) {
  // Error – messagesFamily is not a provider
  final response = ref.watch(messagesFamily);
}

相反,我们需要向 messagesFamily 传递一个参数。

Widget build(BuildContext context,WidgetRef ref) {
  final response = ref.watch(messagesFamily('id'));
}

我们可以同时使用一个具有不同参数的变量。

例如,我们可以使用titleFamily来同时读取法语和英语的翻译。

@override
Widget build(BuildContext context,WidgetRef ref) {
final frenchTitle = ref.watch(titleFamily(const Locale('fr')));
final englishTitle = ref.watch(titleFamily(const Locale('en')));

return Text('fr: $frenchTitle en: $englishTitle');
}

参数限制

为了让families正确工作,传递给Provider的参数必须具有一致的hashCode和==。

理想情况下,参数应该是一个基础类型(bool/int/double/String),一个常数(Provider),或者一个重写==和hashCode的不可变的对象。

当参数不是常数时,更倾向于使用autodispose

你可能想用family来传递一个搜索字段的输入,给你的Provider。但是这个值可能会经常改变,而且永远不会被重复使用。这可能导致内存泄漏,因为在认情况下,即使不再使用,Provider也不会被销毁。

同时使用.family和.autodispose就可以修复这种内存泄漏。

final characters = FutureProvider.autodispose.family<List<Character>,filter) async {
  return fetchCharacters(filter: filter);
});

给family传递多重参数

family没有内置支持一个Provider传递多个值的方法。另一方面,这个值可以是任何东西(只要它符合前面提到的限制)。

包括下面这些类型。

下面是一个对多个参数使用Freezed或equatable的例子。

@freezed
abstract class MyParameter with _$MyParameter {
  factory MyParameter({
    required int userId,required Locale locale,}) = _MyParameter;
}

final exampleProvider = Provider.autodispose.family<Something,MyParameter>((ref,myParameter) {
  print(myParameter.userId);
  print(myParameter.locale);
  // Do something with userId/locale
});

@override
Widget build(BuildContext context,WidgetRef ref) {
  int userId; // Read the user ID from somewhere
  final locale = Localizations.localeOf(context);

  final something = ref.watch(
    exampleProvider(MyParameter(userId: userId,locale: locale)),);

  ...
}

.autodispose

它的一个常见的用例是,当一个Provider不再被使用时,要销毁它的状态。

这样做的原因有很多,比如下面这些场景。

  • 当使用Firebase时,要关闭连接并避免不必要的费用
  • 用户离开一个屏幕并重新进入时,要重置状态

Provider通过.autodisposeModifiers内置了对这种使用情况的支持

要告诉Riverpod当它不再被使用时销毁一个Provider的状态,只需将.autodispose附加到你的Provider上即可。

final userProvider = StreamProvider.autodispose<User>((ref) {

});

就这样了。现在,userProvider的状态将在不再使用时自动被销毁。

注意通用参数是如何在autodispose之后而不是之前传递的–autodispose不是一个命名的构造函数

如果需要,你可以将.autodispose与其他Modifiers结合起来。

final userProvider = StreamProvider.autodispose.family<User,id) {

});

ref.keepAlive

用autodispose标记一个Provider时,也会在ref上增加一个额外的方法:keepAlive。

keep函数是用来告诉Riverpod,即使不再被监听,Provider的状态也应该被保留下来。

它的一个用例是在一个HTTP请求完成后,将这个标志设置为true。

final myProvider = FutureProvider.autodispose((ref) async {
  final response = await httpClient.get(...);
  ref.keepAlive();
  return response;
});

这样一来,如果请求失败,UI离开屏幕然后重新进入屏幕,那么请求将被再次执行。但如果请求成功完成,状态将被保留,重新进入屏幕将不会触发新的请求。

示例:当Http请求不再使用时自动取消

autodisposeModifiers可以与FutureProvider和ref.ondispose相结合,以便在不再需要HTTP请求时轻松取消。

我们的目标是:

  • 用户进入一个屏幕时启动一个HTTP请求
  • 如果用户在请求完成前离开屏幕,则取消HTTP请求
  • 如果请求成功,离开并重新进入屏幕不会启动一个新的请求

代码中,这将是下面这样。

final myProvider = FutureProvider.autodispose((ref) async {
  // An object from package:dio that allows cancelling http requests
  final cancelToken = CancelToken();
  // When the provider is destroyed,cancel the http request
  ref.ondispose(() => cancelToken.cancel());

  // Fetch our data and pass our `cancelToken` for cancellation to work
  final response = await dio.get('path',cancelToken: cancelToken);
  // If the request completed successfully,keep the state
  ref.keepAlive();
  return response;
});

异常

当使用.autodispose时,你可能会发现自己的应用程序无法编译,出现类似下面的错误

The argument type ‘AutodisposeProvider’ can’t be assigned to the parameter type ‘AlwaysAliveProviderBase’

不要担心! 这个错误是正常的。它的发生是因为你很可能有一个bug。

例如,你试图在一个没有标记为.autodispose的Provider中监听一个标记为.autodispose的Provider,比如下面的代码

final firstProvider = Provider.autodispose((ref) => 0);

final secondProvider = Provider((ref) {
  // The argument type 'AutodisposeProvider<int>' can't be assigned to the
  // parameter type 'AlwaysAliveProviderBase<Object,Null>'
  ref.watch(firstProvider);
});

这是不可取的,因为这将导致firstProvider永远不会被dispose。

为了解决这个问题,可以考虑用.autodispose标记secondProvider。

final firstProvider = Provider.autodispose((ref) => 0);

final secondProvider = Provider.autodispose((ref) {
  ref.watch(firstProvider);
});

provider状态关联与整合

我们之前已经看到了如何创建一个简单的Provider。但实际情况是,在很多情况下,一个Provider会想要读取另一个Provider的状态。

要做到这一点,我们可以使用传递给我们Provider的回调的ref对象,并使用其watch方法

作为一个例子,考虑下面的Provider。

final cityProvider = Provider((ref) => 'London');

我们现在可以创建另一个Provider,它将消费我们的cityProvider。

final weatherProvider = FutureProvider((ref) async {
  // We use `ref.watch` to listen to another provider,and we pass it the provider
  // that we want to consume. Here: cityProvider
  final city = ref.watch(cityProvider);

  // We can then use the result to do something based on the value of `cityProvider`.
  return fetchWeather(city: city);
});

这就是了。我们已经创建了一个依赖另一个Provider的Provider。

这个其实在前面的例子中已经讲到了,ref是可以连接多个不同的Provider的,这是Riverpod非常灵活的一个体现。

FAQ

What if the value being listened to changes over time?

根据你正在监听的Provider,获得的值可能会随着时间的推移而改变。例如,你可能正在监听一个StateNotifierProvider,或者被监听的Provider可能已经通过使用ProviderContainer.refresh/ref.refresh强制刷新。

当使用watch时,Riverpod能够检测到被监听的值发生了变化,并将在需要时自动重新执行Provider的创建回调。

这对计算的状态很有用。例如,考虑一个暴露了todo-list的StateNotifierProvider。

class TodoList extends StateNotifier<List<Todo>> {
  TodoList(): super(const []);
}

final todoListProvider = StateNotifierProvider((ref) => TodoList());

一个常见的用例是让用户界面过滤todos的列表,只显示已完成/未完成的todos。

实现这种情况的一个简单方法是。

  • 创建一个StateProvider,它暴露了当前选择的过滤方法
enum Filter {
  none,completed,uncompleted,}

final filterProvider = StateProvider((ref) => Filter.none);
  • 一个单独的Provider,把过滤方法和todo-list结合起来,暴露出过滤后的todo-list。
final filteredTodoListProvider = Provider<List<Todo>>((ref) {
  final filter = ref.watch(filterProvider);
  final todos = ref.watch(todoListProvider);

  switch (filter) {
    case Filter.none:
      return todos;
    case Filter.completed:
      return todos.where((todo) => todo.completed).toList();
    case Filter.uncompleted:
      return todos.where((todo) => !todo.completed).toList();
  }
});

然后,我们的用户界面可以监听filteredTodoListProvider来监听过滤后的todo-list。使用这种方法,当过滤器或todo-list发生变化时,用户界面将自动更新。

要看到这种方法的作用,你可以看一下Todo List例子的源代码

这种行为不是特定于Provider的,它适用于所有的Provider。

例如,你可以将watch与FutureProvider结合起来,实现一个支持实时配置变化的搜索功能

// The current search filter
final searchProvider = StateProvider((ref) => '');

/// Configurations which can change over time
final configsProvider = StreamProvider<Configuration>(...);

final charactersProvider = FutureProvider<List<Character>>((ref) async {
  final search = ref.watch(searchProvider);
  final configs = await ref.watch(configsProvider.future);
  final response = await dio.get('${configs.host}/characters?search=$search');

  return response.data.map((json) => Character.fromJson(json)).toList();
});

这段代码将从服务中获取一个字符列表,并在配置改变或搜索查询改变时自动重新获取该列表。

Can I read a provider without listening to it?

有时,我们想读取一个Provider的内容,但在获得的值发生变化时不需要重新创建值。

一个例子是一个 Repository,它从另一个Provider那里读取用户token用于认证。

我们可以使用观察并在用户token改变时创建一个新的 Repository,但这样做几乎没有任何用处。

在这种情况下,我们可以使用read,这与listen类似,但不会导致Provider在获得的值改变时重新创建它的值。

在这种情况下,一个常见的做法是将ref.read传递给创建的对象。然后,创建的对象将能够随时读取Provider。

final userTokenProvider = StateProvider<String>((ref) => null);

final repositoryProvider = Provider((ref) => Repository(ref.read));

class Repository {
  Repository(this.read);

  /// The `ref.read` function
  final Reader read;

  Future<Catalog> fetchCatalog() async {
    String token = read(userTokenProvider);

    final response = await dio.get('/path',queryParameters: {
      'token': token,});

    return Catalog.fromJson(response.data);
  }
}

你也可以把ref而不是ref.read传给你的对象。

final repositoryProvider = Provider((ref) => Repository(ref));

class Repository {
  Repository(this.ref);

  final Ref ref;
}

传递ref.read带来的唯一区别是,它略微不那么冗长,并确保我们的对象永远不会使用ref.watch。

但是,永远不要像下面这样做。

final myProvider = Provider((ref) {
  // Bad practice to call `read` here
  final value = ref.read(anotherProvider);
});

如果你使用read作为尝试去避免太多的刷新重建,可以参考后面的FAQ

How to test an object that receives read as a parameter of its constructor?

如果你正在使用《我可以在不监听Provider的情况下读取它吗》中描述的模式,你可能想知道如何为你的对象编写测试。

在这种情况下,考虑直接测试Provider而不是原始对象。你可以通过使用ProviderContainer类来做到这一点。

final repositoryProvider = Provider((ref) => Repository(ref.read));

test('fetches catalog',() async {
  final container = ProviderContainer();
  addTearOff(container.dispose);

  Repository repository = container.read(repositoryProvider);

  await expectLater(
    repository.fetchCatalog(),completion(Catalog()),);
});

My provider updates too often,what can I do?

如果你的对象被重新创建得太频繁,你的Provider很可能在监听它不关心的对象。

例如,你可能在监听一个配置对象,但只使用host属性

通过监听整个配置对象,如果host以外的属性发生变化,这仍然会导致你的Provider被重新评估–这可能是不希望的。

这个问题的解决方案是创建一个单独的Provider,只公开你在配置中需要的东西(所以是host)。

应当避免像下面的代码一样,对整个对象进行监听。

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
  // Will cause productsProvider to re-fetch the products if anything in the
  // configurations changes
  final configs = await ref.watch(configProvider.future);

  return dio.get('${configs.host}/products');
});

当你只需要一个对象的单一属性时,更应该使用select。

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
  // Listens only to the host. If something else in the configurations
  // changes,this will not pointlessly re-evaluate our provider.
  final host = await ref.watch(configProvider.selectAsync((config) => config.host));

  return dio.get('$host/products');
});

这将只在host发生变化时重建 productsProvider。

通过这三篇文章,相信大家已经能熟练的对Riverpod进行使用了,相比package:Provider,Riverpod的使用更加简单和灵活,这也是我推荐它的一个非常重要的原因,在入门之后,大家可以根据文档中作者提供的示例来进行学习,充分的了解Riverpod在实战中的使用技巧。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐