第 7 章 状态管理(完整可运行示例)

先明确边界:只影响自己页面的状态用 setState;跨页面共享的状态才需要状态管理库。这一章用两个完整程序把这件事讲透,最后进入 Riverpod。

7.1 反面教材:setState 跨页面共享有多痛

购物车数据放在首页,详情页要加购、底部栏要显示数量,就得把回调一层层往下传(prop drilling)。页面一深,参数会变成这样:

// 为了把「加购」传到第 4 层子组件,中间每个组件都要多一个参数
HomePage(onAddToCart: ..., cartCount: ...)
  └ ProductList(onAddToCart: ..., cartCount: ...)
      └ ProductCard(onAddToCart: ..., cartCount: ...)
          └ AddButton(onAddToCart: ...)   // 真正用到的地方在最底层

Riverpod 的做法是把状态放在「全局容器」里,任何组件直接 ref.watch 订阅,不需要经过中间层。

7.2 完整示例:用 Riverpod 做购物车(单文件可运行)

创建步骤
  1. flutter create cart_demo && cd cart_demo;
  2. flutter pub add flutter_riverpod(会自动选当前兼容的最新版);
  3. 用下面代码整体替换 lib/main.dart,flutter run 运行;
  4. 连续点右下角「加购」,观察 AppBar 角标与列表同时更新;左滑可删除。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() => runApp(const ProviderScope(child: MyApp()));

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2E7D6B)),
      ),
      home: const CartPage(),
    );
  }
}

// ---------- ① 状态层:Notifier 里放数据 + 修改数据的方法 ----------
class CartNotifier extends Notifier<List<String>> {
  @override
  List<String> build() => const []; // 初始状态:空购物车

  void add(String product) {
    // 不要 state.add(...):Riverpod 靠对象引用判断是否变化,必须生成新列表
    state = [...state, product];
  }

  void removeAt(int index) {
    final next = [...state]..removeAt(index);
    state = next;
  }

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

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

// ---------- ② 派生状态:只关心数量,不重复存一份 ----------
final cartCountProvider = Provider<int>((ref) {
  return ref.watch(cartProvider).length;
});

// ---------- ③ 界面层:ConsumerWidget 多一个 ref 参数 ----------
class CartPage extends ConsumerWidget {
  const CartPage({super.key});

  static const _products = ['拿铁', '美式', '可颂', '芝士蛋糕', '气泡水'];

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(cartProvider);      // 订阅列表
    final count = ref.watch(cartCountProvider); // 订阅数量
    final scheme = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('购物车'),
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: 12),
            child: Badge(
              label: Text('$count'),
              isLabelVisible: count > 0,
              child: const Icon(Icons.shopping_cart_outlined),
            ),
          ),
          if (items.isNotEmpty)
            IconButton(
              tooltip: '清空',
              icon: const Icon(Icons.delete_sweep_outlined),
              onPressed: () => ref.read(cartProvider.notifier).clear(),
            ),
        ],
      ),
      body: items.isEmpty
          ? Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(Icons.shopping_cart_outlined,
                      size: 56, color: scheme.outline),
                  const SizedBox(height: 12),
                  const Text('购物车是空的'),
                ],
              ),
            )
          : ListView.separated(
              padding: const EdgeInsets.all(12),
              itemCount: items.length,
              separatorBuilder: (_, __) => const Divider(height: 1),
              itemBuilder: (context, index) => Dismissible(
                key: ValueKey('$index-${items[index]}'),
                direction: DismissDirection.endToStart,
                background: Container(
                  color: scheme.error,
                  alignment: Alignment.centerRight,
                  padding: const EdgeInsets.only(right: 20),
                  child: const Icon(Icons.delete, color: Colors.white),
                ),
                onDismissed: (_) {
                  // 回调里用 read:只执行动作,不订阅
                  ref.read(cartProvider.notifier).removeAt(index);
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('已移除 ${items[index]}')),
                  );
                },
                child: ListTile(
                  leading: CircleAvatar(child: Text('${index + 1}')),
                  title: Text(items[index]),
                  subtitle: const Text('左滑删除'),
                ),
              ),
            ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          // read + notifier:拿到状态对象调用方法
          final name = _products[count % _products.length];
          ref.read(cartProvider.notifier).add(name);
        },
        icon: const Icon(Icons.add),
        label: const Text('加购'),
      ),
    );
  }
}
关键点(背下来)
动态效果 · 加购后角标与列表同步更新
购物车 🛒 3
拿铁
可颂
气泡水
← 上一章第 6 章 路由与导航下一章 →第 7.5 章 Riverpod 入门详解(重点)
✉ 联系作者:linghurongjun@sina.com