likes
comments
collection
share

Flutter Riverpod v2中的Provider带参数初始化

作者站长头像
站长
· 阅读数 15

问题简述

当携带参数跳转至一个页面时,目标页面往往需要根据参数来决定具体的内容展示。比如用传来的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,//...
);

总结

推荐方案三,不够用了再上方案四。

文章不足之处请海涵。