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

更新日期: 2022-05-28阅读: 1.1k标签: Flutter

最后一篇文章,我们在掌握了如何读取状态值,并知道如何根据不同场景选择不同类型的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, int>((ref, 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>, String>((ref, 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, String>((ref, 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在实战中的使用技巧。

向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问

链接: https://fly63.com/article/detial/11614

移动跨平台框架Flutter介绍和学习线路

Flutter是一款移动应用程序SDK,一份代码可以同时生成iOS和Android两个高性能、高保真的应用程序。Flutter目标是使开发人员能够交付在不同平台上都感觉自然流畅的高性能应用程序。我们兼容滚动行为、排版、图标等方面的差异。

初识flutter

关注flutter已经好久,因为没有发正式版,所以一直也不想过早的躺浑水,但是最近无意中看到几篇文章,再加上美团和咸鱼等app也一直在做灰度测试,所以上周开始看了一下官方文档,地址:https://flutter.io/docs/get-started/install,然后在此做一下总结。

深入理解Flutter多线程

Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。和iOS应用很像,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate。

Flutter1.5 开始,将成为全平台 UI 框架!

Flutter 1.5 的发布,同期也宣布发布 Flutter for Web 的 Preview 版本,正式开启了 Flutter 的全平台 UI 框架之路。早在年初发布的 Flutter 2019 Roadmap 中,就有提到,会在今年支持移动设备之外的平台,对 Web 的支持,算是完成了一个新的里程碑吧。

Flutter支持Web开发了!

Flutter作为一个可移植的UI框架,已经支持现代Web应用开发了!我们很开心已经发布了SDK预览版,这样你可以在Web浏览器里直接运行你的Flutter UI代码。

Flutter 混合开发 (交互通信)

Flutter 与原生之间的通信依赖灵活的消息传递方式:1,Flutter 部分通过平台通道将消息发送到其应用程序的所在的宿主环境(原生应用)。2,宿主环境通过监听平台通道,接收消息。

Flutter 局部路由实现

Flutter是借鉴React的开发思想实现的,在子组件的插槽上,React有this.props.children,Vue有<slot></slot>。当然Flutter也有类似的Widget,那就是Navigator,不过是以router的形式实现(像<router-view></router-view>)。

Flutter Kotlin 到底该如何选择?

这两个技术在当下如何选择,我之前在公众号上的回复是:如果你已经处于一个比较满意的公司,并考虑长期发展,公司并未使用这两个技术,你可以专心钻研公司当下使用的,或者未来将要使用的,这些才能助你在公司步步高升。

Flutter 与 iOS 原生 WebView 对比

本文对比的是 UIWebView、WKWebView、flutter_webview_plugin(在 iOS 中使用的是 WKWebView)的加载速度,内存使用情况。测试网页打开的速度,只需要获取 WebView 在开始加载网页和网页加载完成时的时间戳

Flutter For Web

用来构建漂亮、定制化应用的跨平台的 UI 框架 Flutter 现在已经支持 Web 开发了。我们很高兴推出了一个预览版的 SDK 可以让开发者直接使用 Flutter UI 和业务逻辑代码构建 web 应用

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!