Riverpod之StateNotifierProvider(四)

1,444 阅读3分钟

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属性的使用和内部流程

StateProvider主要用于处理状态为基本类型数据的场景,像字符串、数字、枚举之类的,官方甚至说如果复杂系数超过count++这样的,就得用StateNotifierProvider(ChangeNotifierProvider是为了方便从Provider库向Riverpod过渡,状态值是可变的,并不推荐使用)。

其实它们内部流程差不多,区别在于:StateProvider帮你实现了StateController把简单场景处理了,而StateNotifierProvider需要自己实现类似的StateController应对复杂场景。

Demo

我们用User模拟一个复杂对象

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

使用StateNotifierProvider,其泛型参数比其它的Provider要复杂一点

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

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

  void updateName(String name) {
    state = state.copyWith(displayName: name);
  }

  void updateAge(int age) {
    state = state.copyWith(age: age);
  }
}

如下图,第一个泛型类型要求是StateNotifier子类,这样数据变动会通知监听者;第二个泛型类型是状态值类型,这里我们指定类型为User

image.png

在控组件中的使用和之前的StateProvider是一样的,效果是点击后页面文字更新为“Tom”

void main() {
  runApp(const ProviderScope(child: NotifierApp()));
}

class NotifierApp extends StatelessWidget {
  const NotifierApp({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 user = ref.watch(userProvider);
          return Text(user.displayName);
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(userProvider.notifier).updateName("Tom"),
        child: const Icon(Icons.add),
      ),
    );
  }
}

注意update方法,使用的copy的方式去覆盖,这是dart中常用的更新方式

  void updateName(String name) {
    state = state.copyWith(displayName: name);
  }

像下面这种方式是错误的,即使User是可变的,能这样赋值,也没有效果

  void updateName(String name) {
    state.displayName = name;
  }

因为这样只是改了属性,没有触发state的更新,就算触发了更新,StateNotifier在通知监听者之前会比较更新前后的对象,如果是同一个也不会通知

set state(T value) {
    final previousState = _state;
    _state = value;

    /// 看看两个对象的地址是不是同一个
    if (!updateShouldNotify(previousState, value)) {
      return;
    }

    for (final listenerEntry in _listeners) {
      try {
        listenerEntry.listener(value);
      } catch (error, stackTrace) {
       ...
      }
    }
  }

当点击按钮后,页面文字从“unkown”更新为“Tom”,但此后每次点击,页面文字不变,但仍然会打印“build Consumer”,说明Widget更新了,这是无效更新,因为内容都没变。

那内容一样为什么Widget会更新,因为updateShouldNotify仅仅比较的是对象的地址,我们每次都new个对象,地址肯定是不一样的

  bool updateShouldNotify(
    T old,
    T current,
  ) =>
      !identical(old, current);

如果要根据对象内容比较,就需要改造一下User,主要是复写hashCode和复写==方法:除了地址相同以外,如果两个对象的runtimeType、displayName、age一样,我们也认为对象是相同的。

这里的比较条件自由定制,但需要注意的是hashCode和==方法中的字段应该一致,要么都有age字段,要么都没有。如果有很多对象需要这样手写模版代码,考虑使用equatable,地址:pub-web.flutter-io.cn/packages/eq…

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);

}

改造之后,再复写updateShouldNotify方法,使用我们自定义的比较替代

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

  void updateName(String name) {
    state = state.copyWith(displayName: name);
  }

  void updateAge(int age) {
    state = state.copyWith(age: age);
  }

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

这样再次点击后,就不会无效更新了