Flutter Riverpod 2.x 设计思想与最佳实践

0 阅读11分钟

在Flutter状态管理领域,Riverpod 2.x 无疑是当前最主流、最优雅的解决方案之一。作为 Provider 的升级版,它彻底解决了 Provider 嵌套冗余、状态作用域混乱、内存泄漏等痛点,凭借“无上下文依赖、强类型安全、自动状态管理、灵活的状态组合”等特性,成为中大型 Flutter 项目的首选状态管理方案。

很多开发者从 Provider 迁移到 Riverpod 2.x 时,容易陷入“只知其然,不知其所以然”的困境——会用基本的 Provider、StateNotifierProvider,但不懂其设计思想,也不清楚不同场景下该选择哪种 Provider、如何规避常见坑。

本文将从 Riverpod 2.x 的核心设计思想出发,结合5个高频实战案例(覆盖全局状态、页面级状态、列表状态、表单状态、状态组合),拆解其最佳实践,帮你真正吃透 Riverpod 2.x,写出简洁、高效、可维护的 Flutter 代码。

一、先搞懂:Riverpod 2.x 的核心设计思想

Riverpod 2.x 的设计思想,本质是“解耦、精准、可扩展”,它的所有特性都是围绕这三个核心展开,彻底解决了 Provider 的固有缺陷。我们先理清3个核心设计理念,再看具体用法。

1. 无上下文(Context)依赖,彻底摆脱嵌套地狱

这是 Riverpod 最核心的改进,也是它与 Provider 最本质的区别。Provider 依赖 InheritedWidget,必须通过 Context 才能获取状态,这就导致了“嵌套层级过深”的问题(比如全局状态需要嵌套在顶层,页面状态需要嵌套在页面内部)。

而 Riverpod 2.x 引入了 ProviderScope(或 ProviderContainer),将状态与 Context 完全解耦:所有 Provider 都注册在全局或局部的容器中,组件无需依赖 Context,只需通过 Provider 本身就能获取状态,彻底告别嵌套冗余。

2. 强类型安全,编译期校验,减少 runtime 异常

Riverpod 2.x 基于 Dart 的泛型特性,实现了完全的类型安全。无论是定义 Provider、获取状态,还是修改状态,都能在编译期进行类型校验,避免了 Provider 中“类型转换错误”“状态为空”等常见 runtime 异常。

比如,你定义一个返回 String 类型的 Provider,就无法在组件中将其当作 int 类型使用,编译器会直接报错,提前规避潜在问题。

3. 状态分层管理,精准控制作用域与生命周期

Riverpod 2.x 不强制所有状态全局共享,而是允许开发者根据状态的作用范围,灵活定义“全局状态”“页面级状态”“局部组件状态”,并自动管理其生命周期——页面销毁时,页面级状态自动释放,无需手动处理,从根源上避免内存泄漏。

同时,Riverpod 支持“状态组合”,可以将多个独立的状态组合成一个新的状态,无需修改原有状态逻辑,灵活性大幅提升。

4. 自动缓存与重建优化,提升应用性能

Riverpod 2.x 会自动缓存 Provider 的状态,只有当状态依赖的上游状态发生变化时,才会重新计算并通知依赖该状态的组件重建。这种“按需重建”的机制,避免了 Provider 中“修改一个状态,多个无关组件强制重建”的问题,性能更优。

二、必备基础:Riverpod 2.x 核心 API 速览(必掌握)

在看案例之前,先快速掌握 Riverpod 2.x 最常用的5个核心 API,后续案例都会基于这些 API 展开,避免看不懂代码。

1. Provider:无状态的只读状态(用于共享静态数据/计算结果)

适用于不需要修改的状态,比如全局配置、常量、计算衍生值(如“购物车总价”由“商品列表”计算得出)。

// 示例:全局配置 Provider(只读)
final appConfigProvider = Provider((ref) {
  return AppConfig(
    baseUrl: "https://api.example.com",
    timeout: Duration(seconds: 10),
  );
});

// 组件中使用
class HomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 获取只读状态,无需监听变化(也可监听)
    final appConfig = ref.watch(appConfigProvider);
    return Text("接口地址:${appConfig.baseUrl}");
  }
}

2. StateProvider:简单的可修改状态(用于单一值的简单状态)

适用于简单的、单一值的可修改状态,比如“主题模式”“登录状态”“计数器”,底层基于 StateNotifier 实现,用法简洁。

// 示例:计数器 StateProvider
final counterProvider = StateProvider((ref) => 0);

// 组件中使用(修改+监听)
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 监听状态变化
    final count = ref.watch(counterProvider);
    return Column(
      children: [
        Text("计数:$count"),
        ElevatedButton(
          onPressed: () {
            // 修改状态(两种方式)
            ref.read(counterProvider.notifier).state += 1;
            // 或 ref.read(counterProvider.notifier).update((state) => state + 1);
          },
          child: Text("增加"),
        ),
      ],
    );
  }
}

