作用域
Riverpod中的作用域是一个非常强大的功能,但像其他强大的功能一样,应该理智地使用它。
作用域支持:
- 覆盖特定子树的provider状态(类似于主题和
InheritedWidgets
在flutter中的工作方式) (查看示例) - 为一般异步API创建同步provider (查看示例)
- 允许
对话框(Dialog)
和覆盖层(Overlay)
从widget子树继承provider的状态以显示它们(查看示例) - 通过从widget的构造函数中删除参数来优化widget的重建,从而允许你将它们设置为
const
如果你想用作用域来表示上面的第一点,你也可以用family来代替。 family的优点是允许你从widget树中的任何位置访问状态的每个实例,而不仅仅是从你所在的特定子树的状态范围访问。
使用作用域创建provider状态的多个实例与 package:provider
的工作方式类似。
但是,使用作用域来完成该任务的限制更大,因为你不能决定从该作用域访问其他实例。
因此,在确定所使用的每个provider的作用域之前,请仔细考虑为什么要确定provider的作用域。
ProviderScope 和 ProviderContainer
作用域由 ProviderContainer 引入。这个容器保存了所有provider的当前状态。它管理provider之间的查找和订阅功能。
在Flutter中,你应该使用 ProviderScope widget, 它内部包含一个 ProviderContainer,并提供了一种访问该容器到widget树其余部分的方法。
final valueProvider = StateProvider((ref) => 0);
// 这样做:
void main() {
runApp(ProviderScope(child: MyApp()));
}
// 不要这样做:
final myProviderContainer = ProviderContainer();
void main(){
runApp(MyApp());
}
在了解它们的工作原理之前,不要使用多个 ProviderContainer。 每个线程都有自己独立的状态线程,这些状态线程不能相互访问。 拿测试举例,你可能希望使用单独的 ProviderContainer,以便使每个测试的状态独立于其他测试。
仅在纯Dart项目中和测试中创建并使用 ProviderContainer 且不需要 ProviderScope。
How Riverpod如何找到一个Provider
当一个widget或provider请求一个provider的值时,Riverpod在最近的ProviderScope widget中查找该provider的状态。 如果provider和它显式列出的依赖项都没有在该范围内被覆盖到,Riverpod将继续查找widget树。 如果provider没有在任何widget子树中被覆盖到,则默认查找到根 ProviderScope 中的 ProviderContainer。
一旦该进程定位了provider应该驻留的作用域,它就会确定provider是否已经创建。 如果是,它将返回provider的状态。但是,如果provider已经失效或未初始化,它将使用provider的构建方法创建状态。
异步API的同步provider初始化
通常,你可能会对依赖项(如 SharedPreferences
或 FirebaseApp
)进行一些异步初始化。
许多其他provider可能依赖于此,在每个provider中处理错误/加载中状态是多余的。
你可以保证这些provider不会有错误,并且在应用启动时可以快速加载。
那么,如何让这些provider状态同步可用呢?
下面是一个示例,它展示了当异步API准备好时,作用域如何允许你覆盖一个形式上的provider。
// We'd like to obtain an instance of shared preferences synchronously in a provider
final countProvider = StateProvider<int>((ref) {
final preferences = ref.watch(sharedPreferencesProvider);
final currentValue = preferences.getInt('count') ?? 0;
ref.listenSelf((prev, curr) {
preferences.setInt('count', curr);
});
return currentValue;
});
// We don't have an actual instance of SharedPreferences, and we can't get one except asynchronously
final sharedPreferencesProvider =
Provider<SharedPreferences>((ref) => throw UnimplementedError());
Future<void> main() async {
// Show a loading indicator before running the full app (optional)
// The platform's loading screen will be used while awaiting if you omit this.
runApp(const LoadingScreen());
// Get the instance of shared preferences
final prefs = await SharedPreferences.getInstance();
return runApp(
ProviderScope(
overrides: [
// Override the unimplemented provider with the value gotten from the plugin
sharedPreferencesProvider.overrideWithValue(prefs),
],
child: const MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// Use the provider without dealing with async issues
final count = ref.watch(countProvider);
return Text('$count');
}
}
展示对话框
当你显示一个对话框(Dialog)
或 OverlayEntry
时,flutter会创建一个新的 路由(Route)
或添加到具有不同构建范围的 Overlay
中,
这样它就可以摆脱它的父布局,并可以显示在其他 路由(Routes)
之上。
但这通常会给 InheritedWidget
带来一个问题,因为 ProviderScope 也是一个 InheritedWidget
,所以它也会受到影响。
为了解决这个问题,Riverpod允许你创建一个 ProviderScope
,它可以访问父作用域中所有provider的状态。
下面的示例展示了如何使用这个功能,它允许打开的Dialog
从上下文(context)中访问计数器的状态。
// Have a counter that is being incremented by the FloatingActionButton
final counterProvider = StateProvider((ref) => 0);
class Home extends ConsumerWidget {
const Home({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// We want to show a dialog with the count on a button press
return Scaffold(
body: Column(
children: [
ElevatedButton(
onPressed: () {
showDialog<void>(
context: context,
builder: (c) {
// We wrap the dialog with a ProviderScope widget, providing the
// parent container to ensure the dialog can access the same providers
// that are accessible by the Home widget.
return ProviderScope(
parent: ProviderScope.containerOf(context),
child: const AlertDialog(
content: CounterDisplay(),
),
);
},
);
},
child: const Text('Show Dialog'),
),
],
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
));
}
}
class CounterDisplay extends ConsumerWidget {
const CounterDisplay({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
子树作用域
作用域允许你覆盖widget树的特定子树的provider状态。
通过这种方式,它可以提供类似于flutter中的 InheritedWidget
或 package:provider
中的provider机制。
For example, in flutter you can override the Theme
for a particular subtree of your widget tree, by wrapping it in a Theme
widget.
比如,在flutter中,通过将widget树的特定子树包装在Theme widget中,可以覆盖widget树的Theme。
void main() {
runApp(
ProviderScope(
child: MaterialApp(
theme: ThemeData(primaryColor: Colors.blue),
home: const Home(),
),
),
);
}
// Have a counter that is being incremented
final counterProvider = StateProvider(
(ref) => 0,
);
class Home extends ConsumerWidget {
const Home({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Column(
children: [
// This counter will have a primary color of green
Theme(
data: Theme.of(context).copyWith(primaryColor: Colors.green),
child: const CounterDisplay(),
),
// This counter will have a primary color of blue
const CounterDisplay(),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: const Text('Increment Count'),
),
],
));
}
}
class CounterDisplay extends ConsumerWidget {
const CounterDisplay({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
final theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$count',
style: theme.textTheme.displayMedium
?.copyWith(color: theme.primaryColor),
),
],
);
}
}
在底层,Theme
是一个 InheritedWidget
,当widget查找 Theme
时,它们从widget树中找到最近的 Theme
widget 来获得主题。
Riverpod的工作方式不太一样,因为应用的所有状态通常存储在根 ProviderScope widget中。 不要担心,当状态改变时,这不会导致整个应用程序重新构建,它只是允许你从widget树中的任何位置去访问状态。
如果根据所处的页面需要不同的provider该怎么办?
你应该考虑的第一件事是它所提供的行为是否会以某种方式有所不同。
如果不同 -> 只需创建一个不同名称的新的provider,并在该页面中使用它
如果相同 -> 考虑使用family在这里了解更多关于family的内容。
通常,你开始时认为只需要在特定页面上使用provider,但最后却希望在稍后的另一个页面上也使用它。
family可以让你不受这种可能性的影响,如果你是来自 package:provider
的开发者,你应该使用family来调整思维。
但如果family确实不适合你的用例,下面的示例向你展示了如何覆盖特定子树的provider:
/// A counter that is being incremented by each [CounterDisplay]'s ElevatedButton
final counterProvider = StateProvider(
(ref) => 0,
);
final adjustedCountProvider = Provider(
(ref) => ref.watch(counterProvider) * 2,
// Note that if a provider depends on a provider that is overridden for a subtree,
// you must explicitly list that provider in your dependencies list.
dependencies: [counterProvider],
);
class Home extends ConsumerWidget {
const Home({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Column(
children: [
ProviderScope(
/// Just specify which provider you want to have a copy of in the subtree
///
/// Note that dependant providers such as [adjustedCountProvider] will
/// also be copied for this subtree. If that is not the behavior you want,
/// consider using families instead
overrides: [counterProvider],
child: const CounterDisplay(),
),
ProviderScope(
// You can change the provider's behavior in a particular subtree
overrides: [counterProvider.overrideWith((ref) => 1)],
child: const CounterDisplay(),
),
ProviderScope(
overrides: [
counterProvider,
// You can also change dependent provider's behaviors
adjustedCountProvider.overrideWith(
(ref) => ref.watch(counterProvider) * 3,
),
],
child: const CounterDisplay(),
),
// This particular display will use the provider state from the root ProviderScope
const CounterDisplay(),
],
));
}
}
class CounterDisplay extends ConsumerWidget {
const CounterDisplay({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('$count'),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: const Text('Increment Count'),
),
],
);
}
}
什么时候选择有作用域的Provider还是Family
虽然理解作用域很重要,但在使用作用域时很容易失去控制。
如果你想要一个provider状态的不同实例,取决于它在widget树中的位置,你有几个可供选择的选项: Scoping
, Families
,或组合它们。
请根据你的情况选择合适方案。
Family:
- 优点:无论你在哪个子树中,你都可以显示多个状态
- 优点:这使得它成为许多用例的更灵活和可扩展的解决方案
作用域:
- 缺点:你最终会在你的widget树中嵌套更多的ProviderScope widget
- 缺点:你只能访问一个覆盖住你部分的widget树
- 缺点:你最终不得不显式地列出大多数provider的依赖关系
- 优点:可以减少widget构造函数中的参数数量
- 优点:你可以获得轻微的性能优势,并且可以潜在地使你的一些widget的构造函数为const
组合使用这两种方法,你可以同时获得这两种方法的优点,但你仍然必须解决作用域的缺点。
请记住,作用域为每个被覆盖的provider或列出了对被覆盖的provider的依赖项的provider引入了一个新的状态实例。 如果你在应用程序的不同子树中覆盖相同的参数,它将不会是provider状态的相同实例。 一般来说,family更加灵活,并且通过即将到来的代码生成特性,可以很容易地为一个family使用多个参数。 一个很好的组合通常是同时使用family和作用域。使用一个family来提供对应用中任何地方的状态块的访问, 取决于你在widget树中的位置,然后使用作用域来提供一个特定的family状态实例。
作用域的不常见用法
有时你可能想要覆盖应用特定子树中的所有provider。 通过在每个provider的依赖项列表中列出一个公共provider, 你可以通过覆盖公共provider,轻松地一次性为所有这些provider创建新状态。
请注意,如果你尝试使用family来实现此功能,那么你将得到许多具有相同参数的family, 并且你可能会在整个widget树中传递该参数。在这种情况下,也可以使用作用域。
一旦开始使用作用域,请确保始终列出依赖项并保持最新状态,以防止运行时异常。 为了解决这个问题,我们创建了 riverpod_lint,它会在缺少依赖时警告你。 另外,使用 riverpod_generator 这个代码生成器会自动为你生成依赖项列表。