Riverpod学习笔记

1,319 阅读3分钟

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被销毁,会触发refonDispose回调,可以在回调里执行一些操作,比如请求发到一半结果页面退出了,那么可以在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类型的对象. 它有两个作用:

  1. 负责与其它的Provider交互,
  2. 以及与应用的生命周期交互,根据生命周期来管理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;
  },
);

而且cityProvidercityNotifierProvider,在ref操作时是一样的写法,但是大大简化了使用过程:

final result2 = ref.read(cityProvider("成都").notifier);
result2.state = "重庆";

需要注意的是,result2StateController<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.