3. StateNotifierProvider:复杂的可修改状态(用于多逻辑、多值的状态)

适用于复杂状态管理,比如“商品列表”“用户信息”“表单数据”,支持多方法、多状态值,逻辑清晰,可维护性强,是 Riverpod 2.x 中最常用的 API。

// 示例:用户信息 StateNotifierProvider
// 1. 定义状态模型
class UserState {
  final String name;
  final int age;
  final bool isLogin;

  UserState({required this.name, required this.age, required this.isLogin});

  // 拷贝状态(不可变对象,修改时需返回新对象)
  UserState copyWith({String? name, int? age, bool? isLogin}) {
    return UserState(
      name: name ?? this.name,
      age: age ?? this.age,
      isLogin: isLogin ?? this.isLogin,
    );
  }
}

// 2. 定义 StateNotifier(处理状态逻辑)
class UserNotifier extends StateNotifier<UserState> {
  UserNotifier() : super(UserState(name: "未登录", age: 0, isLogin: false));

  // 登录方法(修改状态)
  void login(String name, int age) {
    state = state.copyWith(name: name, age: age, isLogin: true);
  }

  // 退出登录方法
  void logout() {
    state = UserState(name: "未登录", age: 0, isLogin: false);
  }
}

// 3. 定义 Provider
final userProvider = StateNotifierProvider<UserNotifier, UserState>((ref) {
  return UserNotifier();
});

4. FutureProvider:异步状态(用于网络请求、本地存储等异步操作)

适用于异步获取的状态,比如“从接口获取商品列表”“从本地读取用户信息”,自动处理“加载中、成功、失败”三种状态,无需手动管理加载状态。

5. ProviderScope:状态作用域(控制状态的生命周期)

用于定义状态的作用范围,比如“全局状态”(整个 App 可见)、“页面级状态”(仅当前页面及子组件可见),页面销毁时,页面级状态自动释放。

三、实战案例:5个高频场景的最佳实践(多代码+避坑)

结合实际开发中的高频场景,每个案例都包含“需求描述、代码实现、最佳实践说明”,覆盖全局状态、页面级状态、列表状态、表单状态、异步状态,直接套用即可。

案例1:全局状态管理(用户信息+主题模式)

「需求描述」:App 全局共享两个状态——用户信息(登录/退出、用户名、年龄)、主题模式(浅色/深色),所有页面都能访问和修改这两个状态,且退出 App 后状态不丢失(简单模拟持久化)。

「最佳实践」:使用 StateNotifierProvider 管理复杂全局状态,Provider 管理衍生状态,ProviderScope 全局注册,结合 SharedPreferences 模拟持久化。

// 1. 引入依赖(pubspec.yaml)
// flutter_riverpod: ^2.3.0
// shared_preferences: ^2.2.2

// 2. 全局初始化 ProviderContainer(main.dart)
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 初始化 SharedPreferences
  final prefs = await SharedPreferences.getInstance();
  // 创建全局 ProviderContainer
  final container = ProviderContainer(
    overrides: [
      // 注入 SharedPreferences,供其他 Provider 使用
      sharedPreferencesProvider.overrideWithValue(prefs),
    ],
  );

  runApp(
    // 全局 ProviderScope,让所有组件都能访问容器中的状态
    UncontrolledProviderScope(
      container: container,
      child: const MyApp(),
    ),
  );
}

// 3. 定义 SharedPreferences Provider(供其他 Provider 依赖)
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
  throw UnimplementedError("SharedPreferences 需在 main 中注入");
});

// 4. 用户信息状态管理(StateNotifierProvider)
class UserState {
  final String name;
  final int age;
  final bool isLogin;

  UserState({required this.name, required this.age, required this.isLogin});

  UserState copyWith({String? name, int? age, bool? isLogin}) {
    return UserState(
      name: name ?? this.name,
      age: age ?? this.age,
      isLogin: isLogin ?? this.isLogin,
    );
  }
}

class UserNotifier extends StateNotifier<UserState> {
  final Ref ref; // 用于依赖其他 Provider(如 SharedPreferences)

  UserNotifier(this.ref) : super(UserState(name: "未登录", age: 0, isLogin: false)) {
    // 初始化:从本地读取用户信息
    _loadUserInfo();
  }

  // 从本地读取用户信息
  Future<void> _loadUserInfo() async {
    final prefs = ref.read(sharedPreferencesProvider);
    final name = prefs.getString("user_name") ?? "未登录";
    final age = prefs.getInt("user_age") ?? 0;
    final isLogin = prefs.getBool("is_login") ?? false;
    state = state.copyWith(name: name, age: age, isLogin: isLogin);
  }

