Riverpod之family(七)

989 阅读6分钟

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.typicode.com/posts/1

jsonplaceholder.typicode.com/posts/2

上面的接口来自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),
      ),
    );
  }
}

使用起来还是很简单的,效果如下

provider_family_loading.gif

如果要刷新数据,同样需要指定参数

 widgetRef.refresh(postsFamily(1));
 widgetRef.refresh(postsFamily(2));    

provider-family-refresh.gif

那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;
  }