最后一篇文章,咱们在掌握了怎么读取状况值,并知道怎么依据不同场景挑选不同类型的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/ 专心 Android–Kotlin-Flutter 欢迎我们拜访