  // 登录(保存到本地+更新状态)
  Future<void> login(String name, int age) async {
    final prefs = ref.read(sharedPreferencesProvider);
    await prefs.setString("user_name", name);
    await prefs.setInt("user_age", age);
    await prefs.setBool("is_login", true);
    state = state.copyWith(name: name, age: age, isLogin: true);
  }

  // 退出登录(清除本地+重置状态)
  Future<void> logout() async {
    final prefs = ref.read(sharedPreferencesProvider);
    await prefs.clear();
    state = UserState(name: "未登录", age: 0, isLogin: false);
  }
}

final userProvider = StateNotifierProvider<UserNotifier, UserState>((ref) {
  return UserNotifier(ref);
});

// 5. 主题模式状态管理(StateProvider,简单状态)
final themeModeProvider = StateProvider<ThemeMode>((ref) {
  // 从本地读取主题模式
  final prefs = ref.read(sharedPreferencesProvider);
  final isDark = prefs.getBool("is_dark") ?? false;
  return isDark ? ThemeMode.dark : ThemeMode.light;
});

// 6. 衍生状态:根据用户登录状态,判断是否显示欢迎语(Provider)
final welcomeMessageProvider = Provider((ref) {
  final userState = ref.watch(userProvider);
  return userState.isLogin ? "欢迎回来,${userState.name}!" : "请登录";
});

// 7. 组件中使用(首页)
class HomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 监听用户状态
    final userState = ref.watch(userProvider);
    // 监听主题模式
    final themeMode = ref.watch(themeModeProvider);
    // 监听衍生状态
    final welcomeMessage = ref.watch(welcomeMessageProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text(welcomeMessage),
        actions: [
          // 切换主题
          IconButton(
            icon: Icon(themeMode == ThemeMode.dark ? Icons.light_mode : Icons.dark_mode),
            onPressed: () {
              final isDark = themeMode == ThemeMode.dark;
              ref.read(sharedPreferencesProvider).setBool("is_dark", !isDark);
              ref.read(themeModeProvider.notifier).state = !isDark ? ThemeMode.dark : ThemeMode.light;
            },
          ),
        ],
      ),
      body: Center(
        child: userState.isLogin
            ? Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text("用户名:${userState.name}"),
                  Text("年龄:${userState.age}"),
                  ElevatedButton(
                    onPressed: () => ref.read(userProvider.notifier).logout(),
                    child: const Text("退出登录"),
                  ),
                ],
              )
            : ElevatedButton(
                onPressed: () => ref.read(userProvider.notifier).login("Flutter开发者", 25),
                child: const Text("登录"),
              ),
      ),
    );
  }
}

「最佳实践说明」:

  • 全局状态通过“全局 ProviderContainer + UncontrolledProviderScope”注册,所有组件可直接访问,无需嵌套;
  • 复杂状态(用户信息)用 StateNotifierProvider,拆分“状态模型+状态逻辑”,逻辑清晰,可维护性强;
  • 简单状态(主题模式)用 StateProvider,用法简洁,无需额外定义 StateNotifier;
  • 衍生状态(欢迎语)用 Provider,依赖其他状态计算得出,无需单独管理,减少状态冗余;
  • 通过 ref 依赖其他 Provider(如 SharedPreferences),实现状态间的解耦,避免硬编码。

案例2:页面级状态管理(购物页筛选状态)

「需求描述」:购物页面有一个筛选状态(全部/热销/低价),仅购物页及子组件(筛选栏、商品列表)需要访问该状态,页面销毁后,筛选状态自动释放,避免内存泄漏。

「最佳实践」:使用 StateNotifierProvider + 局部 ProviderScope,将状态作用域限制在购物页内部,页面销毁时状态自动释放。

// 1. 筛选状态模型+StateNotifier
class FilterState {
  final String currentFilter; // 全部/热销/低价

  FilterState({this.currentFilter = "全部"});

  FilterState copyWith({String? currentFilter}) {
    return FilterState(currentFilter: currentFilter ?? this.currentFilter);
  }
}

class FilterNotifier extends StateNotifier<FilterState> {
  FilterNotifier() : super(FilterState());

  // 切换筛选条件
  void setFilter(String filter) {
    if (state.currentFilter != filter) {
      state = state.copyWith(currentFilter: filter);
    }
  }
}

// 2. 定义页面级 Provider(无需全局注册)
final filterProvider = StateNotifierProvider<FilterNotifier, FilterState>((ref) {
  return FilterNotifier();
});

// 3. 购物页(局部 ProviderScope)
class ShoppingPage extends ConsumerWidget {
  const ShoppingPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 全局状态:用户信息、购物车数量(从全局容器获取)
    final userState = ref.watch(userProvider);
    final cartCount = ref.watch(cartProvider).length;

