Riverpod之select(六)

663 阅读4分钟

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里面的状态值是一个复杂对象,里面一个字段的变动都可能引起页面重建,而你关心的那个属性并没有变化,这样页面就无效重建了,这个时候就得select方法上场了

select方法是Provider基类的一个方法,之前说过的各种Provider都能用上,还是以StateNotifierProvider为例演示一下用法

Demo

User对象还是用之前的,如果字段也有对象,同样需要复写hashCode和==方法

class User {
 final int age;
 final String displayName;

 User({
   required this.displayName,
   required this.age,
 });

 User copyWith({
   String? displayName,
   int? age,
 }) {
   return User(
       displayName: displayName ?? this.displayName, age: age ?? this.age);
 }

 @override
 bool operator ==(Object other) {
   return identical(this, other) ||
       (other is User &&
           runtimeType == other.runtimeType &&
           (identical(displayName, other.displayName) ||
               displayName == other.displayName) &&
           (identical(age, other.age) || age == other.age));
 }

 @override
 int get hashCode => Object.hash(runtimeType, displayName, age);

 @override
 String toString() {
   return 'User{age: $age, displayName: $displayName}';
 }
}

关于provider的使用,改动不大,主要是select方法的使用,它提供一个函数将对象的某个属性提取出来,这个函数和map的作用差不多

ref.watch(userProvider.select((value) => value.displayName))

final userProvider = StateNotifierProvider<UserController, User>(
   (ref) => UserController(User(displayName: 'unknown', age: 0)));

class UserController extends StateNotifier<User> {
 UserController(super.state);

 void update({String? name, int? age}) {
   state = state.copyWith(displayName: name, age: age);
 }

 @override
 bool updateShouldNotify(User old, User current) {
   // 这里返回的不是相等,而是不相等
   debugPrint("old=$old current=$current");
   return !(old == current);
 }
}

class SelectApp extends StatelessWidget {
 const SelectApp({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 ref) {
   return Scaffold(
     appBar: AppBar(title: const Text('example')),
     body: Center(
       child: Consumer(builder: (context, ref, _) {
         debugPrint("build Consumer");
         只关心名字
         final name =
             ref.watch(userProvider.select((value) => value.displayName));
         return Text("name is $name");
       }),
     ),
     floatingActionButton: FloatingActionButton(
       onPressed: () => ref
           .read(userProvider.notifier)
           .update(name: "Tom", age: random.nextInt(50)), 名字没有变,年龄使用随机数
       child: const Icon(Icons.add),
     ),
   );
 }
}

Random random = Random();

当不停的点击按钮,updateShouldNotify方法打印User前后的信息,名字没变,年龄一直在变动,说明这个对象是一直变动的,按之前的写法,Consumer会重建,但使用select后,我们关心的名字除了第一次以后都没有变动,Consumer就不会重建。那select方法是如何实现的呢?

select内部流程

select方法就是将Provider和selector函数包装成了_ProviderSelector对象,这样在watch、listen的时候走的就是另一个分支了

  AlwaysAliveProviderListenable<Selected> select<Selected>(
    Selected Function(State value) selector,
  ) {
    return _AlwaysAliveProviderSelector<State, Selected>(
      provider: this,
      selector: selector,
    );
  }
}

class _AlwaysAliveProviderSelector<Input, Output> = _ProviderSelector<Input,
    Output> with AlwaysAliveProviderListenable<Output>;

在之前的流程分析中,为了简便,一直都是直接忽略了这个分支,下面是WidgetRef类型的,watch和listen走的都是这个。Ref类型的有所不同,最后走的是_elementListen,但殊途同归。

  ProviderSubscription<State> listen<State>(
    ProviderListenable<State> provider,
    void Function(State? previous, State next) listener, {
    bool fireImmediately = false,
    void Function(Object error, StackTrace stackTrace)? onError,
  }) {
    如果provider是selector类型,走这个分支
    if (provider is _ProviderSelector<Object?, State>) {
      return provider.listen(
        this,
        listener,
        fireImmediately: fireImmediately,
        onError: onError,
      );
    }

    ...
  }
  

在_ProviderSelector的listen方法中,有两个重点,一是_select方法

var lastSelectedValue = _select(selectedElement.getState()!);

它将Provider中的状态值按我们提供的selector函数进行转换,比如案例中将User转换成String

  Result<Output> _select(Result<Input> value) {
    try {
      return value.map(
        data: (data) => Result.data(selector(data.state)),
        error: (error) => Result.error(error.error, error.stackTrace),
      );
    } catch (err, stack) {
      // TODO test
      return Result.error(err, stack);
    } finally {
     ...
    }
  }

还有一个就是_selectOnChange函数,用来比较_select方法处理后的前后值,判断是否需要更新,比如name前后没变化就不会往下走,注意这里使用的是!=,比较的对象需要复写hashCode和==,否则默认比较的是地址,很可能不相等(看const修饰符)

为啥是很可能,比如我们的User,如果没有复写hashCode和==,那么const User(tom,10)和另一个const User(tom,10)是相等的,但const User(tom,10)和另一个User(tom,10)就实会相等。

 void _selectOnChange({
    required Input newState,
    required Result<Output> lastSelectedValue,
    required void Function(Object error, StackTrace stackTrace) onError,
    required void Function(Output? prev, Output next) listener,
    required void Function(Result<Output> newState) onChange,
  }) {
    final newSelectedValue = _select(Result.data(newState));
    判断是否需要通知更新
    if (!lastSelectedValue.hasState ||
        !newSelectedValue.hasState ||
        lastSelectedValue.requireState != newSelectedValue.requireState) {
     onChange(newSelectedValue);

      newSelectedValue.map(
        data: (data) {
          listener(
            lastSelectedValue.stateOrNull,
            data.state,
          );
        },
        error: (error) => onError(error.error, error.stackTrace),
      );
    }
  }

其他流程和之前还是类似的,不再赘述。