两年前分享过一篇Flutter状态管理之Riverpod,当时riverpod
的版本还是0.8.0(后来文章更新到0.14版本)。当时提到过有一些不足之处:
- 毕竟诞生不久,它还不能保证是完全稳定的。
- 可能后期会有API的破坏性改动。
- 目前生产环境中使用需要谨慎。
随着作者两年来的不断维护完善,这些问题基本上已经不存在了。毕竟最痛苦的API破坏性改动已经在1.0版本时处理了一遍。
目前版本来到了2.1.1,所以本篇是对最新API用法的一个更新,还是沿用之前那篇的用例作说明。
1.配置
将它添加到pubspec.yaml
中:
flutter_riverpod: ^2.1.1
然后执行flutter pub get
。
3.基础使用
Provider
这里使用Riverpod
的Provider
需要三步就可以。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final Provider<String> helloWorldProvider = Provider((_) => 'Hello World!');
void main() {
runApp(
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(),
);
}
}
class ProviderExample extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final String value = ref.watch(helloWorldProvider);
return Scaffold(
appBar: AppBar(title: Text('Provider')),
body: Center(
child: Text(value),
),
);
}
}
这里储存“Hello World!” 使用的是Provider
,它提供一个永远不变的对象。不过大部分场景下状态都是可变的,下面用计数器来举例。
StateProvider
在“Hello World”的基础上,做两点修改即可。
定义一个全局常量StateProvider
。
final StateProvider<int> counterProvider = StateProvider((_) => 0);
class StateProviderExample extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text('StateProvider'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Consumer(
builder: (context, ref, _) {
int count = ref.watch(stateProvider);
return Text(
'$count',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(stateProvider.notifier).state++,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
ChangeNotifierProvider
这部分没啥说的,注意ChangeNotifier
需要自己调用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 ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text('ChangeNotifierProvider'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Consumer(
builder: (context, ref, _) {
int count = ref.watch(changeNotifierProvider).count;
return Text(
'$count',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(changeNotifierProvider).increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
FutureProvider
final FutureProvider<String> futureProvider = FutureProvider((_) async {
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'),
),
body: Center(
child: Consumer(
builder: (context, ref, _) {
AsyncValue<String> futureProviderValue = ref.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
。用法大同小异,有兴趣的可以查看我的示例代码。
ProviderListener
如果你希望在Widget Tree上监听provider的状态变化,可以使用listen
。用上面的计数器例子,当计数器为5时,触发监听。
final StateProvider<int> stateProvider = StateProvider((_) => 0);
class ProviderListenerExample extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(stateProvider, (previous, next) {
if (next == 5) {
print('当前计数器为5,触发监听。');
}
});
...
}
}
ProviderScope
一般我们在实现一个列表的Item时,需要传入相应的index大致如下:
ListView.builder(
itemCount: 50,
itemBuilder: (context, index) {
return ProductItem(index: index);
},
)
如果使用 ProviderScope
,就可以覆盖index的值,内部通过ref.watch
获取,就不必从构造方法接收它。使用起来很简单,直接上代码:
final Provider<int> currentProductIndex = Provider<int>((_) => 0);
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: [
currentProductIndex.overrideWithValue(index),
],
child: const ProductItem(),
);
},
),
);
}
}
class ProductItem extends ConsumerWidget {
const ProductItem({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final index = ref.watch(currentProductIndex);
return ListTile(title: Text('item $index'));
}
}
4.修饰符
family
family
的作用是可以在获取provider时可以添加一个参数。直接上例子,一看便知:
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, ref, _) {
final String weather = ref.watch(_weatherProvider('London'));
return Text('$weather',);
},
),
),
);
}
}
注意: 使用family
时传入的参数是有限制的。比如bool
、 int
、 double
、 String
、常量或是重写了==
和hashCode
的不可变对象。
family
的目的是根据外部参数获取对应的provider。例如接口传参:
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
return dio.get('http://my_api.dev/messages/$id');
});
autoDispose
前面我们的例子中,创建的provider因为保存在Widget Tree的根部。所以即使页面关闭,再次进入页面时会获取之前的状态。
这显然是有局限性的,那么这里就可以使用autoDispose
,它可以在我们不再使用provider时,自动将其销毁。那么合理的使用它可以避免内存泄漏。
比如之前的计数器例子,只需加一个autoDispose
就可以避免此类问题。
final stateProvider = StateProvider.autoDispose((_) => 0);
如果你需要自定义dispose事件,可以使用onDispose
。比如你的provider中有网络请求(使用Dio):
final myProvider = FutureProvider.autoDispose((ref) async {
final cancelToken = CancelToken();
ref.onDispose(() => cancelToken.cancel());
final response = await dio.get('path', cancelToken: cancelToken);
ref.keepAlive();
return response;
});
上面代码中出现了keepAlive
方法。如果用户离开页面并且请求失败,下次则将再次执行该请求。但是,如果请求成功完成调用了keepAlivee
,则将保留状态,下次重新进入页面时不会触发新的请求。
使用autoDispose
可以达到限制provider是全局还是局部作用。这样一来,可以更方便的解决跨页面使用provider的问题。
5.进阶使用
Combining providers
1.如果创建的provider需要另一个provider的状态,这时就需要使用Ref
的read
方法。
下面的示例是,给予城市和国家的provider,当创建locationProvider时,获取城市和国家的状态。
final Provider<String> cityProvider = Provider<String>((ref) => 'London');
final Provider<String> countryProvider = Provider<String>((ref) => 'England');
final Provider<Location> locationProvider = Provider<Location>((ref) => Location(ref));
class Location {
Location(this._ref);
final Ref _ref;
String get label {
final city = _ref.read(cityProvider);
final country = _ref.read(countryProvider);
return '$city ($country)';
}
}
使用Riverpod
就可以提供多个相同类型的Provider,这也是相比Provider
框架的一个优点。
2.如果获取的状态值会发生变化,我们需要监听它。可以使用Ref
的watch
方法。
下面的示例是,当cityProvider
变化时,weatherProvider
也相应变化。
final StateProvider<String> cityProvider = StateProvider((ref) => 'London');
final StateProvider<String> weatherProvider = StateProvider((ref) {
final String city = ref.watch(cityProvider).state;
return '$city (Sunny)';
});
class CombiningProviderExample2 extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: Text('CombiningProvider')),
body: Center(
child: Consumer(
builder: (context, ref, _) {
final String weather = ref.watch(weatherProvider);
return Text('$weather',);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
String city = ref.read(cityProvider.notifier).state;
if (city == 'London') {
ref.read(cityProvider.notifier).state = "Xi'an";
} else {
ref.read(cityProvider.notifier).state = 'London';
}
},
tooltip: 'Refresh',
child: Icon(Icons.refresh),
),
);
}
}
refresh
强制provider立即刷新,重新返回创建的值。这种适合列表下拉刷新,或者请求数据错误时重试。
final FutureProvider<List<String>> productsProvider = FutureProvider((_) async {
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, ref, _) {
AsyncValue<List<String>> productsProviderValue = ref.watch(productsProvider);
return productsProviderValue.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Oops, something unexpected happened'),
data: (list) => RefreshIndicator(
onRefresh: () => ref.refresh(productsProvider.future),
child: ListView(
children: [
for (final item in list) ListTile(title: Text(item)),
],
),
),
);
},
),
),
);
}
}
select
当状态中某一个值发生变化时,相应Consumer
下的builder
就会执行,重建widget。如果使用select
可以指定某一值更改时进行刷新,精准控制刷新范围,避免不必要的rebuild。
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 ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text('Select Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Consumer(
builder: (_, ref, __) {
String name = ref.watch(personProvider.select((p) => p.name));
return Text(
'name:$name',
);
},
),
Consumer(
builder: (_, ref, __) {
int age = ref.watch(personProvider.select((p) => p.age));
return Text(
'age:$age',
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(personProvider).age = Random.secure().nextInt(100),
tooltip: 'Refresh',
child: Icon(Icons.refresh),
),
);
}
}
补充
- 上面的例子中,我们为了获取
ref
使用了ConsumerWidget
。如果你使用的是StatelessWidget
可以直接替换为ConsumerWidget
。如果你需要使用StatefulWidget
里面的一些方法(比如initState
)。可以使用ConsumerStatefulWidget + ConsumerState
替代。
class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}): super(key: key);
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
ref.read(counterProvider);
}
@override
Widget build(BuildContext context) {
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
- 作者建议:最好使用
ref.watch
而不是ref.read
或ref.listen
来实现功能。通过依赖ref.watch
,您的应用程序变得既被动又具有声明性,这使其更具可维护性。
ref.read
方法是无需监听即可获得provider状态的方法,通常用于执行方法。例如上面的例子中当点击按钮时,计数加一。如果这里你使用ref.watch
获取provider,整个widget都会刷新。
其他场景建议都使用ref.watch
获取状态,即使目前可能不会更新(有时你可能因为优化性能的考虑使用read),但不能保证未来它不会变,一旦未来需要更改,修改为watch的过程中你可能会忘记一些情况,导致出错。为了更具可维护性,所以建议最好使用ref.watch
。这样重构时遇到的问题会更少。
- 对于上面的各种provider,有时很难理解何时使用这种provider类型而不是另一种provider类型。可以参考下面的表格:
类型 | 创建方法 | 使用场景 |
---|
Provider | 返回任何类型 | 单例/计算属性 |
StateProvider | 返回任何类型 | 过滤器条件/简单状态对象 |
FutureProvider | 返回任何类型的Future | API调用的结果 |
StreamProvider | 返回任何类型的Stream | 来自API的结果流 |
StateNotifierProvider | 返回StateNotifier的子类 | 复杂的状态对象,它是不可变的,除非通过接口 |
ChangeNotifierProvider | 返回ChangeNotifier的子类 | 需要可变性的复杂状态对象 |
个人认为Riverpod
是相对更轻松便捷的一种状态管理方式,待它稳定时应该能被更多的人喜爱。
以上这段话是我两年前的文中说到的,两年后的今天我的观点还是没变。或者说Riverpod
是我更推荐的一种状态管理方式。别问我getx,我个人不喜欢把鸡蛋放到一个篮子里。
以上有关Riverpod
的相关示例代码我已经上传至Github,有兴趣的可以看看,后面我也会持续维护。
参考
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)