    // 局部 ProviderScope:将筛选状态作用域限制在当前页面
    return ProviderScope(
      child: Scaffold(
        appBar: AppBar(
          title: Text("购物页 ${userState.name}"),
        ),
        body: Column(
          children: [
            // 筛选栏组件(依赖筛选状态)
            const FilterBar(),
            // 购物车数量
            Text("购物车数量:$cartCount"),
            // 商品列表组件(依赖筛选状态)
            const Expanded(child: ProductList()),
          ],
        ),
      ),
    );
  }
}

// 4. 筛选栏组件(依赖页面级筛选状态)
class FilterBar extends ConsumerWidget {
  const FilterBar({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final filterState = ref.watch(filterProvider);
    final filterNotifier = ref.read(filterProvider.notifier);

    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        ElevatedButton(
          onPressed: () => filterNotifier.setFilter("全部"),
          style: ElevatedButton.styleFrom(
            backgroundColor: filterState.currentFilter == "全部" ? Colors.blue : Colors.grey,
          ),
          child: const Text("全部"),
        ),
        ElevatedButton(
          onPressed: () => filterNotifier.setFilter("热销"),
          style: ElevatedButton.styleFrom(
            backgroundColor: filterState.currentFilter == "热销" ? Colors.blue : Colors.grey,
          ),
          child: const Text("热销"),
        ),
        ElevatedButton(
          onPressed: () => filterNotifier.setFilter("低价"),
          style: ElevatedButton.styleFrom(
            backgroundColor: filterState.currentFilter == "低价" ? Colors.blue : Colors.grey,
          ),
          child: const Text("低价"),
        ),
      ],
    );
  }
}

// 5. 商品列表组件(依赖筛选状态)
class ProductList extends ConsumerWidget {
  const ProductList({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final filterState = ref.watch(filterProvider);
    // 模拟商品数据(根据筛选条件过滤)
    final allProducts = [
      {"name": "商品1", "price": 99, "isHot": true},
      {"name": "商品2", "price": 59, "isHot": false},
      {"name": "商品3", "price": 199, "isHot": true},
      {"name": "商品4", "price": 39, "isHot": false},
    ];

    final filteredProducts = switch (filterState.currentFilter) {
      "热销" => allProducts.where((p) => p["isHot"] == true).toList(),
      "低价" => allProducts.where((p) => p["price"] < 60).toList(),
      _ => allProducts,
    };

    return ListView.builder(
      itemCount: filteredProducts.length,
      itemBuilder: (context, index) {
        final product = filteredProducts[index];
        return ListTile(
          title: Text(product["name"]!),
          subtitle: Text("¥${product["price"]}"),
        );
      },
    );
  }
}

「最佳实践说明」:

  • 页面级状态通过“局部 ProviderScope”包裹,仅当前页面及子组件可访问,避免状态污染;
  • 页面销毁时,ProviderScope 会自动销毁其内部的所有 Provider(filterProvider),无需手动 dispose,彻底避免内存泄漏;
  • 页面级状态与全局状态(用户、购物车)分离,各司其职,降低耦合度;
  • 筛选逻辑与 UI 组件分离(FilterNotifier 处理逻辑,FilterBar、ProductList 负责展示),符合“单一职责原则”。

案例3:列表状态管理(商品列表+下拉刷新+上拉加载)

「需求描述」:商品列表页面,支持下拉刷新(重新获取数据)、上拉加载更多,需要管理“商品列表、加载状态、分页参数”,且列表项有独立的“选中状态”。

「最佳实践」:用 StateNotifierProvider 管理列表整体状态,用 FamilyProvider 管理单个列表项的选中状态,结合 FutureProvider 处理异步请求。

// 1. 商品模型
class Product {
  final int id;
  final String name;
  final double price;
  final bool isHot;

  Product({required this.id, required this.name, required this.price, required this.isHot});
}

// 2. 列表状态模型+StateNotifier(管理列表整体状态)
class ProductListState {
  final List<Product> products; // 商品列表
  final bool isLoading; // 整体加载状态
  final bool hasMore; // 是否有更多数据
  final int page; // 当前页码
  final String errorMsg; // 错误信息

  ProductListState({
    this.products = const [],
    this.isLoading = false,
    this.hasMore = true,
    this.page = 1,
    this.errorMsg = "",
  });

  ProductListState copyWith({
    List<Product>? products,
    bool? isLoading,
    bool? hasMore,
    int? page,
    String? errorMsg,
  }) {
    return ProductListState(
      products: products ?? this.products,
      isLoading: isLoading ?? this.isLoading,
      hasMore: hasMore ?? this.hasMore,
      page: page ?? this.page,
      errorMsg: errorMsg ?? this.errorMsg,
    );
  }
}

class ProductListNotifier extends StateNotifier<ProductListState> {
  final Ref ref;

  ProductListNotifier(this.ref) : super(ProductListState()) {
    // 初始化:获取第一页数据
    fetchProducts();
  }

