Flutter: 状态管理 — Riverpod

状态是应用的血液——管理不当,应用就会"内出血":数据不一致、重建风暴、内存泄漏。

📋 前置知识:需要先掌握以下内容

1. 你将学到


2. 一个状态灾难的真实故事

(1) 痛点:setState 的全局状态困局

Bob 的 ShopApp 用 setState 管理购物车状态。问题是:购物车数据在首页、详情页、购物车页都需要,必须层层回调传递。CartPage 修改数量后,返回 HomePage 时 AppBar 角标不更新——因为 HomePage 的 State 不知道 CartPage 改了什么。更糟的是,10 个页面有 5 份购物车数据的副本,彼此不同步。

(2) Riverpod 的解法

Riverpod 把状态提升到全局 Provider,任何页面都可以 ref.watch 监听同一个购物车状态,修改一处所有监听者自动更新。

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

// ⚙️ **安装依赖**:flutter pub add flutter_riverpod riverpod_annotation
// ⚙️ **开发依赖**:flutter pub add --dev riverpod_generator build_runner

// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型
// - CartItem: 见下方 CartNotifier 示例中的定义

// Global cart state - any page can access
@riverpod
class CartNotifier extends _$CartNotifier {
  @override
  List<CartItem> build() => [];

  void addItem(Product product) {
    state = [...state, CartItem(product: product, quantity: 1)];
  }

  void removeItem(int productId) {
    state = state.where((item) => item.product.id != productId).toList();
  }
}

// Watch in any widget
final cart = ref.watch(cartNotifierProvider);
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(3) 收益:一处修改,全局同步

Bob 用 Riverpod 后,购物车状态只有一份,任何页面修改后所有监听者自动更新。回调地狱消失,数据始终一致。


3. Riverpod 核心概念

100%
graph TD
    PS[ProviderScope] --> CN[CartNotifier]
    PS --> PN[ProductAsyncNotifier]
    PS --> UN[UserNotifier]
    CN --> |ref.watch| CV[CartView]
    PN --> |ref.watch| PV[ProductListView]
    UN --> |ref.watch| UV[UserProfileView]
    CN --> |totalItems| Badge[BottomNav Badge]
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(1) Provider 类型

Provider 状态类型 适用场景
Provider 不可变值 依赖注入、配置
StateProvider 简单可变值 计数器、开关
FutureProvider Future 结果 一次性异步数据
StreamProvider Stream 结果 实时数据流
NotifierProvider Notifier 实例 复杂状态 + 业务逻辑
AsyncNotifierProvider AsyncNotifier 异步初始化 + 业务逻辑

(2) ref 方法对比

方法 用途 触发重建 适用位置
ref.watch 监听状态变化 是(状态变化时) build 方法内
ref.read 一次性读取 回调/事件处理
ref.listen 监听 + 执行副作用 build 内(导航/SnackBar)
⚠️ 注意: 不要在 build 之外使用 ref.watch,不要在 build 内使用 ref.read 监听状态。


4. 手写 Provider

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:StateProvider 简单状态

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ⚙️ **安装依赖**:flutter pub add flutter_riverpod

// Simple counter with StateProvider
final counterProvider = StateProvider<int>((ref) => 0);

// Theme mode provider
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);

// Usage in widget
class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).state++,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:FutureProvider 异步数据

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ⚙️ **安装依赖**:flutter pub add flutter_riverpod

// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型
// - ProductTile: 自定义 Widget,展示单个商品
// - productRepositoryProvider: 见第14课 ProductRepository

// Load products once
final productsProvider = FutureProvider<List<Product>>((ref) async {
  final repo = ref.read(productRepositoryProvider);
  return repo.getProducts();
});

// Usage
class ProductList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productsAsync = ref.watch(productsProvider);
    return productsAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (err, _) => Text('Error: $err'),
      data: (products) => ListView.builder(
        itemCount: products.length,
        itemBuilder: (_, i) => ProductTile(product: products[i]),
      ),
    );
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

5. Notifier 与 AsyncNotifier

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:CartNotifier(完整购物车逻辑)

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

// ⚙️ **安装依赖**:flutter pub add flutter_riverpod

// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型(简化版如下)
/*
class Product {
  final int id;
  final String name;
  final double price;
  const Product({required this.id, required this.name, required this.price});
}
*/

class CartItem {
  final Product product;
  final int quantity;
  const CartItem({required this.product, required this.quantity});
  double get total => product.price * quantity;
  CartItem copyWith({int? quantity}) => CartItem(product: product, quantity: quantity ?? this.quantity);
}

class CartNotifier extends Notifier<List<CartItem>> {
  @override
  List<CartItem> build() => [];

  void addItem(Product product) {
    final idx = state.indexWhere((i) => i.product.id == product.id);
    if (idx >= 0) {
      state = [...state]..[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
    } else {
      state = [...state, CartItem(product: product, quantity: 1)];
    }
  }

  void updateQuantity(int productId, int quantity) {
    if (quantity <= 0) {
      removeItem(productId);
      return;
    }
    state = [
      for (final item in state)
        if (item.product.id == productId) item.copyWith(quantity: quantity) else item,
    ];
  }

  void removeItem(int productId) {
    state = state.where((i) => i.product.id != productId).toList();
  }

  void clear() => state = [];
}

final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>(CartNotifier.new);

// Derived state: total price
final cartTotalProvider = Provider<double>((ref) {
  final items = ref.watch(cartProvider);
  return items.fold(0.0, (sum, item) => sum + item.total);
});

// Derived state: item count
final cartItemCountProvider = Provider<int>((ref) {
  final items = ref.watch(cartProvider);
  return items.fold(0, (sum, item) => sum + item.quantity);
});
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:AsyncNotifier(异步初始化商品)

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

// ⚙️ **安装依赖**:flutter pub add flutter_riverpod

// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型
// - productRepositoryProvider: 见第14课 ProductRepository

class ProductAsyncNotifier extends AsyncNotifier<List<Product>> {
  @override
  Future<List<Product>> build() async {
    final repo = ref.read(productRepositoryProvider);
    return repo.getProducts();
  }

  Future<void> loadMore() async {
    final current = state.valueOrNull ?? [];
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final more = await ref.read(productRepositoryProvider).getProducts(page: (current.length ~/ 20) + 1);
      return [...current, ...more];
    });
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => ref.read(productRepositoryProvider).getProducts());
  }
}

final productProvider = AsyncNotifierProvider<ProductAsyncNotifier, List<Product>>(
  ProductAsyncNotifier.new,
);
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

6. @riverpod 代码生成

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:使用 riverpod_generator

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

// ⚙️ **安装依赖**:flutter pub add flutter_riverpod riverpod_annotation
// ⚙️ **开发依赖**:flutter pub add --dev riverpod_generator build_runner

// 自定义类定义来源:
// - CartItem: 见本课第5节 CartNotifier 示例
// - User/AuthService: 见第14课 Auth Notifier

part 'providers.g.dart';

// Generated notifier
@riverpod
class Cart extends _$Cart {
  @override
  List<CartItem> build() => [];

  void addItem(Product product) {
    final idx = state.indexWhere((i) => i.product.id == product.id);
    if (idx >= 0) {
      state[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
      state = [...state]; // Trigger rebuild
    } else {
      state = [...state, CartItem(product: product, quantity: 1)];
    }
  }
}

// Generated provider (derived)
@riverpod
double cartTotal(CartTotalRef ref) {
  final items = ref.watch(cartProvider);
  return items.fold(0.0, (sum, item) => sum + item.total);
}

// Keep alive across widget lifecycle
@Riverpod(keepAlive: true)
class Auth extends _$Auth {
  @override
  AsyncValue<User?> build() => const AsyncData(null);

  Future<void> login(String email, String password) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => AuthService.login(email, password));
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

7. 完整示例:ShopApp Riverpod 集成

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

// ⚙️ **安装依赖**:flutter pub add flutter_riverpod go_router

// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型
// - cartProvider/cartTotalProvider/cartItemCountProvider: 见本课第5节
// - themeModeProvider: 见本课第4节 StateProvider

// main.dart
void main() {
  runApp(ProviderScope(child: ShopApp()));
}

class ShopApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeModeProvider);
    return MaterialApp.router(
      title: 'ShopApp',
      theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
      darkTheme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true, brightness: Brightness.dark),
      themeMode: themeMode,
      routerConfig: router,
    );
  }
}

