CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Flutter 开发入门/课时/状态管理
RawBlame
课时目录12 节

Flutter 开发入门

  1. 01Flutter 环境
  2. 02Dart 语言
  3. 03Widget 体系
  4. 04架构原则
  5. 05状态管理
  6. 06动画
  7. 07网络与本地存储
  8. 08Firebase 集成
  9. 09测试
  10. 10CI/CD 与发布
  11. 11分析与监控
  12. 12综合练习

状态管理

Provider/Riverpod/BLoC。

30 分钟

状态管理

这一课你将能做到

  1. 用 setState 管理单个 Widget 内部状态,并清楚它的边界
  2. 用 Provider + ChangeNotifier 在多个页面之间共享状态
  3. 用 Riverpod 写现代化的、可测试的状态管理,并理解状态提升

为什么要学

setState 在 1-2 个页面就够用,一旦跨页面共享数据(购物车、登录用户、主题),就必须用专门的状态管理方案。不会状态管理 = 只能写 demo。

人话:setState 是「家里的便签」,Provider/Riverpod 是「公司内部的 OA 系统」。便签贴自己屏幕上行,要发给同事就得走 OA。

先建立图像

                    ┌──────────────────┐
                    │   ChangeNotifier │   ← 数据 + 通知
                    └────────▲─────────┘
                             │ notifyListeners()
                             │
       UI 层                  │                        动作层
  ┌──────────────┐            │              ┌──────────────────┐
  │ Consumer/    │  ←─────────┘              │ 按钮 onPressed   │
  │ context.watch│    自动 rebuild            │ notifier.add()   │
  └──────────────┘                            └──────────────────┘

数据单向流动:事件 → 修改 Model → Model 通知 → UI 重建。

跟着做

步骤 1:setState 的边界

setState 适合:单个 Widget 内部的、不需要跨页面共享的状态。例如表单输入、Tab 选中:

class LoginForm extends StatefulWidget {
  const LoginForm({super.key});
  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  String _email = '';
  bool _obscure = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(onChanged: (v) => setState(() => _email = v)),
        TextField(
          obscureText: _obscure,
          decoration: InputDecoration(
            suffixIcon: IconButton(
              icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility),
              onPressed: () => setState(() => _obscure = !_obscure),
            ),
          ),
        ),
        Text('当前输入:$_email'),
      ],
    );
  }
}

红线:跨页面共享、复杂业务状态、需要持久化——不要用 setState。

步骤 2:InheritedWidget 是底层

Flutter 内置 InheritedWidget,让子孙 Widget 通过 context.dependOnInheritedWidgetOfExactType 访问数据并自动 rebuild。Provider 就是对它的友好封装。理解即可,不要手写。

class Counter extends InheritedWidget {
  final int value;
  const Counter({super.key, required this.value, required super.child});

  static Counter of(BuildContext ctx) =>
      ctx.dependOnInheritedWidgetOfExactType<Counter>()!;

  @override
  bool updateShouldNotify(Counter old) => value != old.value;
}

步骤 3:Provider 实战

pubspec.yaml 加:

dependencies:
  provider: ^6.1.0

定义 Model:

import 'package:flutter/foundation.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];
  List<String> get items => List.unmodifiable(_items);
  int get count => _items.length;

  void add(String item) {
    _items.add(item);
    notifyListeners();          // 通知所有监听者 rebuild
  }

  void remove(String item) {
    _items.remove(item);
    notifyListeners();
  }
}

在顶层提供:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CartModel(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) =>
      const MaterialApp(home: ShopPage());
}

在任意子树使用:

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

  @override
  Widget build(BuildContext context) {
    // 三种读法
    final cart = context.watch<CartModel>();          // 订阅 +  rebuild
    // final cart = context.read<CartModel>();        // 只读一次,不订阅
    // final count = context.select<CartModel, int>((c) => c.count); // 精准订阅

    return Scaffold(
      appBar: AppBar(title: Text('购物车 ${cart.count}')),
      body: ListView(
        children: [
          for (var p in ['苹果', '香蕉', '橙子'])
            ListTile(
              title: Text(p),
              trailing: IconButton(
                icon: const Icon(Icons.add),
                onPressed: () => context.read<CartModel>().add(p),
              ),
            ),
          const Divider(),
          for (var item in cart.items) ListTile(title: Text('已选: $item')),
        ],
      ),
    );
  }
}

核心规则:事件回调里用 context.read,build 里要响应数据用 context.watch 或 Consumer。

步骤 4:Consumer 精准 rebuild

// 只有这个小块会 rebuild
Consumer<CartModel>(
  builder: (ctx, cart, child) => Text('数量: ${cart.count}'),
)

context.watch 会让整个所在的 Widget rebuild;Consumer 把范围圈小。

步骤 5:Riverpod(新一代推荐)

pubspec.yaml:

dependencies:
  flutter_riverpod: ^2.5.0
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 1. 定义 Provider
final counterProvider = StateProvider<int>((ref) => 0);

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) =>
      const MaterialApp(home: CounterPage());
}