  // 模拟网络请求:获取商品列表
  Future<List<Product>> _fetchProducts(int page) async {
    await Future.delayed(const Duration(seconds: 1)); // 模拟网络延迟
    if (page == 3) {
      return []; // 第三页无数据,模拟“没有更多”
    }
    return List.generate(10, (index) {
      final id = (page - 1) * 10 + index + 1;
      return Product(
        id: id,
        name: "商品$id",
        price: 39.9 + index * 10,
        isHot: index % 2 == 0,
      );
    });
  }

  // 下拉刷新:重新获取第一页数据
  Future<void> refreshProducts() async {
    state = state.copyWith(isLoading: true, errorMsg: "");
    try {
      final products = await _fetchProducts(1);
      state = state.copyWith(
        products: products,
        page: 1,
        hasMore: products.isNotEmpty,
        isLoading: false,
      );
    } catch (e) {
      state = state.copyWith(
        errorMsg: "刷新失败:${e.toString()}",
        isLoading: false,
      );
    }
  }

  // 上拉加载更多
  Future<void> loadMoreProducts() async {
    if (state.isLoading || !state.hasMore) return; // 正在加载或无更多数据,直接返回
    state = state.copyWith(isLoading: true, errorMsg: "");
    try {
      final nextPage = state.page + 1;
      final newProducts = await _fetchProducts(nextPage);
      state = state.copyWith(
        products: [...state.products, ...newProducts],
        page: nextPage,
        hasMore: newProducts.isNotEmpty,
        isLoading: false,
      );
    } catch (e) {
      state = state.copyWith(
        errorMsg: "加载失败:${e.toString()}",
        isLoading: false,
      );
    }
  }
}

// 3. 列表整体状态 Provider
final productListProvider = StateNotifierProvider<ProductListNotifier, ProductListState>((ref) {
  return ProductListNotifier(ref);
});

// 4. 列表项选中状态(FamilyProvider:为每个item生成独立的Provider)
final productSelectedProvider = FamilyProvider<bool, int>((ref, productId) => false);

// 5. 商品列表页面
class ProductListPage extends ConsumerWidget {
  const ProductListPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productListState = ref.watch(productListProvider);
    final productListNotifier = ref.read(productListProvider.notifier);

    return Scaffold(
      appBar: const AppBar(title: Text("商品列表")),
      body: RefreshIndicator(
        onRefresh: productListNotifier.refreshProducts,
        child: ListView.builder(
          itemCount: productListState.products.length + 1, // +1 用于加载更多提示
          itemBuilder: (context, index) {
            // 加载更多提示
            if (index == productListState.products.length) {
              if (productListState.isLoading) {
                return const Padding(
                  padding: EdgeInsets.symmetric(vertical: 16),
                  child: Center(child: CircularProgressIndicator()),
                );
              } else if (productListState.errorMsg.isNotEmpty) {
                return Padding(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                  child: Center(child: Text(productListState.errorMsg)),
                );
              } else if (!productListState.hasMore) {
                return const Padding(
                  padding: EdgeInsets.symmetric(vertical: 16),
                  child: Center(child: Text("没有更多商品了")),
                );
              } else {
                return const SizedBox.shrink();
              }
            }

            // 商品item
            final product = productListState.products[index];
            // 获取当前item的选中状态(FamilyProvider,传入productId)
            final isSelected = ref.watch(productSelectedProvider(product.id));
            final selectedNotifier = ref.read(productSelectedProvider(product.id).notifier);

            return ListTile(
              title: Text(product.name),
              subtitle: Text("¥${product.price}"),
              trailing: isSelected ? const Icon(Icons.check, color: Colors.blue) : null,
              onTap: () {
                // 切换选中状态
                selectedNotifier.state = !isSelected;
              },
              onLongPress: () {
                // 模拟加入购物车
                ref.read(cartProvider.notifier).addProduct(product);
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text("${product.name} 已加入购物车")),
                );
              },
            );
          },
          // 上拉加载更多(监听列表滚动到底部)
          controller: ScrollController()
            ..addListener(() {
              final controller = ScrollController();
              if (controller.position.pixels >= controller.position.maxScrollExtent - 200) {
                productListNotifier.loadMoreProducts();
              }
            }),
        ),
      ),
    );
  }
}

「最佳实践说明」:

  • 列表整体状态(商品列表、加载状态、分页)用 StateNotifierProvider 管理,将异步请求、分页逻辑封装在 StateNotifier 中,UI 组件只负责展示;
  • 列表项独立状态(选中状态)用 FamilyProvider,为每个 item 生成独立的 Provider,修改单个 item 状态时,仅触发该 item 重建,避免整个列表重建,提升性能;
  • 下拉刷新、上拉加载逻辑与 UI 分离,通过 StateNotifier 的方法暴露给组件,降低耦合度;
  • 处理加载中、失败、无更多数据三种状态,提升用户体验;
  • 通过 ref 依赖其他 Provider(如购物车),实现组件间的通信,无需 Context。

