最后一篇文章,咱们在掌握了怎么读取状况值,并知道怎么依据不同场景挑选不同类型的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传递多个值的办法。另一方面,这个值能够是任何东西(只需它符合前面说到的约束)。

这包括下面这些类型。

  • tuple类型,相似Python的元组,pub.dev/packages/tu…
  • 用Freezed或build_value生成的方针,pub.dev/packages/fr…
  • 运用equatable的方针,pub.dev/packages/eq…

下面是一个对多个参数运用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在实战中的运用技巧。

向我们引荐下我的网站 xuyisheng.top/ 专心 AndroidKotlin-Flutter 欢迎我们拜访

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