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
Provider
vsRiverpod
对比 - 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 应用。
评论已关闭