案例4:复杂表单状态管理(登录表单)

「需求描述」:登录表单包含“用户名、密码、验证码、协议勾选”四个状态,需要实现“输入验证、按钮禁用逻辑、表单提交”,且状态变化时实时更新 UI。

「最佳实践」:用 StateNotifierProvider 合并所有表单状态,封装验证逻辑和提交逻辑,衍生状态控制按钮禁用状态,避免多个状态单独管理导致的冗余。

// 1. 表单状态模型
class LoginFormState {
  final String username; // 用户名
  final String password; // 密码
  final String code; // 验证码
  final bool isAgreed; // 是否同意协议
  final bool isSubmitting; // 是否正在提交
  final String errorMsg; // 提交错误信息

  LoginFormState({
    this.username = "",
    this.password = "",
    this.code = "",
    this.isAgreed = false,
    this.isSubmitting = false,
    this.errorMsg = "",
  });

  LoginFormState copyWith({
    String? username,
    String? password,
    String? code,
    bool? isAgreed,
    bool? isSubmitting,
    String? errorMsg,
  }) {
    return LoginFormState(
      username: username ?? this.username,
      password: password ?? this.password,
      code: code ?? this.code,
      isAgreed: isAgreed ?? this.isAgreed,
      isSubmitting: isSubmitting ?? this.isSubmitting,
      errorMsg: errorMsg ?? this.errorMsg,
    );
  }

  // 表单验证:判断是否可以提交
  bool get isFormValid {
    return username.isNotEmpty &&
        password.length >= 6 &&
        code.length == 6 &&
        isAgreed;
  }
}

// 2. 表单状态逻辑(StateNotifier)
class LoginFormNotifier extends StateNotifier<LoginFormState> {
  final Ref ref;

  LoginFormNotifier(this.ref) : super(LoginFormState());

  // 更新用户名
  void updateUsername(String username) {
    state = state.copyWith(username: username, errorMsg: "");
  }

  // 更新密码
  void updatePassword(String password) {
    state = state.copyWith(password: password, errorMsg: "");
  }

  // 更新验证码
  void updateCode(String code) {
    state = state.copyWith(code: code, errorMsg: "");
  }

  // 切换协议勾选状态
  void toggleAgreed() {
    state = state.copyWith(isAgreed: !state.isAgreed, errorMsg: "");
  }

  // 表单提交(模拟网络请求)
  Future<void> submit() async {
    if (!state.isFormValid) return; // 表单无效,不提交
    state = state.copyWith(isSubmitting: true, errorMsg: "");
    try {
      // 模拟登录请求
      await Future.delayed(const Duration(seconds: 1));
      if (state.username == "admin" && state.password == "123456") {
        // 登录成功:调用用户状态的登录方法
        ref.read(userProvider.notifier).login(state.username, 25);
      } else {
        throw Exception("用户名或密码错误");
      }
    } catch (e) {
      state = state.copyWith(
        errorMsg: e.toString(),
        isSubmitting: false,
      );
    } finally {
      if (mounted) {
        state = state.copyWith(isSubmitting: false);
      }
    }
  }
}

// 3. 表单 Provider
final loginFormProvider = StateNotifierProvider<LoginFormNotifier, LoginFormState>((ref) {
  return LoginFormNotifier(ref);
});

// 4. 登录表单页面
class LoginPage extends ConsumerWidget {
  const LoginPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final formState = ref.watch(loginFormProvider);
    final formNotifier = ref.read(loginFormProvider.notifier);

