最近一两个月在一些Flutter的话题中不断的见到了Riverpod
这个关键词,细看后发现它是Flutter状态管理的一个新方式。
Flutter的状态管理方式有很多,Redux
、 Bloc
、 MobX
、Provider
等等。单单一个Provider,我也见到了各种组合,例如ChangeNotifier + Provider / StateNotifier + Provider( + freezed)。各种方式各有千秋,我们根据自己的习惯和项目的情况去选择就好,这里不做讨论。本篇只是来介绍一下Riverpod
,给大家提供一个新的选择。
Riverpod和Provider
师出同门,都来自作者Remi,Riverpod
可以被认为是Provider
的重写,来实现原本不可能的功能。就像它的名字一样,字母与provider相同,但是又不相同。
你可以理解Riverpod
是Provider
的升级版,解决了Provider
的一些痛点:
Provider
是InheritedWidget
的封装,所以在读取状态时需要BuildContext
。这导致了许多的限制,许多新手在不理解InheritedWidget
和BuildContext
时,跨页面获取状态经常会ProviderNotFoundException
。而Riverpod
不再依赖Flutter,也就是没有使用InheritedWidget
,所以也不需要BuildContext
。当然目前Riverpod
也有一些不足(0.9.1版本):
作者提供了Riverpod
的三种方式,怎样选择如下图:
本篇不引入flutter_hooks相关内容,这里我就选择flutter_riverpod
。那么将它添加到pubspec.yaml
中:
flutter_riverpod: ^0.9.1
然后执行flutter pub get
。
这里使用Riverpod
的Provider
需要三步就可以。
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // 1.创建一个全局的provider,里面储存“Hello World!” final Provider<String> helloWorldProvider = Provider((_) => 'Hello World!'); void main() { runApp( // 2.添加“ProviderScope”。所有使用Riverpod的Flutter程序都必须 // 在widget tree的根部添加它,用来储存各个provider。 ProviderScope( child: MyApp(), ), ); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Riverpod Example', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: ProviderExample(), ); } } // 3.使用“ConsumerWidget”,在“build”中获取对应的provider class ProviderExample extends ConsumerWidget { @override Widget build(BuildContext context, ScopedReader watch) { final String value = watch(helloWorldProvider); return Scaffold( appBar: AppBar(title: Text('Provider Example')), body: Center( child: Text(value), ), ); } }
这里储存“Hello World!” 使用的是Provider
,它提供一个永远不变的对象。不过大部分场景下状态都是可变的,下面用计数器来举例。
在“Hello World”的基础上,做两点修改即可。
定义一个全局常量StateProvider
。
final StateProvider<int> counterProvider = StateProvider((_) => 0);
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class StateProviderExample extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('StateProvider Example'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Consumer( builder: (context, watch, _) { /// 使用Consumer(ConsumerWidget的封装),控制刷新的范围。 int count = watch(counterProvider).state; return Text( '$count', style: Theme.of(context).textTheme.headline4, ); }, ), ], ), ), floatingActionButton: FloatingActionButton( /// 使用read获取counterProvider,操作state。 onPressed: () => context.read(counterProvider).state++, tooltip: 'Increment', child: Icon(Icons.add), ), ); } }
如果你的状态比较复杂可以使用ChangeNotifierProvider
,如果习惯使用StateNotifier
,可以使用StateNotifierProvider
。其实StateProvider
的内部是StateController
,也还是StateNotifier
。源码如下;
class StateProvider<T> extends AlwaysAliveProviderBase<StateController<T>, StateController<T>> { StateProvider( Create<T, ProviderReference> create, { String name, }) : super((ref) => StateController(create(ref)), name); ... } class StateController<T> extends StateNotifier<T> { StateController(T state) : super(state); @override T get state => super.state; @override set state(T value) => super.state = value; }
StateNotifierProvider
的用法与StateProvider
基本一致,这里就不贴出来了,有兴趣的可以点击这里查看。
这部分没啥说的,注意ChangeNotifier
与StateNotifier
的区别,需要自己调用notifyListeners
通知变更。
final ChangeNotifierProvider<Counter> _counterProvider = ChangeNotifierProvider((_) => Counter()); class Counter extends ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); } void decrement(){ _count--; notifyListeners(); } } class ChangeProviderNotifierExample extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('ChangeNotifierProvider Example'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Consumer( builder: (context, watch, _) { int count = watch(_counterProvider).count; return Text( '$count', style: Theme.of(context).textTheme.headline4, ); }, ), ], ), ), floatingActionButton: FloatingActionButton( /// 使用read获取counterProvider。 onPressed: () => context.read(_counterProvider).increment(), tooltip: 'Increment', child: Icon(Icons.add), ), ); } }
final FutureProvider<String> futureProvider = FutureProvider((_) async { /// 延时3s await Future.delayed(const Duration(seconds: 3)); return 'Riverpod'; }); class FutureProviderExample extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('FutureProvider Example'), ), body: Center( child: Consumer( builder: (context, watch, _) { AsyncValue<String> futureProviderValue = watch(futureProvider); /// 根据相应状态展示 return futureProviderValue.when( loading: () => CircularProgressIndicator(), error: (error, stack) => Text('Oops, something unexpected happened'), data: (value) => Text( 'Hello $value', style: Theme.of(context).textTheme.headline4, ), ); }, ), ), ); } }
作者也提供了StreamProvider
。用法大同小异,有兴趣的可以查看我的示例代码。
如果你希望在Widget Tree上监听provider的状态变化,可以使用ProviderListener
。用上面的计数器例子,当计数器为5时,触发监听。
ProviderListener<StateController<int>>( provider: counterProvider, onChange: (_, counter) { if (counter.state == 5) { print('当前计数器为5,触发监听。'); } }, child: Consumer( builder: (context, watch, _) { int count = watch(counterProvider).state; return Text( '$count', style: Theme.of(context).textTheme.headline4, ); }, ), ),
一般我们在实现一个列表的Item时,需要传入相应的index大致如下:
ListView.builder( itemCount: 50, itemBuilder: (context, index) { return ProductItem(index: index); }, )
如果使用ScopedProvider
并结合 ProviderScope
,就可以简单的获取index,不必从构造方法接收它。使用起来很简单,直接上代码:
/// 定义ScopedProvider final ScopedProvider<int> currentProductIndex = ScopedProvider<int>(null); class ScopeProviderExample extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('ScopedProvider'), ), body: ListView.builder( itemCount: 50, itemBuilder: (context, index) { return ProviderScope( overrides: [ /// 修改value currentProductIndex.overrideWithValue(index), ], /// 使用'const'关键字实例化了“ProductItem”, /// 但仍然可以在内部动态获取内容。 child: const ProductItem(), ); }, ), ); } } class ProductItem extends ConsumerWidget { const ProductItem({Key key}): super(key: key); @override Widget build(BuildContext context, ScopedReader watch) { /// 获取相应index final index = watch(currentProductIndex); return ListTile(title: Text('item $index')); } }
family
的作用是可以在获取provider时可以添加一个参数。直接上例子,一看便知:
/// 使用family,可以在获取provider时传入city final _weatherProvider = Provider.family<String, String>((ref, city) { return '$city (Sunny)'; }); class FamilyExample extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Family')), body: Center( child: Consumer( builder: (context, watch, _) { /// 这里可以传参“London” final String weather = watch(_weatherProvider('London')); return Text('$weather',); }, ), ), ); } }
注意: 使用family
时传入的参数是有限制的。比如bool
、 int
、 double
、 String
、常量或是重写了==
和hashCode
的不可变对象。
前面我们的例子中,创建的provider因为保存在Widget Tree的根部。所以即使页面关闭,再次进入页面时会获取之前的状态。
这显然是不灵活的,那么这里就可以使用autoDispose
,它可以在我们不再使用provider时,自动将其销毁。那么合理的使用它可以避免内存泄漏。
比如之前的计数器例子,只需加一个autoDispose
就可以避免此类问题。
final stateProvider = StateProvider.autoDispose((_) => 0);
如果你需要自定义dispose事件,可以使用onDispose
。比如你的provider中有网络请求(使用Dio):
final myProvider = FutureProvider.autoDispose((ref) async { final cancelToken = CancelToken(); // 当provider被销毁时,取消http请求 ref.onDispose(() => cancelToken.cancel()); // http请求 final response = await dio.get('path', cancelToken: cancelToken); // 如果请求成功完成,则保持该状态。 ref.maintainState = true; return response; });
上面代码中出现了ref.maintainState
,这个参数默认为false。如果用户离开页面并且请求失败,下次则将再次执行该请求。但是,如果请求成功完成(maintainState为true),则将保留状态,下次重新进入页面时不会触发新的请求。
使用autoDispose
可以达到限制provider是全局还是局部作用。这样一来,可以更方便的解决跨页面使用provider的问题。
1.如果创建的provider需要另一个provider的状态,这时就需要使用ProviderReference
的read
方法。
下面的示例是,给予城市和国家的provider,当创建locationProvider时,获取城市和国家的状态。
final Provider<String> cityProvider = Provider((ref) => 'London'); final Provider<String> countryProvider = Provider((ref) => 'England'); final Provider<Location> locationProvider = Provider((ref) => Location(ref)); class Location { Location(this._ref); final ProviderReference _ref; String get label { /// read 获取 final city = _ref.read(cityProvider); final country = _ref.read(countryProvider); return '$city ($country)'; } }
使用Riverpod
就可以提供多个相同类型的Provider,这也是相比Provider
的一个优点。
2.如果获取的状态值会发生变化,我们需要监听它。可以使用ProviderReference
的watch
方法。
下面的示例是,给予城市provider,当城市变化时,天气也相应变化。
final StateProvider<String> cityProvider = StateProvider((ref) => 'London'); final StateProvider<String> weatherProvider = StateProvider((ref) { /// watch监听 final String city = ref.watch(cityProvider).state; return '$city (Sunny)'; }); class CombiningProviderExample2 extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('CombiningProvider')), body: Center( child: Consumer( builder: (context, watch, _) { final String weather = watch(weatherProvider).state; return Text('$weather',); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () { String city = context.read(cityProvider).state; /// 修改状态 if (city == 'London') { context.read(cityProvider).state = "Xi'an"; } else { context.read(cityProvider).state = 'London'; } }, tooltip: 'Refresh', child: Icon(Icons.refresh), ), ); } }
强制provider立即刷新,重新返回创建的值。这种适合列表下拉刷新,或者请求数据错误时重试。
final FutureProvider<List<String>> productsProvider = FutureProvider((_) async { /// 延时3s await Future.delayed(const Duration(seconds: 3)); return List.generate(50, (index) => 'Item $index'); }); class RefreshProviderExample extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('RefreshProvider'), ), body: Center( child: Consumer( builder: (context, watch, _) { AsyncValue<List<String>> productsProviderValue = watch(productsProvider); return productsProviderValue.when( loading: () => CircularProgressIndicator(), error: (error, stack) => Text('Oops, something unexpected happened'), data: (list) => RefreshIndicator( onRefresh: () => context.refresh(productsProvider), /// 刷新 child: ListView( children: [ for (final item in list) ListTile(title: Text(item)), ], ), ), ); }, ), ), ); } }
当状态中某一个值发生变化时,相应Consumer
下的builder
就会执行,重建widget。如果使用select
可以指定某一值更改时进行刷新,精准控制刷新范围,避免不必要的rebuild。
不过目前(0.9.1版本),select
这种局部监听只支持使用hooks_riverpod
包的useProvider
。所以这里需要引用hooks_riverpod
。
final ChangeNotifierProvider<Person> personProvider = ChangeNotifierProvider((_) => Person()); class Person extends ChangeNotifier { int _age = 0; int get age => _age; set age(int age) { _age = age; notifyListeners(); } String _name = 'weilu'; String get name => _name; set name(String name) { _name = name; notifyListeners(); } } class SelectExample extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Select Example'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ HookBuilder( builder: (_) { String name = useProvider(personProvider.select((p) => p.name)); /// 如果使用下面的方式,则age变化时,这里的Text也会刷新。 // String name = useProvider(personProvider).name; return Text( 'name:$name', ); }, ), HookBuilder( builder: (_) { int age = useProvider(personProvider.select((p) => p.age)); return Text( 'age:$age', ); }, ), ], ), ), floatingActionButton: FloatingActionButton( // 这里age变化时,只有对应的Text会变化。 onPressed: () => context.read(personProvider).age = Random.secure().nextInt(255), tooltip: 'Refresh', child: Icon(Icons.refresh), ), ); } }
细心的你会发现,在使用read
获取provider时还是使用了context
。一开始不是说,没有使用InheritedWidget
,所以也不需要BuildContext
吗?
其实Riverpod
本身确实如此,但是在Flutter的应用中,为了便于高效(时间复杂度O(1))的在Widget Tree中获取ProviderContainer
(在ProviderScope
中隐式创建,用来储存provider),需要在根部使用InheritedWidget
,便于最终获取provider。
read
、refresh
、Consumer
、ProviderListener
等方法和Widget的内部其实都调用了ProviderScope.containerOf(context, listen = xx);
,不同的是listen的值。
static ProviderContainer containerOf( BuildContext context, { bool listen = true, }) { UncontrolledProviderScope scope; if (listen) { scope = context // .dependOnInheritedWidgetOfExactType<UncontrolledProviderScope>(); } else { scope = context .getElementForInheritedWidgetOfExactType<UncontrolledProviderScope>() .widget as UncontrolledProviderScope; } return scope.container; }
比如read
中listen的值为false,使用getElementForInheritedWidgetOfExactType
方法,这样在数据发生变化时就不会掉用didChangeDependencies
,避免不必要的rebuild。相对的,Consumer
、ProviderListener
中listen的值为ture,会实现我们需要的widget重建。
我们可以还可以通过Flutter Inspector检查已有的状态,所有状态汇总在ProviderScope
下面,这也是Riverpod
的一个优点。如下图所示:
发布本篇时,有关Riverpod
的资料与讨论很少。本篇也是我在实践完官网文档后的理解,如有错误,欢迎指出!
个人认为Riverpod
是相对更轻松便捷的一种状态管理方式,待它稳定时应该能被更多的人喜爱。
Riverpod
的相关示例代码我已经上传至Github,有兴趣的可以看看。后面如果Riverpod
有变动时,我也会及时更新。大家可以收藏起来,多多点赞支持一下,给我点更新动力!