// Cart badge in AppBar - auto-updates
class CartBadge extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(cartItemCountProvider);
    return Stack(
      alignment: Alignment.center,
      children: [
        const Icon(Icons.shopping_cart),
        if (count > 0)
          Positioned(right: 0, top: 0,
            child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
              child: Text('$count', style: const TextStyle(fontSize: 9, color: Colors.white)))),
      ],
    );
  }
}

// Add to cart action
class AddToCartButton extends ConsumerWidget {
  final Product product;
  const AddToCartButton({super.key, required this.product});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return FilledButton.icon(
      onPressed: () {
        ref.read(cartProvider.notifier).addItem(product);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('${product.name} added to cart')),
        );
      },
      icon: const Icon(Icons.shopping_cart),
      label: const Text('Add to Cart'),
    );
  }
}

// Cart page with Riverpod
class CartPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(cartProvider);
    final total = ref.watch(cartTotalProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Cart (${items.length})')),
      body: items.isEmpty
          ? const Center(child: Text('Cart is empty'))
          : Column(children: [
              Expanded(child: ListView.separated(
                itemCount: items.length,
                separatorBuilder: (_, __) => const Divider(),
                itemBuilder: (_, i) {
                  final item = items[i];
                  return ListTile(
                    title: Text(item.product.name),
                    subtitle: Text('\$${item.total.toStringAsFixed(2)}'),
                    trailing: Row(mainAxisSize: MainAxisSize.min, children: [
                      IconButton(icon: const Icon(Icons.remove), onPressed: () =>
                        ref.read(cartProvider.notifier).updateQuantity(item.product.id, item.quantity - 1)),
                      Text('${item.quantity}'),
                      IconButton(icon: const Icon(Icons.add), onPressed: () =>
                        ref.read(cartProvider.notifier).updateQuantity(item.product.id, item.quantity + 1)),
                    ]),
                  );
                },
              )),
              Padding(padding: const EdgeInsets.all(16),
                child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
                  Text('\$${total.toStringAsFixed(2)}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
                  FilledButton(onPressed: () => context.push('/checkout'), child: const Text('Checkout')),
                ])),
            ]),
    );
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

❓ 常见问题

Q Riverpod 和 Provider 包有什么区别?
A Riverpod 是 Provider 的进化版,解决了 Provider 的依赖注入不安全、无法测试等问题。新项目直接用 Riverpod。
Q ref.watch 和 ref.read 怎么选?
A build 方法内用 ref.watch(需要 UI 响应状态变化);回调/事件处理中用 ref.read(不需要重建)。
Q state = [...state] 是必须的吗?
A 是的。Riverpod 用 identical 检查状态是否变化。修改 List 内容不创建新对象,Riverpod 不会通知更新。
Q AsyncNotifier 的 AsyncLoading/AsyncData/AsyncError 怎么处理?
A.when(loading:, error:, data:) 模式匹配,分别显示加载指示器、错误页面、数据内容。
Q @riverpod 注解需要什么依赖?
A pubspec.yaml 添加 riverpod_annotation + riverpod_generator + build_runner,运行 dart run build_runner watch
Q keepAlive 和 autoDispose 有什么区别?
A autoDispose(默认)当没有监听者时自动销毁;keepAlive 永远不销毁。全局状态(如 Auth)用 keepAlive。

📖 小节


📝 作业

  1. 基础题(难度⭐):用 StateProvider 实现暗色模式切换,用 ConsumerWidget 在 AppBar 显示当前模式。
  2. 进阶题(难度⭐⭐):用 Notifier 实现 CartNotifier,支持增删改查和总价计算,在两个不同页面展示购物车状态。
  3. 挑战题(难度⭐⭐⭐):用 @riverpod 代码生成实现 Auth + Product + Cart 三个 Notifier,实现:登录后自动加载商品,加购后角标自动更新。

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