    return Scaffold(
      appBar: const AppBar(title: Text("登录")),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // 用户名输入框
            TextField(
              onChanged: formNotifier.updateUsername,
              decoration: InputDecoration(
                hintText: "请输入用户名",
                errorText: formState.username.isEmpty && formState.errorMsg.isEmpty
                    ? null
                    : formState.username.isEmpty
                        ? "用户名不能为空"
                        : null,
              ),
            ),
            const SizedBox(height: 16),
            // 密码输入框
            TextField(
              obscureText: true,
              onChanged: formNotifier.updatePassword,
              decoration: InputDecoration(
                hintText: "请输入密码(至少6位)",
                errorText: formState.password.isNotEmpty && formState.password.length < 6
                    ? "密码至少6位"
                    : null,
              ),
            ),
            const SizedBox(height: 16),
            // 验证码输入框
            TextField(
              onChanged: formNotifier.updateCode,
              decoration: InputDecoration(
                hintText: "请输入6位验证码",
                errorText: formState.code.isNotEmpty && formState.code.length != 6
                    ? "验证码必须6位"
                    : null,
              ),
            ),
            const SizedBox(height: 16),
            // 协议勾选
            Row(
              children: [
                Checkbox(
                  value: formState.isAgreed,
                  onChanged: (_) => formNotifier.toggleAgreed(),
                ),
                const Text("同意用户协议和隐私政策"),
              ],
            ),
            const SizedBox(height: 24),
            // 错误提示
            if (formState.errorMsg.isNotEmpty)
              Text(
                formState.errorMsg,
                style: const TextStyle(color: Colors.red),
              ),
            const SizedBox(height: 16),
            // 登录按钮
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: formState.isFormValid && !formState.isSubmitting
                    ? formNotifier.submit
                    : null,
                child: formState.isSubmitting
                    ? const CircularProgressIndicator(color: Colors.white)
                    : const Text("登录"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

「最佳实践说明」:

  • 合并表单所有相关状态,用一个 StateNotifierProvider 管理,避免多个状态单独管理导致的嵌套冗余和逻辑混乱;
  • 表单验证逻辑(isFormValid)作为衍生状态,封装在状态模型中,组件直接使用,无需重复计算;
  • 提交逻辑封装在 StateNotifier 中,组件只需调用 submit 方法,无需关心内部实现,降低耦合度;
  • 处理“正在提交”状态,避免重复提交;处理错误信息,提升用户体验;
  • 输入验证实时反馈,用户输入时立即提示错误,提升交互体验。

案例5:异步状态管理(网络请求+本地缓存)

「需求描述」:从接口获取用户个人信息,展示“加载中、成功、失败”三种状态,同时将获取到的用户信息缓存到本地,下次打开页面时优先展示本地缓存,再请求接口更新。

「最佳实践」:用 FutureProvider 处理异步请求,结合 StateNotifierProvider 管理本地缓存,实现“缓存优先、接口更新”的逻辑,提升用户体验。

// 1. 用户信息模型(与全局用户状态一致,可复用)
class UserInfo {
  final String name;
  final int age;
  final String avatar;
  final String email;

  UserInfo({required this.name, required this.age, required this.avatar, required this.email});

  // 从JSON解析
  factory UserInfo.fromJson(Map<String, dynamic> json) {
    return UserInfo(
      name: json["name"],
      age: json["age"],
      avatar: json["avatar"],
      email: json["email"],
    );
  }

  // 转换为JSON(用于本地缓存)
  Map<String, dynamic> toJson() {
    return {
      "name": name,
      "age": age,
      "avatar": avatar,
      "email": email,
    };
  }
}

// 2. 本地缓存状态管理(StateNotifierProvider)
class UserCacheNotifier extends StateNotifier<UserInfo?> {
  final Ref ref;

  UserCacheNotifier(this.ref) : super(null) {
    // 初始化:从本地读取缓存
    _loadCache();
  }

  // 从本地读取缓存
  Future<void> _loadCache() async {
    final prefs = ref.read(sharedPreferencesProvider);
    final jsonStr = prefs.getString("user_info_cache");
    if (jsonStr != null) {
      final json = jsonDecode(jsonStr);
      state = UserInfo.fromJson(json);
    }
  }

  // 保存缓存到本地
  Future<void> saveCache(UserInfo userInfo) async {
    final prefs = ref.read(sharedPreferencesProvider);
    final jsonStr = jsonEncode(userInfo.toJson());
    await prefs.setString("user_info_cache", jsonStr);
    state = userInfo;
  }

  // 清除缓存
  Future<void> clearCache() async {
    final prefs = ref.read(sharedPreferencesProvider);
    await prefs.remove("user_info_cache");
    state = null;
  }
}

final userCacheProvider = StateNotifierProvider<UserCacheNotifier, UserInfo?>((ref) {
  return UserCacheNotifier(ref);
});

// 3. 异步请求用户信息(FutureProvider)
final fetchUserInfoProvider = FutureProvider<UserInfo>((ref) async {
  // 模拟网络请求:从接口获取用户信息
  final response = await http.get(Uri.parse("https://api.example.com/user/info"));
  if (response.statusCode == 200) {
    final json = jsonDecode(response.body);
    final userInfo = UserInfo.fromJson(json);
    // 请求成功后,保存到本地缓存
    ref.read(userCacheProvider.notifier).saveCache(userInfo);
    return userInfo;
  } else {
    throw Exception("获取用户信息失败");
  }
});

// 4. 个人信息页面
class ProfilePage extends ConsumerWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 监听本地缓存
    final userCache = ref.watch(userCacheProvider);
    // 监听异步请求状态
    final fetchUserAsync = ref.watch(fetchUserInfoProvider);

    return Scaffold(
      appBar: const AppBar(title: Text("个人信息")),
      body: fetchUserAsync.when(
        loading: () {
          // 加载中:如果有缓存,展示缓存;否则展示加载中
          if (userCache != null) {
            return _buildProfileContent(userCache, isCache: true);
          } else {
            return const Center(child: CircularProgressIndicator());
          }
        },
        error: (error, stackTrace) {
          // 失败:如果有缓存,展示缓存+错误提示;否则展示错误提示
          if (userCache != null) {
            return Column(
              children: [
                _buildProfileContent(userCache, isCache: true),
                Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Text(
                    "加载失败:${error.toString()},展示缓存数据",
                    style: const TextStyle(color: Colors.orange),
                  ),
                ),
              ],
            );
          } else {
            return Center(child: Text("获取信息失败:${error.toString()}"));
          }
        },
        data: (userInfo) {
          // 成功:展示最新数据
          return _buildProfileContent(userInfo, isCache: false);
        },
      ),
    );
  }

