Flutter Riverpod v2中的Provider带参数初始化
问题简述
当携带参数跳转至一个页面时,目标页面往往需要根据参数来决定具体的内容展示。比如用传来的productId
去查询当前商品的详细信息(像url = /product/:productId)。
一种很直觉的做法,是把页面设置成StatefulWidget
,然后在initState
里,去根据参数初始化所需状态对应的provider
,例如:
initState(){
super.initState();
ref.read(productProvider.notifier).init(productId);
}
但这样 riverpod 会报错:尝试在 widget 的生命周期里改变 provider 的值。理由是:两个 widget 监听一个 provider 可能会出现两个 widget 获取状态不一致的情况。
解决方案
一、Future 延迟初始化路由参数
Future 延迟执行改变状态,这也是官方对于上述错误给出的解法之一。为了防止阻塞 UI,一些简单快速的操作可以采用此方法。 This will perform your update after the widget tree is done building。
initState(){
super.initState();
Future.delayed(Duration.zero,(){
//do something
ref.read(productIdStateProvider.notifier).init(productId);
});
}
我们可以把路由参数设置成 provider,以简述中的用路由参数productId
去查询商品的详细信息为例子:
// url = /product/:productId
@riverpod
class ProductIdState extend _$ProductIdState{
int? build() => null;
void init(int productId) => state = productId;
}
//其他的provider可以直接调用
@riverpod
Future<Product> productState(productStateRef ref) async{
final id = ref.watch(productIdStateProvider);
return http.getProduct(id);
}
不推荐这个方法,原因是每个路由参数都要写一个 XXnotifierProvider
很麻烦。绕过去绕过来也挺反直觉的。但是我看官方第三方示例中有个天气项目是这么写的。
二、Provider .family 修饰符
.family
可以让 provider 根据参数建立初始状态,用上了代码生成器后,直接给 provider 的 build()
方法添加参数即可,比如:
@riverpod
class ProductState extend _$ProductIdState{
Future<Product>? build(int? productId){
if(productId == null) return null;
return http.getProduct(productId);
};
}
//用provider的时候传参
final productState = ref.watch(productStateProvider(productId));
当两个 widget 同时需要该 provider 时,它们提供的参数一致(利用 hashcode 和双等号比较)就能共用数据而不需要再次发出请求。因此,参数必须是不可变变量。
这种做法如果把页面各部分分离成组件的话,会要求每个组件都要有所需的路由参数,这会导致数据重复存储并显著增加 widget 构造函数中的参数数量。
如果不分离整个页面写一起,可以共用路由参数,但至少也要分离成函数,防止组件嵌套过多。另外需要用Consumer
和.select or .selectAsync
手动规定刷新范围来防止过度刷新。
三、子作用域 override
利用 override 把路由参数初始化到一个公共 provider 中,思路跟第一个很像,但更为简洁。APP 的数据库实例初始化也基本采用此方案。
@riverpod
int? productIdState(productStateRef ref) => throw UnimplementedError();
//in builder
return ProviderScope(
overrides: [
productIdStateProvider.overrideWithValue(productId),
],
child: XXX,//...
);
如果有其他provider会使用到在子作用域被override过的provider,那么需要显式地添加依赖,在上述例子中我们可以使用@Riverpod(dependencies: [productIdState])
注解,在 watchproductIdStateProvider
的provider上替换掉@riverpod
(注意riverpod首字母是大写的)。
作用域的其他缺点在官方文档里说的很清楚。
四、作用域+family
胜在灵活,ProductStateInterfaceProvider
甚至可以在其他页面复用。
注意本节代码未验证,仅当作伪代码参考。
@riverpod
Future<Product>? productState(ProductStateRef ref) => throw UnimplementedError();
@riverpod
Future<Product>? productStateInterface(ProductStateInterfaceRef ref)=>
productId == null ? null: http.getProduct(productId);
// in builder
return ProviderScope(
overrides: [
productStateProvider.overrideWith(
(ref)=>ref.watch(productStateInterfaceProvider(productId))
),
],
child: XXX,//...
);
总结
推荐方案三,不够用了再上方案四。
文章不足之处请海涵。
转载自:https://juejin.cn/post/7217435047488569401