在构建 Flutter 应用时,性能优化的核心往往在于减少不必要的 Widget 重绘。默认情况下,Riverpod 的 ref.watch 会监听整个状态对象的变化。如果一个 Widget 只依赖对象中的某个字段,但该对象的其他字段发生了变化,Widget 依然会被重建。

Riverpod 提供了强大的 .select() 方法,允许我们对监听的数据进行“过滤”,仅在关注的数据发生变化时才触发重绘。


1. 为什么需要 Select?

假设我们有一个包含用户详细信息的 Provider(包含姓名、年龄、地址等)。

  • 默认行为:如果你只在 UI 上显示“姓名”,但后台更新了用户的“年龄”,使用 ref.watch(userProvider) 的 Widget 依然会重新构建。
  • 优化行为:使用 select 明确告诉 Riverpod:“我只关心‘姓名’,别的事情不要打扰我。”

2. 基础用法:同步数据的筛选

首先,我们定义一个简单的数据模型和一个使用 riverpod_generator 生成的 Provider。

2.1 定义数据模型与 Provider

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'user_provider.g.dart';

class User {
  final String firstName;
  final String lastName;
  final int age;

  User({required this.firstName, required this.lastName, required this.age});

  // 为了演示,通常建议实现 copyWith 方法
  User copyWith({String? firstName, String? lastName, int? age}) {
    return User(
      firstName: firstName ?? this.firstName,
      lastName: lastName ?? this.lastName,
      age: age ?? this.age,
    );
  }
}

// 使用 riverpod_generator 定义 Provider
@riverpod
class UserNotifier extends _$UserNotifier {
  @override
  User build() {
    return User(firstName: 'John', lastName: 'Doe', age: 25);
  }

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

2.2 在 Widget 中使用 Select

ConsumerWidget 中,我们可以使用 select 来仅监听 firstName

class UserGreeting extends ConsumerWidget {
  const UserGreeting({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 关键点:使用 select 提取 firstName
    // 只有当 userNotifierProvider 中的 firstName 发生变化时,
    // build 方法才会被再次调用。
    // 如果 age 发生变化,这里不会触发重绘。
    final firstName = ref.watch(
      userNotifierProvider.select((user) => user.firstName),
    );

    return Text('Hello, $firstName');
  }
}

对比:

  • 不使用 Select: final user = ref.watch(userNotifierProvider); -> 任何属性变动都会触发重绘。
  • 使用 Select: final name = ref.watch(userNotifierProvider.select(...)); -> 仅选中属性变动触发重绘。

3. 处理异步数据:selectAsync

当你需要在一个 Provider 中监听另一个异步 Provider 的特定部分时,普通的 select 返回的是 AsyncValue。如果你希望直接等待数据就绪并获取其中的某个字段,可以使用 selectAsync

这在组合 Provider 时非常有用。

示例:监听异步用户的某个字段

@riverpod
Future<User> fetchUser(Ref ref) async {
  // 模拟网络请求
  await Future.delayed(const Duration(seconds: 1));
  return User(firstName: 'Jane', lastName: 'Smith', age: 30);
}

@riverpod
Future<String> derivedMessage(Ref ref) async {
  // 我们只关心 fetchUserProvider 返回的 firstName。
  // selectAsync 会等待 Future 完成,并直接返回筛选后的数据。
  final firstName = await ref.watch(
    fetchUserProvider.selectAsync((user) => user.firstName),
  );

  return 'Welcome back, $firstName!';
}

4. 最佳实践与注意事项

为了确保正确使用 select 并避免潜在坑点,请遵循以下原则:

4.1 确保数据不可变 (Immutability)

select 的比较机制依赖于对象的不可变性。

  • 不要select 返回的集合(如 List)上直接进行修改(mutation)。
  • 如果你返回一个 List 并直接修改其内容,Riverpod 无法检测到变化,从而不会触发重绘。始终通过创建新实例来更新状态。

4.2 权衡性能开销

select 本身在读取数据时会增加微小的计算开销(用于执行选择器函数和比较结果)。

  • 不要过度优化:如果一个 Provider 的状态很少改变,或者 Widget 树非常轻量,直接 watch 整个对象通常没问题。
  • 适用场景:当 Provider 状态更新频繁,且 Widget 树较深或重绘代价较大时,使用 select 收益最高。

4.3 多次 Select

你可以在一个 build 方法中多次调用 select 来监听不同的属性:

@override
Widget build(BuildContext context, WidgetRef ref) {
  final firstName = ref.watch(userNotifierProvider.select((u) => u.firstName));
  final isAdult = ref.watch(userNotifierProvider.select((u) => u.age >= 18));

  // ...
}

总结

使用 select 是掌握 Riverpod 高级用法的关键一步。

  1. 减少重绘:通过只监听需要的数据字段,显著降低 Widget 重建频率。
  2. 语法简洁:结合 riverpod_generator,代码依然保持类型安全和简洁。
  3. 异步支持:利用 selectAsync 轻松处理异步状态的局部依赖。

在开发大型应用时,养成“按需监听”的习惯,将有助于保持应用的高性能与流畅度。