Flutter状态管理最佳实践:实战分享
导读:在 Flutter 中,状态管理(State Management)是构建复杂应用时的核心难题。一套清晰、可维护、性能优越的状态管理方案能让项目更具扩展性、更易测试、更少 BUG。本文从最基础的setState到社区主流的Provider、Riverpod、Bloc等方案,结合代码示例、图解与实战心得,帮助你快速掌握 Flutter 状态管理的最佳实践。
目录
- 为什么需要状态管理
- 2.1
setState的原理 - 2.2 局部状态如何拆分
- 2.3 代码示例与图解
- 2.1
- 3.1
InheritedWidget原理简介 - 3.2 自定义
InheritedNotifier/InheritedModel - 3.3 代码示例与示意图
- 3.1
- 4.1
Provider基本用法 - 4.2
ChangeNotifierProvider与Consumer - 4.3
FutureProvider/StreamProvider - 4.4 代码示例与图解
- 4.1
- 5.1 为什么选择 Riverpod
- 5.2
ProvidervsRiverpod对比 - 5.3
Riverpod代码示例
- 6.1
Bloc概念与优缺点 - 6.2
Cubit简化版本 - 6.3 代码示例与事件流图解
- 6.1
- 7.1 需求描述与核心功能
- 7.2
setState实现 - 7.3
Provider实现 - 7.4
Bloc实现 - 7.5 性能与可维护性对比分析
- 总结与最佳实践
一、为什么需要状态管理
Flutter 的 UI 完全由代码控制,几乎所有业务逻辑都要通过“状态(State)”来驱动界面渲染。当应用规模变大时,如果仅依赖散落在各个 StatefulWidget 中的 setState,会出现以下问题:
- 状态分散难以复用:同一个数据需要在多个页面或组件中共享时,若各自用
setState,会导致重复逻辑、同步困难。 - 难以追踪重建范围:不清楚何时某个组件会因父级
setState被无谓重建,影响性能。 - 测试不便:将业务逻辑与 UI 紧耦合,单元测试需要额外包装 Widget,很不直观。
- 生命周期管理复杂:多个页面同时需要监听同一份数据变化时,如果不集中管理会出现内存泄漏、回调错乱等问题。
因此,Flutter 社区发展出多种状态管理方案,从简单到复杂,满足不同项目需求。本文将依次介绍并对比它们的原理和实战用法。
二、基础:setState 与局部状态
2.1 setState 的原理
- 原理概述
每个StatefulWidget对应一个State<T>对象,内部持有一颗“依赖于build()函数重绘”的 Widget 树。当调用setState(() { ... })时,Flutter 会标记这个State为“需要重建”(dirty),并在下一个帧(frame)时调用其build(),从而重新渲染该子树。 示意图
┌────────────────────────────┐ │ StatefulWidget │ │ (与其对应的 State 对象) │ └────────────────────────────┘ │ setState() 被调用 │ ┌────────────────────────────┐ │ State<StatefulWidget> │───► 被标记为 dirty │ build() 执行重绘 │ └────────────────────────────┘注意事项
- 只要
build()中有setState,整个 Widget 子树都会重建,可能会导致无谓布局和绘制。 setState不要在build()、initState()之外的异步回调中频繁调用,否则难以控制重建节奏。
- 只要
2.2 局部状态如何拆分
当页面中存在多块需要维护状态的区域时,不建议把所有状态都放在同一个 StatefulWidget 中,而应拆分成多个更小的 StatefulWidget。这样:
- 当一块状态变化时,只会触发对应局部
build(),其余部分不会被重建。 - 可提高性能,也增强组件复用性。
代码示例:拆分计数器
import 'package:flutter/material.dart';
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('局部状态拆分示例')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Padding(padding: EdgeInsets.all(8), child: Text('上方静态内容')),
SizedBox(height: 20),
CounterSection(), // 单独的计数器组件
SizedBox(height: 20),
StaticTextWidget(), // 不依赖任何状态,永远不重建
],
),
);
}
}
class CounterSection extends StatefulWidget {
const CounterSection({super.key});
@override
State<CounterSection> createState() => _CounterSectionState();
}
class _CounterSectionState extends State<CounterSection> {
int _count = 0;
@override
Widget build(BuildContext context) {
print('CounterSection build'); // 可在控制台观察重建
return Column(
children: [
Text('计数:$_count'),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('++'),
),
],
);
}
}
class StaticTextWidget extends StatelessWidget {
const StaticTextWidget({super.key});
@override
Widget build(BuildContext context) {
print('StaticTextWidget build'); // 只会执行一次
return const Text('永不重建的静态文本');
}
}解释:
CounterSection中的setState只会重建自身;StaticTextWidget不含setState,且为const,永远不会重建。
2.3 代码示例与图解
图解:局部状态拆分前后差异
┌────────────────────────────┐ ┌────────────────────────────┐
│ SingleStatefulPage │ │ StatelessPage │
│ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
│ │ StatefulContent │◄─┤ setState()│ CounterSection │◄─┤ setState()
│ │ (所有状态混在一起) │ └─────────────────────┘
│ └─────────────────────┘ │ │ ┌─────────────────────┐ │
│ │ │ │ StaticTextWidget │ │
│ │ │ │ (永不重建) │ │
│ │ │ └─────────────────────┘ │
└────────────────────────────┘ └────────────────────────────┘- 左图:单个
StatefulWidget管理所有状态,每次setState都会重建全部内容; - 右图:将可分离的部分拆为多个子组件,只重建所需部分。
三、进阶:InheritedWidget 与通知机制
在全局或跨多个页面共享状态时,单靠局部 setState 已难以满足需求,这时可以使用更底层的 InheritedWidget 及其衍生组件。
3.1 InheritedWidget 原理简介
核心概念
InheritedWidget是一个特殊的 Widget,放在 Widget 树上方,将数据“注入”到其子树,供后代通过context.dependOnInheritedWidgetOfExactType<YourInherited>()获取。- 当
InheritedWidget的updateShouldNotify(oldWidget)返回true时,所有依赖该实例的子树都会收到通知并重建。
适用场景
- 某些数据需要在整个应用或大部分子树中共享,如“主题”、“语言”、“当前登录用户”等。
- 比较低级,一般应用会用基于
InheritedWidget的封装(如Provider、Bloc、Riverpod)来简化。
基本实现示例
// 1. 自定义 InheritedWidget
class CounterInherited extends InheritedWidget {
final int count;
final Widget child;
const CounterInherited({
super.key,
required this.count,
required this.child,
}) : super(child: child);
static CounterInherited? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterInherited>();
}
@override
bool updateShouldNotify(CounterInherited oldWidget) {
// 只有 count 变化时才通知子树更新
return oldWidget.count != count;
}
}
// 2. 顶层 StatefulWidget 持有状态
class InheritedCounterPage extends StatefulWidget {
const InheritedCounterPage({super.key});
@override
State<InheritedCounterPage> createState() => _InheritedCounterPageState();
}
class _InheritedCounterPageState extends State<InheritedCounterPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return CounterInherited(
count: _count,
child: Scaffold(
appBar: AppBar(title: const Text('InheritedWidget 示例')),
body: const CounterDisplay(), // 读取 count
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _count++),
child: const Icon(Icons.add),
),
),
);
}
}
// 3. 子孙 Widget 通过 of() 获取 count
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
final inherited = CounterInherited.of(context);
final count = inherited?.count ?? 0;
print('CounterDisplay build: $count');
return Center(child: Text('当前计数:$count', style: const TextStyle(fontSize: 24)));
}
}运行流程
_InheritedCounterPageState中_count变化 →setState()→ 重新构建CounterInherited;CounterInherited.updateShouldNotify(old)返回true→ 所有依赖CounterInherited的子 Widget(如CounterDisplay)重新构建;CounterDisplay.build()中通过CounterInherited.of(context)取到最新count并更新 UI。
3.2 自定义 InheritedNotifier / InheritedModel
InheritedNotifier
- 当状态变动时,常用
ChangeNotifier作为状态持有者,再包装成InheritedNotifier传递给子树。 - 子树只要依赖,
ChangeNotifier.notifyListeners()就能触发InheritedNotifier更新。
// 1. 定义 ChangeNotifier class CounterNotifier extends ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); } } // 2. InheritedNotifier 封装 class CounterInheritedNotifier extends InheritedNotifier<CounterNotifier> { const CounterInheritedNotifier({ super.key, required CounterNotifier notifier, required Widget child, }) : super(notifier: notifier, child: child); static CounterNotifier of(BuildContext context) { final inherited = context .dependOnInheritedWidgetOfExactType<CounterInheritedNotifier>(); return inherited!.notifier!; } } // 3. 在顶层提供 notifier class InheritedNotifierPage extends StatefulWidget { const InheritedNotifierPage({super.key}); @override State<InheritedNotifierPage> createState() => _InheritedNotifierPageState(); } class _InheritedNotifierPageState extends State<InheritedNotifierPage> { final _notifier = CounterNotifier(); @override Widget build(BuildContext context) { return CounterInheritedNotifier( notifier: _notifier, child: Scaffold( appBar: AppBar(title: const Text('InheritedNotifier 示例')), body: const CounterDisplay(), floatingActionButton: FloatingActionButton( onPressed: () => _notifier.increment(), child: const Icon(Icons.add), ), ), ); } } // 4. 子 Widget 订阅 notifier class CounterDisplay extends StatelessWidget { const CounterDisplay({super.key}); @override Widget build(BuildContext context) { final notifier = CounterInheritedNotifier.of(context); final count = notifier.count; print('CounterDisplay build: $count'); return Center(child: Text('当前计数:$count', style: const TextStyle(fontSize: 24))); } }- 当状态变动时,常用
InheritedModel
- 允许按“关键字”区分通知粒度,只有使用到某个 aspect(方面)的子 Widget 在该“方面”变化时才重建。
- 适合一个 InheritedWidget 管理多种状态,每个子 Widget 只关心其中一小部分变化。
// 示例略;通常直接用 Provider 或 Riverpod 即可,无需手动 InheritedModel
3.3 代码示例与示意图
图解:InheritedWidget 更新流程
┌─────────────────────────────────┐
│ MyApp ──► InheritedCounterPage │
│ (Stateful, 持有 _count) │
│ │ │ │
│ │ setState() │
│ ▼ ▼ │
│ CounterInherited (count=x) │◄─── 数据分发
│ │ │
│ ┌────────┴────────┐ │
│ │ 子树 Widget │ │
│ │ CounterDisplay │◄─── dependOnInheritedWidgetOfExactType
│ └─────────────────┘ │
└─────────────────────────────────┘- 只要
CounterInherited的count变化(updateShouldNotify返回true),依赖它的子树就会重新构建。
四、推荐:Provider 生态
在实际项目中,InheritedWidget 虽然灵活,但较为底层,写起来冗长。不少团队更倾向于使用基于它的封装——Provider。
4.1 Provider 基本用法
Provider将InheritedWidget、ChangeNotifier、Consumer等都做了封装,使得状态管理更加简洁。- 只需在树顶用
ChangeNotifierProvider(或Provider、StateNotifierProvider等)包装,然后在子孙组件里用context.watch<T>()、context.read<T>()、Consumer<T>()等方式获取或监听数据。
简单示例:计数器
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 1. 定义 ChangeNotifier
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// 2. 在根部提供 CounterModel
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const CounterProviderPage(),
);
}
}
// 3. 子组件使用 Provider
class CounterProviderPage extends StatelessWidget {
const CounterProviderPage({super.key});
@override
Widget build(BuildContext context) {
// 通过 context.watch<CounterModel>() 获取并订阅变化
final counter = context.watch<CounterModel>().count;
print('CounterProviderPage build: $counter');
return Scaffold(
appBar: AppBar(title: const Text('Provider 示例')),
body: Center(child: Text('当前计数:$counter', style: const TextStyle(fontSize: 24))),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
);
}
}解释:
ChangeNotifierProvider创建并提供CounterModel;context.watch<CounterModel>()在build时订阅CounterModel,当notifyListeners()触发时,自动重建;context.read<CounterModel>()仅一次性获取实例,不订阅变化,可用来调用increment()。
4.2 ChangeNotifierProvider 与 Consumer
Consumer<T>- 如果只想让 Widget 树中某个子树依赖变化,避免整个
build重新执行,可用Consumer包裹一小段子树。 - 例如在大型页面里,仅头部计数器区域需要跟随
CounterModel.count更新,其它部分不用。
- 如果只想让 Widget 树中某个子树依赖变化,避免整个
class ConsumerExamplePage extends StatelessWidget {
const ConsumerExamplePage({super.key});
@override
Widget build(BuildContext context) {
print('ConsumerExamplePage build'); // 仅执行一次
return Scaffold(
appBar: AppBar(title: const Text('Consumer 示例')),
body: Column(
children: [
const StaticWidget(), // 静态区
// 只让 CounterConsumer 重建
Consumer<CounterModel>(
builder: (context, counterModel, child) {
print('CounterConsumer build: ${counterModel.count}');
return Text('计数:${counterModel.count}', style: const TextStyle(fontSize: 24));
},
),
const StaticWidget(), // 另一静态区
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
);
}
}效果:
- 点击按钮只会触发
CounterConsumer内的builder,外层ConsumerExamplePage.build和StaticWidget都不会重建。
- 点击按钮只会触发
4.3 FutureProvider / StreamProvider
FutureProvider- 当状态需要来自异步操作(如网络请求)时,用
FutureProvider自动管理Future的生命周期,并根据AsyncValue<T>(data、loading、error)自动刷新。
- 当状态需要来自异步操作(如网络请求)时,用
Future<String> fetchUsername() async {
await Future.delayed(const Duration(seconds: 2));
return '张三';
}
class UserNamePage extends StatelessWidget {
const UserNamePage({super.key});
@override
Widget build(BuildContext context) {
return FutureProvider<String>(
create: (_) => fetchUsername(),
initialData: '加载中...',
child: Scaffold(
appBar: AppBar(title: const Text('FutureProvider 示例')),
body: Center(
child: Consumer<String>(
builder: (context, username, child) {
return Text('用户名:$username', style: const TextStyle(fontSize: 24));
},
),
),
),
);
}
}StreamProvider- 若状态来自持续变化的数据流(如 WebSocket、数据库监听),可用
StreamProvider。
- 若状态来自持续变化的数据流(如 WebSocket、数据库监听),可用
Stream<int> counterStream() async* {
int i = 0;
while (true) {
await Future.delayed(const Duration(seconds: 1));
yield i++;
}
}
class StreamCounterPage extends StatelessWidget {
const StreamCounterPage({super.key});
@override
Widget build(BuildContext context) {
return StreamProvider<int>(
create: (_) => counterStream(),
initialData: 0,
child: Scaffold(
appBar: AppBar(title: const Text('StreamProvider 示例')),
body: Center(
child: Consumer<int>(
builder: (context, count, child) {
return Text('流计数:$count', style: const TextStyle(fontSize: 24));
},
),
),
),
);
}
}4.4 代码示例与图解
图解:Provider 数据流向
┌────────────────────────────────────────┐
│ ChangeNotifierProvider │
│ (创建 CounterModel 并注入子树) │
│ ──► CounterModel │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ │ Consumer<CounterModel> │ │
│ │ (监听并自动重建部分子树) │ │
│ └───────────────────────────────┘ │
│ │ │
│ FloatingActionButton │
│ (调用 CounterModel.increment) │
└────────────────────────────────────────┘- 当子组件调用
increment()时,ChangeNotifier.notifyListeners()会通知所有依赖的Consumer或context.watch重新构建。
五、替代与扩展:Riverpod
虽然 Provider 生态非常成熟,但其依赖于 InheritedWidget,在某些场景下会有一些限制。Riverpod 由 Provider 作者推出,解决了一些 Provider 的局限性。
5.1 为什么选择 Riverpod
与 Widget 树解耦:
Provider的ChangeNotifierProvider必须放在 Widget 树中,Riverpod的ProviderScope也要放在最外面,但内部 Provider 的创建与使用不依赖于BuildContext。
可测试性更强:
- 在
Riverpod中,可以轻易在单位测试中 override(覆盖)任何 Provider,无需依赖 Widget 测试。
- 在
消除
context范围限制:context.read<T>()只能在Widget的build()中使用,而Riverpod的ref.read()可在任意地方使用。
更丰富的高级特性:
- 支持
Provider之间的依赖注入、自动销毁、状态复用(AutoDispose)、Family、AsyncValue等。
- 支持
5.2 Provider vs Riverpod 对比
| 特性 | Provider | Riverpod |
|---|---|---|
| 依赖注入 | 通过 ChangeNotifierProvider 等包裹 Widget 树 | 通过 ProviderScope,并用 ref.watch / ref.read 在任意地方获取状态 |
| 生命周期管理 | 依赖 InheritedWidget,与 Widget 生存期紧耦合 | 独立于 Widget 树,可使用 autoDispose 自动释放 |
| 测试友好 | 需要借助 Provider 包装 Mock,并在 Widget 测试中使用 | 可以直接在单元测试中用 ProviderContainer override Provider,不需 Widget |
| Async 状态封装 | FutureProvider / StreamProvider | FutureProvider / StreamProvider + AsyncValue 强大的错误 / 载入 / 数据模型 |
| 依赖 Provider 依赖链 | 通过 context.read<A>().someMethod() | providerA = Provider((ref) => ...); providerB = Provider((ref) => ref.watch(providerA) + 1); |
5.3 Riverpod 代码示例
下面演示一个用 Riverpod 实现的计数器示例。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. 定义一个 StateProvider 管理简单的 int 状态
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) {
return MaterialApp(home: const CounterRiverpodPage());
}
}
class CounterRiverpodPage extends ConsumerWidget {
const CounterRiverpodPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 2. 通过 ref.watch 订阅 counterProvider
final count = ref.watch(counterProvider);
print('CounterRiverpodPage build: $count');
return Scaffold(
appBar: AppBar(title: const Text('Riverpod 示例')),
body: Center(child: Text('当前计数:$count', style: const TextStyle(fontSize: 24))),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 3. 通过 ref.read 修改状态
ref.read(counterProvider.notifier).state++;
},
child: const Icon(Icons.add),
),
);
}
}解释
counterProvider是一个StateProvider<int>,管理一个整数状态。ref.watch(counterProvider)相当于订阅了该状态,state值变化时会触发build()。ref.read(counterProvider.notifier).state++修改状态,自动通知所有订阅该 Provider 的 Widget。
优点
- 不依赖于
BuildContext,可以在回调、API 层直接使用ref.read。 - 自动处理生命周期,若页面销毁,相关状态可用
autoDispose自动释放。
- 不依赖于
六、复杂场景:Bloc(业务逻辑组件)
当应用业务逻辑复杂,需要更明确地控制事件与状态转换时,Bloc(Business Logic Component)模式常用于将 UI 与逻辑完全分离。
6.1 Bloc 概念与优缺点
概念
Bloc强调“事件 -> 处理逻辑 -> 状态” 三个阶段,通过流(Stream)来传递这些数据。- 常用库:
flutter_bloc,提供BlocProvider、BlocBuilder、BlocListener等封装。
优点
- 业务逻辑与 UI 完全解耦,易于测试。
- 事件与状态变化可序列化,便于调试、日志与回放。
- 支持复杂场景,如并行请求、序列化操作。
缺点
- 学习曲线较陡,新手理解事件流与状态流较为困难。
- 代码量相对较多,模板化较重。
6.2 Cubit 简化版本
Cubit 是 Bloc 的轻量化版本,只保留状态管理,不需要事件类,最小化模板。适用于中等复杂度场景。
Cubit 代码示例:计数器
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// 1. 定义 CounterCubit,继承 Cubit<int>
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0); // 初始状态为 0
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
void main() {
runApp(const MyBlocApp());
}
class MyBlocApp extends StatelessWidget {
const MyBlocApp({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
// 2. 在根部提供 CounterCubit
create: (_) => CounterCubit(),
child: const MaterialApp(home: CounterBlocPage()),
);
}
}
class CounterBlocPage extends StatelessWidget {
const CounterBlocPage({super.key});
@override
Widget build(BuildContext context) {
print('CounterBlocPage build');
return Scaffold(
appBar: AppBar(title: const Text('Cubit 示例')),
body: Center(
// 3. BlocBuilder 监听 CounterCubit 的状态
child: BlocBuilder<CounterCubit, int>(
builder: (context, count) {
print('BlocBuilder build: $count');
return Text('当前计数:$count', style: const TextStyle(fontSize: 24));
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'inc',
onPressed: () => context.read<CounterCubit>().increment(),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'dec',
onPressed: () => context.read<CounterCubit>().decrement(),
child: const Icon(Icons.remove),
),
],
),
);
}
}流程
CounterCubit初始化state=0;BlocProvider将CounterCubit注入子树;BlocBuilder订阅CounterCubit,每当调用emit()改变状态时,builder会被触发。
6.3 代码示例与事件流图解
图解:Cubit 状态流
┌───────────────────────────────────────────┐
│ CounterCubit (extends Cubit<int>) │
│ Initial state: 0 │
│ │ │
│ increment()/decrement() │
│ │ │
│ emit(newState) ──► 状态变更通知 │
│ │ │
│ BlocBuilder<CounterCubit, int> listens │
│ to state and rebuilds on change │
└───────────────────────────────────────────┘BlocBuilder对应 UI 部分,只会重建依赖状态的文本 Widget,其余页面部分不受影响。
七、实战案例:Todo 应用对比多种方案
下面通过一个Todo 列表应用,逐步用三种不同的状态管理方案实现:setState、Provider、Bloc。对比它们在实现难度、可维护性、性能、测试友好等方面的差异。
7.1 需求描述与核心功能
- 显示一个 Todo 列表(
List<TodoItem>),每条包含title、completed。 - 可以新增 Todo:弹出对话框输入标题后加入列表。
- 可以切换 Todo 的完成状态(勾选)。
- 显示“已完成数量 / 总数量” 的统计。
7.2 setState 实现
7.2.1 完整代码
import 'package:flutter/material.dart';
class TodoItem {
String title;
bool completed;
TodoItem({required this.title, this.completed = false});
}
class TodoSetStatePage extends StatefulWidget {
const TodoSetStatePage({super.key});
@override
State<TodoSetStatePage> createState() => _TodoSetStatePageState();
}
class _TodoSetStatePageState extends State<TodoSetStatePage> {
final List<TodoItem> _todos = [];
void _addTodo() async {
final title = await showDialog<String>(
context: context,
builder: (context) {
String input = '';
return AlertDialog(
title: const Text('新建 Todo'),
content: TextField(onChanged: (v) => input = v),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(context, input), child: const Text('确定')),
],
);
},
);
if (title != null && title.trim().isNotEmpty) {
setState(() {
_todos.add(TodoItem(title: title.trim()));
});
}
}
void _toggleCompleted(int index) {
setState(() {
_todos[index].completed = !_todos[index].completed;
});
}
@override
Widget build(BuildContext context) {
final total = _todos.length;
final completedCount = _todos.where((t) => t.completed).length;
return Scaffold(
appBar: AppBar(title: const Text('Todo (setState)')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Text('已完成 $completedCount / $total', style: const TextStyle(fontSize: 18)),
),
Expanded(
child: ListView.builder(
itemCount: _todos.length,
itemBuilder: (ctx, i) {
final item = _todos[i];
return CheckboxListTile(
title: Text(item.title),
value: item.completed,
onChanged: (_) => _toggleCompleted(i),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _addTodo,
child: const Icon(Icons.add),
),
);
}
}7.2.2 分析
- 优点:实现简单,上手快,适合小型页面或状态范围很局部的场景。
缺点:当业务增大时:
_todos集合仅在该State内,若从其他页面也需要访问 Todo 列表,就需要通过回调或路由参数传递;- 统计逻辑直接在
build中计算,若列表很大,会导致每次重建都遍历一次; - 难以单独测试业务逻辑。
7.3 Provider 实现
7.3.1 定义 Model 与 Provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 1. Model
class TodoItem {
String title;
bool completed;
TodoItem({required this.title, this.completed = false});
}
// 2. ChangeNotifier 提供全局状态
class TodoModel extends ChangeNotifier {
final List<TodoItem> _todos = [];
List<TodoItem> get todos => List.unmodifiable(_todos);
void addTodo(String title) {
_todos.add(TodoItem(title: title));
notifyListeners();
}
void toggle(int index) {
_todos[index].completed = !_todos[index].completed;
notifyListeners();
}
int get total => _todos.length;
int get completedCount => _todos.where((t) => t.completed).length;
}7.3.2 主应用与页面
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => TodoModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(home: const TodoProviderPage());
}
}
class TodoProviderPage extends StatelessWidget {
const TodoProviderPage({super.key});
Future<void> _addTodoDialog(BuildContext context) async {
final model = context.read<TodoModel>();
final title = await showDialog<String>(
context: context,
builder: (ctx) {
String input = '';
return AlertDialog(
title: const Text('新建 Todo'),
content: TextField(onChanged: (v) => input = v),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, input), child: const Text('确定')),
],
);
},
);
if (title != null && title.trim().isNotEmpty) {
model.addTodo(title.trim());
}
}
@override
Widget build(BuildContext context) {
print('TodoProviderPage build');
return Scaffold(
appBar: AppBar(title: const Text('Todo (Provider)')),
body: Column(
children: [
// 3. Consumer 仅重建统计部分
Consumer<TodoModel>(
builder: (context, model, child) {
return Padding(
padding: const EdgeInsets.all(8),
child: Text(
'已完成 ${model.completedCount} / ${model.total}',
style: const TextStyle(fontSize: 18),
),
);
},
),
const Divider(),
// 4. Consumer 重建列表
Expanded(
child: Consumer<TodoModel>(
builder: (context, model, child) {
return ListView.builder(
itemCount: model.todos.length,
itemBuilder: (ctx, i) {
final item = model.todos[i];
return CheckboxListTile(
title: Text(item.title),
value: item.completed,
onChanged: (_) => model.toggle(i),
);
},
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addTodoDialog(context),
child: const Icon(Icons.add),
),
);
}
}分析
TodoModel抽离了业务逻辑:新增、切换完成状态、统计;- 页面层只专注于 UI,通过
Consumer或context.watch监听TodoModel状态变化,重建相应子树; - 逻辑与 UI 解耦,可单独对
TodoModel编写单元测试。
7.4 Bloc 实现
7.4.1 定义事件与状态
import 'package:equatable/equatable.dart';
class TodoItem {
String title;
bool completed;
TodoItem({required this.title, this.completed = false});
}
// 1. 事件 (Event)
abstract class TodoEvent extends Equatable {
@override
List<Object?> get props => [];
}
class AddTodoEvent extends TodoEvent {
final String title;
AddTodoEvent(this.title);
@override
List<Object?> get props => [title];
}
class ToggleTodoEvent extends TodoEvent {
final int index;
ToggleTodoEvent(this.index);
@override
List<Object?> get props => [index];
}
// 2. 状态 (State)
class TodoState extends Equatable {
final List<TodoItem> todos;
const TodoState(this.todos);
@override
List<Object?> get props => [todos];
}7.4.2 定义 Bloc
import 'package:flutter_bloc/flutter_bloc.dart';
class TodoBloc extends Bloc<TodoEvent, TodoState> {
TodoBloc() : super(const TodoState([])) {
on<AddTodoEvent>((event, emit) {
final newList = List<TodoItem>.from(state.todos)
..add(TodoItem(title: event.title));
emit(TodoState(newList));
});
on<ToggleTodoEvent>((event, emit) {
final newList = List<TodoItem>.from(state.todos);
newList[event.index].completed = !newList[event.index].completed;
emit(TodoState(newList));
});
}
}7.4.3 页面层
void main() {
runApp(const MyBlocTodoApp());
}
class MyBlocTodoApp extends StatelessWidget {
const MyBlocTodoApp({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TodoBloc(),
child: const MaterialApp(home: TodoBlocPage()),
);
}
}
class TodoBlocPage extends StatelessWidget {
const TodoBlocPage({super.key});
Future<void> _addTodoDialog(BuildContext context) async {
final bloc = context.read<TodoBloc>();
final title = await showDialog<String>(
context: context,
builder: (ctx) {
String input = '';
return AlertDialog(
title: const Text('新建 Todo'),
content: TextField(onChanged: (v) => input = v),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, input), child: const Text('确定')),
],
);
},
);
if (title != null && title.trim().isNotEmpty) {
bloc.add(AddTodoEvent(title.trim()));
}
}
@override
Widget build(BuildContext context) {
print('TodoBlocPage build');
return Scaffold(
appBar: AppBar(title: const Text('Todo (Bloc)')),
body: Column(
children: [
// 1. BlocBuilder 只监听状态变化
BlocBuilder<TodoBloc, TodoState>(
buildWhen: (previous, current) =>
previous.todos.length != current.todos.length ||
previous.todos.where((t) => t.completed).length !=
current.todos.where((t) => t.completed).length,
builder: (context, state) {
final total = state.todos.length;
final completed = state.todos.where((t) => t.completed).length;
return Padding(
padding: const EdgeInsets.all(8),
child: Text('已完成 $completed / $total', style: const TextStyle(fontSize: 18)),
);
},
),
const Divider(),
Expanded(
child: BlocBuilder<TodoBloc, TodoState>(
buildWhen: (previous, current) => previous.todos != current.todos,
builder: (context, state) {
return ListView.builder(
itemCount: state.todos.length,
itemBuilder: (ctx, i) {
final item = state.todos[i];
return CheckboxListTile(
title: Text(item.title),
value: item.completed,
onChanged: (_) => context.read<TodoBloc>().add(ToggleTodoEvent(i)),
);
},
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addTodoDialog(context),
child: const Icon(Icons.add),
),
);
}
}7.4.4 性能与可维护性对比
| 特性 | setState | Provider | Bloc |
|---|---|---|---|
| 学习成本 | 低 | 中 | 较高 |
| 代码量 | 少 | 适中 | 较多 |
| 解耦程度 | 差 | 中 | 优秀 |
| 可测试性 | 较差 | 较好 | 极佳 |
| 频繁重建控制 | 需手动拆分局部组件 | 通过 Consumer、context.watch 控制 | 通过 buildWhen 精准控制 |
| 业务逻辑复用 | 困难 | 较方便 | 容易 |
| 适用场景 | 页面很小、状态范围局部 | 小-中型应用,全局少量共享状态 | 大型应用、业务逻辑复杂、多模块解耦场景 |
八、总结与最佳实践
先从最简单做起
- 对于非常简单的页面,只需局部
setState即可;避免过度设计。 - 将状态拆分到最小粒度的
StatefulWidget,只重建必要子树。
- 对于非常简单的页面,只需局部
中型场景首选
Provider系列方案- 当需要跨多个页面或组件共享状态时,
Provider(或Riverpod)能快速上手,代码简洁,易于重构、测试。 - 使用
Consumer、context.watch可以精准控制重建范围,避免 UI 卡顿。
- 当需要跨多个页面或组件共享状态时,
复杂业务逻辑使用
Bloc/Cubit- 当业务逻辑非常复杂,需要事件驱动、严格分层、可测试时,选择
Bloc或其简化版Cubit。 - 通过
buildWhen、listenWhen、BlocListener等可精细控制 UI 更新,结合Equatable简化状态比较。
- 当业务逻辑非常复杂,需要事件驱动、严格分层、可测试时,选择
更现代的工具
Riverpod- 如果项目注重自动销毁、依赖注入灵活性与测试友好性,可直接使用
Riverpod。 Riverpod让状态定义与使用更加直观,且易于 Mock 和覆盖,极大简化测试流程。
- 如果项目注重自动销毁、依赖注入灵活性与测试友好性,可直接使用
避免状态管理的常见错误
- 不要把业务逻辑写在
build()中,避免大量计算拖慢 UI; - 不要在
initState()、build()中直接执行耗时操作,应使用异步 Provider、FutureProvider、Bloc 在事件中处理; - 不要频繁调用
setState,避免重建范围过大; - 注意资源释放:对于
Stream、Timer、AnimationController等,应在dispose()中手动释放,或使用autoDispose(Riverpod)自动释放。
- 不要把业务逻辑写在
持续监测与优化
- 在开发过程中使用 Flutter DevTools 的 Performance 和 Flutter Inspector 查看到底哪些组件在重建、哪些操作在占用大量 CPU;
- 定期做性能评测(Profile 模式),保证不同方案在设备上的流畅度。
结语:
Flutter 状态管理无“万金油”方案,需结合项目规模、团队熟悉度及性能要求选型。本文所述的从“纯setState”到“Provider/Riverpod”再到“Bloc”的三种实践思路,可覆盖绝大部分真实场景。希望你能在实战中找到最适合自己团队和项目的状态管理方式,写出高质量、易维护且性能出色的 Flutter 应用。
评论已关闭