// 2. 用 ConsumerWidget
class CounterPage extends ConsumerWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Scaffold(
      body: Center(child: Text('$count', style: const TextStyle(fontSize: 48))),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).state++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Riverpod 优势:

  • 编译期安全:不存在 ProviderNotFoundException
  • 可覆盖:测试时 ProviderScope(overrides: [...]) 一行替换实现
  • 解耦:Provider 不依赖 BuildContext,纯 Dart 测试友好

步骤 6:状态提升

兄弟 Widget 共享状态 → 把状态提升到最近的公共祖先:

class Parent extends StatefulWidget {
  const Parent({super.key});
  @override
  State<Parent> createState() => _ParentState();
}

class _ParentState extends State<Parent> {
  bool _on = false;
  @override
  Widget build(BuildContext context) => Column(children: [
        Switch(value: _on, onChanged: (v) => setState(() => _on = v)),
        Lamp(on: _on),
      ]);
}

class Lamp extends StatelessWidget {
  final bool on;
  const Lamp({super.key, required this.on});
  @override
  Widget build(BuildContext context) =>
      Icon(Icons.lightbulb, color: on ? Colors.yellow : Colors.grey, size: 64);
}

状态提升是状态管理的根本思想,Provider/Riverpod 只是把「公共祖先」搬到了 App 顶层。

讲清楚要点

  1. setState 边界:单 Widget、临时状态、不涉及业务。
  2. ChangeNotifier + Provider:经典方案,简单好懂,文档丰富。
  3. Riverpod:编译安全 + 测试友好,新项目首选。
  4. BLoC:把事件和状态分离成 Stream,大型项目可读性高但样板代码多。
  5. 状态提升:共享状态放公共祖先;状态管理中所有方案都是这个思想的扩展。

你可能会懵的地方

  • Provider 和 Riverpod 是同一个作者? 是,都是 Remi Rousselet。Riverpod 是 Provider 的「重写版」,解决了 Provider 依赖 BuildContext 的问题。

  • watch vs read 怎么选? build 方法里要 UI 跟随数据变化 → watch;事件回调里调用方法 → read。在 build 里用 read 不会触发更新。

  • ChangeNotifier 里的字段要不要加 final? 可变状态字段不要 final(要改),但对外的 getter 要返回不可修改的视图(如 List.unmodifiable),避免外部绕开 notifyListeners 直接改。

  • 为什么不直接用全局变量? 全局变量没有通知机制,UI 不知道何时刷新;测试时也无法隔离。

  • Riverpod 的 ref.watch 能写在 build 之外吗? 不能。ref.watch 只能在 build 中调用,事件回调里用 ref.read。

错法对照

错法问题正解
在 onPressed 里 context.watch编译期不报错但触发异常事件回调用 context.read
notifyListeners() 之后修改字段UI 拿到旧值先改字段再 notify
把 ChangeNotifier 字段直接暴露成可变 List外部绕过 notify 直接改getter 返回 List.unmodifiable
顶层只用 Provider.value 包已存在对象无法 dispose,内存泄漏用 ChangeNotifierProvider(create: ...) 让 Provider 管理生命周期
在 Riverpod 的 build 里 ref.read 又 watch 字段数据不刷新build 里只用 ref.watch

卡点急救

  1. ProviderNotFoundException:Provider 没在上层注册,或读的 context 来自注册点之上。检查 ChangeNotifierProvider 位置是否包住使用点。
  2. watch 后 UI 不刷新:字段是不是改了?notifyListeners() 调了吗?返回的对象是不是同一个引用?考虑返回新对象。
  3. Riverpod Bad state: Tried to read ... from a place other than build:在事件回调里写了 ref.watch,改成 ref.read。
  4. 内存泄漏:AnimationController/StreamController 放 ChangeNotifier 里时,重写 dispose() 里 dispose 掉。

面试常问

  • setState 的局限? 只能影响当前 Widget 子树;状态无法跨页面共享;业务逻辑混在 UI 里难测试。

  • Provider 的原理? 基于 InheritedWidget:通过 context.dependOnInheritedWidgetOfExactType 建立依赖,数据变化时 InheritedWidget 的 updateShouldNotify 触发依赖方 rebuild。

  • Riverpod 相比 Provider 解决了什么? 去掉了对 BuildContext 的依赖(编译期安全);支持 override 方便测试;可组合(一个 Provider 依赖另一个);不依赖 Flutter 也可在纯 Dart 使用。

  • BLoC 和 Provider 怎么选? 项目小、团队新 → Provider/Riverpod;事件流复杂、需要清晰的事件-状态映射、团队大 → BLoC。

自测三问

  1. 为什么 onPressed: () => context.watch<CartModel>().add('x') 是错的?应该怎么写?
  2. 写一个 ChangeNotifier 暴露 count,并用 Provider 在两个页面之间共享。
  3. Riverpod 的 ref.watch 和 ref.read 区别?各自的使用场景?

小结一句话

小范围用 setState,跨页面用 Provider,新项目用 Riverpod;核心思想是「状态提升 + 单向数据流」。

延伸

  • Riverpod 官方文档:<https://riverpod.dev>
  • Provider 官方文档:<https://pub.dev/packages/provider>
  • 下一课:动画,让 UI 活起来。
加载本节练习…
← 上一课架构原则下一课 →动画