  // 构建个人信息内容
  Widget _buildProfileContent(UserInfo userInfo, {required bool isCache}) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          // 头像
          CircleAvatar(
            radius: 50,
            backgroundImage: NetworkImage(userInfo.avatar),
          ),
          const SizedBox(height: 16),
          // 用户名
          Text("用户名:${userInfo.name}", style: const TextStyle(fontSize: 18)),
          const SizedBox(height: 8),
          // 年龄
          Text("年龄:${userInfo.age}"),
          const SizedBox(height: 8),
          // 邮箱
          Text("邮箱:${userInfo.email}"),
          const SizedBox(height: 16),
          // 缓存提示
          if (isCache)
            const Text(
              "展示的是缓存数据",
              style: TextStyle(color: Colors.grey),
            ),
        ],
      ),
    );
  }
}

「最佳实践说明」:

  • 用 FutureProvider 处理异步请求,自动管理“加载中、成功、失败”三种状态,无需手动管理;
  • 用 StateNotifierProvider 管理本地缓存,实现“缓存优先、接口更新”的逻辑,提升用户体验(无网络时也能展示数据);
  • 异步请求成功后,自动更新本地缓存,确保缓存与接口数据一致;
  • 加载中、失败状态下,优先展示缓存数据,减少用户等待时间,提升交互体验;
  • 缓存逻辑与请求逻辑分离,各司其职,可维护性强。

四、Riverpod 2.x 最佳实践总结(避坑指南)

结合前面的案例,总结 Riverpod 2.x 的5个最佳实践,帮你避开常见坑,写出高质量代码。

1. 按状态作用范围选择合适的 Provider

  • 全局状态:用 StateNotifierProvider/Provider,通过全局 ProviderContainer 注册,供所有组件访问;
  • 页面级状态:用 StateNotifierProvider/StateProvider,通过局部 ProviderScope 注册,页面销毁时自动释放;
  • 局部组件状态:用 StateProvider/FamilyProvider,仅在组件内部使用,避免状态污染;
  • 异步状态:用 FutureProvider,自动处理加载状态,无需手动管理;
  • 衍生状态:用 Provider,依赖其他状态计算得出,减少状态冗余。

2. 复杂状态必用 StateNotifierProvider,拆分“状态模型+逻辑”

对于多值、多逻辑的复杂状态(如列表、表单),不要用多个 StateProvider 单独管理,而是用 StateNotifierProvider,将“状态模型”(不可变对象)和“状态逻辑”(StateNotifier)拆分,逻辑清晰,可维护性强。

3. 避免过度依赖,精准监听状态

  • 组件仅监听自身需要的状态,不要监听无关状态,避免不必要的重建;
  • 使用 ref.watch 监听状态变化,使用 ref.read 仅获取状态(不监听),比如按钮点击修改状态时,用 ref.read;
  • 对于不需要实时监听的状态,可使用 ref.read 或 listen: false,减少重建次数。

4. 合理使用 ProviderScope,控制状态生命周期

全局状态用全局 ProviderContainer + UncontrolledProviderScope,页面级状态用局部 ProviderScope,确保页面销毁时,页面级状态自动释放,避免内存泄漏。

5. 状态不可变,修改状态需返回新对象

Riverpod 推荐使用不可变对象管理状态(如案例中的 UserState、FilterState),修改状态时,不要直接修改原对象,而是通过 copyWith 方法返回新对象,确保状态变化可追踪,避免不可预期的 bug。

五、总结

Riverpod 2.x 的核心价值,在于“解耦、精准、可扩展”——它彻底摆脱了 Provider 的嵌套地狱,通过强类型安全减少 runtime 异常,通过灵活的状态分层管理避免内存泄漏,通过自动缓存优化应用性能。

本文通过5个高频实战案例,覆盖了全局状态、页面级状态、列表状态、表单状态、异步状态,每个案例都包含完整代码和最佳实践说明,希望能帮你快速上手 Riverpod 2.x。

记住:Riverpod 不是“越复杂越好”,而是“合适才好”——根据状态的作用范围、复杂度,选择合适的 Provider,遵循“状态不可变、逻辑与 UI 分离”的原则,就能写出简洁、高效、可维护的 Flutter 代码。