作者: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可用。
一个Provider可以同时使用多个Modifiers。
final userProvider = FutureProvider.autodispose.family<User,userId) async {
return fetchUser(userId);
});
.family
.family修饰符有一个目的:根据外部参数创建一个独特的Provider。family的一些常见用例是下面这些。
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传递多个值的方法。另一方面,这个值可以是任何东西(只要它符合前面提到的限制)。
这包括下面这些类型。
- 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不再被使用时,要销毁它的状态。
这样做的原因有很多,比如下面这些场景。
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请求时轻松取消。
我们的目标是:
在代码中,这将是下面这样。
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。
enum Filter {
none,completed,uncompleted,}
final filterProvider = StateProvider((ref) => Filter.none);
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。
// 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以外的属性发生变化,这仍然会导致你的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');
});
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] 举报,一经查实,本站将立刻删除。