Riverpod相比于之前的Provider库,是可以独立存在独立使用的,不依赖于BuildContext,以前需要使用单例或者EventBus来实现的功能,现在统一可以使用Riverpod.
安装
将下面代码添加到pubspec.yaml文件:
flutter_riverpod: ^1.0.4
使用
ProviderScope 作用域
ProviderScope 实际上是一个widget,用来存储项目里面用到的Provider的state,如果不添加 Provider机制无法工作.
void main() async {
runApp(const ProviderScope(
child: MyApp(),
));
}
Provider 提供者
Provider 将一段状态封装成一个对象,并允许监听该状态. Provider通常是一个全局的常量.比如
final cityProvider = Provider((ref) => 'London');
如何创建
用本地存储器SharedPreferences 为例, 创建一个sharedPreferencesProvider:
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError();
});
此时默认是抛出一个未实现的错误的,因为没有创建具体的SharedPreferences对象. 再创建一个本地存储的工具类SharedUtility:
class SharedUtility {
SharedUtility({
required this.sharedPreferences,
});
///具体使用的SharedPreferences对象
final SharedPreferences sharedPreferences;
/// 读取暗黑模式
bool isDarkModeEnabled() {
return sharedPreferences.getBool(sharedDarkModeKey) ?? false;
}
/// 保存暗黑模式
void setDarkModeEnabled(bool value) {
sharedPreferences.setBool(sharedDarkModeKey, value);
}
}
SharedUtility工具包含一个全局的 sharedUtilityProvider,来为SharedUtility指定sharedPreferences对象:
final sharedUtilityProvider = Provider<SharedUtility>((ref) {
final _sharedPrefs = ref.watch(sharedPreferencesProvider);
return SharedUtility(sharedPreferences: _sharedPrefs);
});
sharedUtilityProvider只做了一件事:观察sharedPreferencesProvider对象,当它包含的SharedPreferences发生变化时,return一个新的SharedUtility对象.
需要注意的是: Provider里面,不能使用
ref.read来操作另一个provider
那么sharedPreferencesProvider是从什么时候变的呢?
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
final storage = LocalStorage();
runApp(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
storageProvider.overrideWithValue(storage),
],
child: const MyApp(),
),
);
}
此处代码:
sharedPreferencesProvider.overrideWithValue(sharedPreferences)
就是用sharedPreferences对象覆盖掉了默认的UnimplementedError,所以覆盖之后,sharedUtilityProvider可以正常运作了.
修饰词
.family
普通的Provider是无法传入参数来决定输出结果的,如果想要实现,只能通过 ref来操作另一个Provider获取值,如果想要传入一个参数,就需要用到family.比如使用FutureProvider来创建一个请求获取请求结果,请求的入参是可变的:
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
return dio.get('http://my_api.dev/messages/$id');
});
使用了family修饰词之后,再通过ref来操作它时,需要传入指定类型的参数:
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(messagesFamily('id'));
}
使用family接收的参数,必须是equatable或者hashable的:
class MyParameter extends Equatable {
MyParameter({
required this.userId,
required this.locale,
});
final int userId;
final Locale locale;
@override
List<Object> get props => [userId, locale];
}
final exampleProvider = Provider.family<Something, MyParameter>((ref, myParameter) {
print(myParameter.userId);
print(myParameter.locale);
// Do something with userId/locale
});
.autoDispose
使用autoDispose修饰词后,provider将在不再使用时,销毁state:
final myProvider = FutureProvider.autoDispose((ref) async {
final response = await httpClient.get(...);
return response;
});
上面的例子,myProvider被使用时,将会发起请求获取到数据并更新state,在未被销毁前,都能通过state获取到数据,但是一旦state被销毁,下次再使用时,就需要重新发起请求获取数据.
ref.keepAlive 状态保活
当使用了 autoDispose 修饰Provider之后,也可以使用ref.keepAlive()让它不自动销毁state:
final myProvider = FutureProvider.autoDispose((ref) async {
final response = await httpClient.get(...);
ref.keepAlive();
return response;
});
这样适用于有些请求数据不会经常改变的情况,只需要请求一次,数据就会一直保存着,达到类似单例的效果.
ref.onDispose 销毁回调
当使用了 autoDispose 修饰Provider之后,如果state被销毁,会触发ref的onDispose回调,可以在回调里执行一些操作,比如请求发到一半结果页面退出了,那么可以在onDispose里面取消请求:
final myProvider = FutureProvider.autoDispose((ref) async {
final cancelToken = CancelToken();
// 当state被自动销毁时,取消正在发起的请求
ref.onDispose(() => cancelToken.cancel());
final response = await dio.get('path', cancelToken: cancelToken);
// 如果请求成功,那么保留数据不被销毁,此时state不会再被销毁
ref.keepAlive();
return response;
});
修饰词使用的注意事项:
可以对Provider同时使用两个修饰词
final userProvider = StreamProvider.autoDispose.family<User, String>((ref, id) {
});
autoDispose会改变Provider的类型
Provider默认继承自AlwaysAliveProviderBase
class Provider<State> extends AlwaysAliveProviderBase<State>
但是使用了autoDispose修饰词后,会使用AutoDisposeProviderBuilder来创建Provider
static const family = ProviderFamilyBuilder();
/// {@macro riverpod.autoDispose}
static const autoDispose = AutoDisposeProviderBuilder();
那么创建出来的,结果就指向了AutoDisposeProvider<State>类型,它是继承自AutoDisposeProviderBase<State>的,和默认的AlwaysAliveProviderBase<State>不一样,它们各自的ref类型也不一样.
AutoDisposeProvider对应的ref是AutoDisposeRef, 它的watch方法接收的参数是ProviderListenable,而AlwaysAliveProviderBase对应的ProviderRef,watch的入参类型被限定了AlwaysAliveProviderListenable, 所以
Provider无法对使用autoDispose修饰词的Provider使用watch,但是后者可以对前者使用watch
final firstProvider = Provider.autoDispose((ref) => 0);
final secondProvider = Provider((ref) {
ref.watch(firstProvider);
});
这样写会直接编译失败:
The argument type 'AutoDisposeProvider<int>' can't be assigned to the parameter type 'AlwaysAliveProviderBase<Object, Null>'
但是反过来写就不会报错:
final firstProvider = Provider((ref) => 0);
final secondProvider = Provider.autoDispose((ref) {
ref.watch(firstProvider);
});
如何使用
Provider全局常量的创建,是无限制的,可以包含任意类型.而且根据上面的创建例子可以看出,各个Provider之间是可以联动的, 只要有 ref这个参数的地方,就可以使用watch等方法来操作Provider.
ref是什么?
ref是一个Ref类型的对象. 它有两个作用:
- 负责与其它的Provider交互,
- 以及与应用的生命周期交互,根据生命周期来管理watch等方法.
不同类型的Provider的子类,使用不同类型的Ref子类
typedef Create<T, R extends Ref> = T Function(R ref);
比如ChangeNotifierProvider创建时,使用ChangeNotifierProviderRef,因为需要监听变化,ChangeNotifierProvider只支持传入ChangeNotifier类型的参数:
/// 创建是否是暗黑模式的Provider对象
final isDarkProvider = ChangeNotifierProvider<DarkThemeNotifier>((ref) {
return DarkThemeNotifier(ref);
});
class DarkThemeNotifier extends ChangeNotifier {
DarkThemeNotifier(this.ref);
Ref ref;
bool getTheme() {
return ref.watch(sharedUtilityProvider).isDarkModeEnabled();
}
void toggleTheme() {
ref.watch(sharedUtilityProvider).setDarkModeEnabled(
!ref.watch(sharedUtilityProvider).isDarkModeEnabled(),
);
notifyListeners();
}
}
Ref有哪些操作呢?
ref.watch读取并监听后续变化.适用于一直关注最新值的场景ref.read只读取当前值,不监听后续变化,适用于只在乎当前值的场景,比如按钮点击时的瞬时操作ref.listen添加监听, 当provider发生变化时,执行某个操作,适用于在乎值的变化情况的场景.
可以根据需要来选择不同的操作.
ref还有一些精细化操作,比如只观察某个对象的特定属性select:
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
对Provider的操作,离不开Ref, 虽然摆脱了BuildContext的限制,但是又需要Ref,那么怎么在Widget里面使用呢?
Consumer
Riverpod 提供了一套widget, 重写了build方法,将ref参数传入了.我们在widget里面使用Provider时,可以直接使用其封装的Widget组件:
ConsumerStatefulWidget
如果需要使用state,那么可以使用ConsumerStatefulWidget,与StatefulWidget类似
ConsumerWidget
如果不需要使用state,那么可以直接使用ConsumerWidget,与StatelessWidget类似,但是它依然继承自StatefulWidget, 因为它需要使用WidgetRef来管理生命周期.
Consumer
类似于小部件Widget, 实际上也是继承自ConsumerWidget.
有时候会有一些特殊情况,比如我在dio请求时,可能没有使用Provider进行封装,如果请求返回了特定错误码,就需要跳转到登录页面,这涉及到的问题是:
如何在没有ref参数环境的情况下,修改Provider的值,从而触发在其它Widget里面的watch或者listen?
没有ref的情况下,无法用ref来操作Provider,而Provider又只是一个常量,是无法进行操作的. 要实现全局广播的效果,可以使用ChangeNotifier.
ChangeNotifier
ChangeNotifier是一个可被监听的对象,可以像创建Provider常量一样创建ChangeNotifier,一般如果我们关注某个值的变化,就直接使用ValueNotifier:
final routerNotifer = ValueNotifier<String>("");
比如我创建一个全局的routerNotifer常量,来监听传入的路由string,然后根据string跳转不同的页面:
@override
void initState() {
super.initState();
routerNotifer.addListener(() {
final str = routerNotifer.value;
print(str);
Navigator.of(context)
.push(...);
});
}
需要注意的是: 为了避免内存泄露,最好在disposed时,remove掉这个listener. 或者直接在全局只创建一次而且不会销毁的地方使用.
后续修改routerNotifer的值时,会触发listener回调:
routerNotifer.value = "login";
需要注意的是: 多次调用
routerNotifer.value赋值同一个值时,只会触发一次回调,只有与上次的value不相同时,才会触发回调.
ChangeNotifier 一般创建子类来使用,而且要使用notifyListeners()方法来触发添加的listener:
final isDarkProvider = ChangeNotifierProvider<DarkThemeNotifier>((ref) {
return DarkThemeNotifier(ref);
});
class DarkThemeNotifier extends ChangeNotifier {
DarkThemeNotifier(this.ref);
Ref ref;
bool getTheme() {
return ref.watch(sharedUtilityProvider).isDarkModeEnabled();
}
void toggleTheme() {
ref.watch(sharedUtilityProvider).setDarkModeEnabled(
!ref.watch(sharedUtilityProvider).isDarkModeEnabled(),
);
notifyListeners();
}
}
这样可以在ref操作isDarkProvider的同时能够修改本身的值.
问题: 如果直接使用Provider和普通的Class,能够达到这样的效果吗?
StateNotifier<T> 必须创建子类才能使用,因为它是abstract类型:
class City extends StateNotifier<String> {
City(String state) : super(state);
}
创建好的City可以像 ValueNotifier一样单独使用:
final city = City("");
...
city.addListener(
(state) {
//TODO something
},
);
也可以结合Provider使用,StateNotifierProvider就需要传入StateNotifier类型:
final cityNotifierProvider = StateNotifierProvider.family<City, String, String>(
(ref, arg) {
print("输入参数 $arg");
return City(arg);
},
);
cityNotifierProvider里面虽然返回的是City类型,实际使用read等取值时,获取到的还是最终的State的值:
final result = ref.read(cityNotifierProvider("成都")); // result = "成都"
而这样取出的值,是无法修改的,如果需要修改,那么需要加上.notifier取到外层的City
final result = ref.read(cityNotifierProvider("成都").notifier); // result = City("成都")
result.state = "大成都"; //_state是私有变量,这样写会有警告
这样就能修改掉cityNotifierProvider之前的值. 对于这个流程Provider提供了一个StateProvider简化了操作,可以不用创建City类型:
final cityProvider = StateProvider.family<String, String>(
(ref, arg) {
print("输入参数 $arg");
return arg;
},
);
而且cityProvider和cityNotifierProvider,在ref操作时是一样的写法,但是大大简化了使用过程:
final result2 = ref.read(cityProvider("成都").notifier);
result2.state = "重庆";
需要注意的是,
result2是StateController<String>类型,StateController<T>是继承自StateNotifier<T>的.
选择不同种类的Provider
Provider
最基础的Provider,可以缓存同步操作,作用有限,无法被ref修改
FutureProvider
相比基础的Provider, FutureProvider可以执行异步操作,FutureProvider通常用于:
- 执行和缓存异步操作(例如网络请求)
- 很好地处理异步操作的错误/加载状态
- 将多个异步值组合成另一个值
FutureProvider使用async/await语法:
final myProvider = FutureProvider.autoDispose((ref) async {
final cancelToken = CancelToken();
// 当state被自动销毁时,取消正在发起的请求
ref.onDispose(() => cancelToken.cancel());
final response = await Dio().get('path', cancelToken: cancelToken);
// 如果请求成功,那么保留数据不被销毁,此时state不会再被销毁
return response;
});
里面可以处理多个异步操作.并给出返回值,需要注意的是,它返回的是AsyncValue<T>类型, 可以处理错误和加载状态:
Widget build(BuildContext context, WidgetRef ref) {
final res = ref.read(myProvider); // res 是AsyncValue<Response<dynamic>>类型
return config.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (data) {
return Text(data.toString());
},
);
}
注意上面的处理方法, 它可以异步指定请求中的页面,请求失败的页面,请求成功的页面,然后根据结果自动切换,很方便实现骨架图,缺省页等功能.
ChangeNotifierProvider
监听不可变对象时可以使用这个,配合ChangeNotifier使用。在调用notifyListeners()时,会触发watch。
StateNotifierProvider 和 StateProvider
监听可变对象时使用,两者的区别上文有介绍。StateProvider更简便,不需要创建StateNotifier.