Riverpod之Provider(一),使用Provider讲解了WidgetRef和Ref的watch要点
Riverpod之StateProvider(二),讲解了StateProvider的内部流程,主要涉及的是其内部的名叫state的Provider。
Riverpod之Provider&StateProvider(三),讲解了Provider和StateProvider的组合使用。
Riverpod之StateNotifierProvider(四),介绍了StateNotifierProvider的使用。
Riverpod之FutureProvider(五),介绍了FutureProvider的使用。
Riverpod之select(六),介绍了Provider的select方法使用和原理。
Riverpod之family(七),介绍了family方法得使用和内部流程
Riverpod之autoDispose(八),介绍了autoDispose方法的使用和内部流程
Riverpod之override(九),介绍了override属性的使用和内部流程
前面说的Provider基本都是单个单个的、有名有姓的,但有时候需要批量的Providers,比如在处理网络请求的时候,可能会遇到下面这种情况,Provider中的url除了一些参数不一样,其它基本雷同,要是一个一个处理可能需要CV工程师出马,那有没有更好办法?
上面的接口来自JSONPlaceholder 可以用来获取json数据
final dioProvider = Provider((ref) => Dio());
final posts1Provider = FutureProvider<String>((ref) async {
Response<String> response = await ref
.watch(dioProvider)
.get<String>("https://jsonplaceholder.typicode.com/posts/1");
if (response.data == null || response.statusCode != 200) {
throw HttpException('Http Error! ${response.statusCode}');
} else {
return response.data!;
}
});
final posts2Provider = FutureProvider<String>((ref) async {
Response<String> response = await ref
.watch(dioProvider)
.get<String>("https://jsonplaceholder.typicode.com/posts/2");
if (response.data == null || response.statusCode != 200) {
throw HttpException('Http Error! ${response.statusCode}');
} else {
return response.data!;
}
});
其实是有的,family修饰符就是用来处理这种情况的(family这个名字起的不错)
// id 为int类型参数
final postsFamily = FutureProvider.family<String,int>((ref, id) async {
Response<String> response = await ref
.watch(dioProvider)
.get<String>("https://jsonplaceholder.typicode.com/posts/$id");
if (response.data == null || response.statusCode != 200) {
throw HttpException('Http Error! ${response.statusCode}');
} else {
return response.data!;
}
});
下面通过一个Demo看如何使用family修饰符
Demo
不能直接watch(postsFamily),Family并不是一个Provider,而是一个Provider生成器,需要传入一个参数来生成一个Provider,这里我们传入的就是int类型参数
AsyncValue post1 = ref.watch(postsFamily(1));
AsyncValue post2 = ref.watch(postsFamily(2));
class FamilyApp extends StatelessWidget {
const FamilyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(home: _Home());
}
}
class _Home extends ConsumerWidget {
const _Home();
@override
Widget build(BuildContext context, WidgetRef widgetRef) {
return Scaffold(
appBar: AppBar(title: const Text('example')),
body: Column(
children: [
Consumer(builder: (context, ref, _) {
AsyncValue<String> post1 = ref.watch(postsFamily(1));
return post1.when(
data: (value) {
return Text(value);
},
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text("$err"));
}),
Consumer(builder: (context, ref, _) {
AsyncValue<String> post2 = ref.watch(postsFamily(2));
return post2.when(
data: (value) {
return Text(value);
},
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text("$err"));
}),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 刷新数据,同样需要指定参数
widgetRef.refresh(postsFamily(1));
widgetRef.refresh(postsFamily(2));
},
child: const Icon(Icons.refresh),
),
);
}
}
使用起来还是很简单的,效果如下
如果要刷新数据,同样需要指定参数
widgetRef.refresh(postsFamily(1));
widgetRef.refresh(postsFamily(2));
那family是如何凭一己之力挡住了CV工程师的起飞
family创建Provider的流程
每个Provider都有自己的family,每个family对象其实就是一个Provider Builder
Provider
static const family = ProviderFamilyBuilder();
FutureProvider
static const family = FutureProviderFamilyBuilder();
StateProvider
static const family = StateProviderFamilyBuilder();
以FutureProviderFamilyBuilder为例,里面主要是一个call方法
class FutureProviderFamilyBuilder {
const FutureProviderFamilyBuilder();
/// {@macro riverpod.family}
FutureProviderFamily<State, Arg> call<State, Arg>(
FamilyCreate<FutureOr<State>, FutureProviderRef<State>, Arg> create, {
String? name,
List<ProviderOrFamily>? dependencies,
}) {
return FutureProviderFamily(
create,
name: name,
dependencies: dependencies,
);
}
...
}
call方法是比较特殊的一个方法,在调用的时候是可以省略方法名的,直接传参数即可,所以你在代码中看不到这个方法的调用
final postsFamily = FutureProvider.family<String, int>((ref, id) async {
...
});
上面的展开应该是下面这个样子,调用call方法完成了FutureProviderFamily对象的初始化
final FutureProviderFamily<String,int> postsFamily = FutureProvider.family.call<String, int>((ref, id) async {
...
});
从demo我们知道,postFamily(1)就能生成一个Provider,这是如何做到的?
原来FutureProviderFamily继承自Family,它也有一个call方法,postFamily(1)就是postFamily.call(1),call方法调用的是下面的create方法
abstract class Family<State, Arg, FamilyProvider extends ProviderBase<State>>
extends ProviderOrFamily implements FamilyOverride<Arg> {
强调argument应该是不可变的 而且要复写`==`/`hashCode`
/// That external value should be immutable and preferably override `==`/`hashCode`.
FamilyProvider call(Arg argument) => create(argument);
使用参数创建一个provider
/// Creates the provider for a given parameter.
@protected
FamilyProvider create(Arg argument);
}
这个create方法是由子类复写的,FutureProviderFamily的如下,这里就是其创建Provider的地方,argument就是我们传的1、2参数
@override
FutureProvider<State> create(Arg argument) {
return FutureProvider<State>(
(ref) => _create(ref, argument),
name: name,
from: this,
argument: argument,
);
}
为什么参数要复写==/hashCode
call方法中强调参数要复写==/hashCode,为什么么?从上面create方法也能看出,同一个参数,create方法每次都会生成不同对象的,demo中大家可能没注意
watch(postsFamily(1)) 和 refresh(postsFamily(1))操作的对象是一样的吗
也就是下面两个对象是等价的吗?
FutureProvider first = postsFamily(1);
FutureProvider second = postsFamily(1);
如果测试一下发现这两个不同的对象确实是相等的,这也是为什么refresh操作有效的原因
test("test_provider",(){
var futureProvider = postsFamily(1);
var futureProvider2 = postsFamily(1);
assert(futureProvider == futureProvider2); // true
});
但如果遇到的参数是个对象
class Person {
final int id;
final String name;
Person({
required this.id,
required this.name,
});
}
把postsFamily参数替换成Person就是下面这样
final postsFamily = FutureProvider.family<String, Person>((ref, person) async {
Response<String> response = await ref
.watch(dioProvider)
.get<String>("https://jsonplaceholder.typicode.com/posts/${person.id}");
...
});
用对象传参后,测试的结果是不相等
test("test_provider",(){
var futureProvider = postsFamily(Person(id: 1, name: "tom"));
var futureProvider2 = postsFamily(Person(id: 1, name: "tom"));
assert(futureProvider == futureProvider2);// false
});
为什么呢?这个就需要看Provider是如何判断对象相等的,既然要判断对象是否相等,就需要复写==/hashCode,这个复写由父类ProviderBase实现,其复写的目的就是针对由family创建的provider,如果不是family创建的Provider都不会瞅一眼,主要判断两点
- from,是由哪个family创建的,不同的family创建的肯定不一样,都不用往下比了,上面的例子中都是FutureProviderFamily
- argument,比较参数是否相等,需要我们实现,上面的例子中参数Person没有复写
==/hashCode,比较的是地址,所以不相等
abstract class ProviderBase<State> extends ProviderOrFamily
implements ProviderListenable<State>, ProviderOverride {
/// If this provider was created with the `.family` modifier, [from] is the `.family` instance.
@override
final Family? from;
/// If this provider was created with the `.family` modifier, [argument] is
/// variable used.
final Object? argument;
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode {
if (from == null) return super.hashCode;
return from.hashCode ^ argument.hashCode;
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (from == null) return identical(other, this);
return other.runtimeType == runtimeType &&
other is ProviderBase<State> &&
other.from == from &&
other.argument == argument;
}
}
给Person加上==/hashCode方法后,再次测试就相等了,前面文章也说过,这个手工活多的时候还是交给Equatable 实现,但我们得了解原理
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is Person &&
runtimeType == other.runtimeType &&
(identical(name, other.name) ||
name == other.name) &&
(identical(id, other.id) || id == other.id));
}
@override
int get hashCode => Object.hash(runtimeType, id, name);
test("test_provider",(){
var futureProvider = postsFamily(Person(id: 1, name: "tom"));
var futureProvider2 = postsFamily(Person(id: 1, name: "tom"));
assert(futureProvider == futureProvider2);// true
});
为什么参数要求不可变
call方法中除了要求参数复写==/hashCode外,还要求对象是不可变的,为什么呢?先看不可变数据的测试,下面是符合预期的
Map<Person, String> programmer = {};
Person person1 = Person(id: 1, name: "tom");
Person person2 = person1.copyWith(id: 2);
programmer.putIfAbsent(person1, () => "1");
programmer.putIfAbsent(person2, () => "2");
debugPrint(programmer.toString()); //输出 {Person{id: 1, name: tom}: 1, Person{id: 2, name: tom}: 2}
assert(programmer.length == 2);// 输出 true
但如何Person是可变的,比如id字段不是final,是可以修改的
class Person {
int id;
final String name;
Person({
required this.id,
required this.name,
});
...省略`==`/`hashCode`
}
把上面测试用例改一下,同一个person对象改个字段值存两次会怎样?按理说,同一个对象第二次是存不进去的,但结果是同一个对象在map中存了两份。这种情况不会报错,但也不正确,运行起来可能符合预期也有可能不符合,程序这玩意对一半比全错还要操蛋。
Map<Person, String> programmer = {};
Person person = Person(id: 1, name: "tom");
// 存person
programmer.putIfAbsent(person, () => "1");
// 修改字段再次存person
person.id = 2;
programmer.putIfAbsent(person, () => "2");
debugPrint(programmer.toString()); 输出 {Person{id: 2, name: tom}: 1, Person{id: 2, name: tom}: 2}
assert(programmer.length == 2); 输出 true
为什么会这样呢?这和Map存数据的方式有关,常用的HashMap对key的比较是依赖==/hashCode的,字段的修改破环了==/hashCode前后一致性,导致Map认为前后存的是不同对象。所以严谨的说,参与==/hashCode的字段都要是不可变的。
之前的分析中也提到过,这些Provider是作为key存在map中的,如果不注意,误修改了参数,也会出现同样问题
Res watch<Res>(ProviderListenable<Res> target) {
return _dependencies.putIfAbsent(target, () {
final oldDependency = _oldDependencies?.remove(target);
...
}).read() as Res;
}