状态管理
状态管理是 Flutter 开发的核心问题。本文从最基础的 setState 开始,逐步讲解 InheritedWidget、Provider、Riverpod,帮你在不同场景下选择合适的方案。
什么是状态管理?
想象一个场景:你在 App 顶部有一个购物车图标,显示商品数量;在商品列表页点击"加入购物车"后,顶部的数字要立刻更新。
问题:这两个组件不在同一个 Widget 树层级,怎么让它们共享同一份数据?
答案:这就是状态管理要解决的事。从简到繁,Flutter 有多种方案。
方案总览与选择
| 方案 | 难度 | 适用场景 |
|---|---|---|
setState | ⭐ | 组件内部简单状态 |
InheritedWidget | ⭐⭐ | 自定义状态共享(理解原理) |
Provider | ⭐⭐ | 中小型项目状态共享 |
Riverpod | ⭐⭐⭐ | Provider 的改进版,更安全更灵活 |
Bloc / Cubit | ⭐⭐⭐ | 大型项目、复杂业务逻辑 |
GetX | ⭐ | 快速开发(有维护风险) |
选择指南:
项目规模 / 复杂度
│
├── 小型 Demo / 学习阶段
│ └── setState
│
├── 中型项目(几个页面共享状态)
│ └── Provider
│
├── 大型项目(复杂状态逻辑)
│ └── Riverpod 或 Bloc
│
└── 追求开发速度
└── GetX(但要注意维护风险)推荐学习路线: setState → Provider → Riverpod
一、setState — 最基础的状态更新
setState() 是 Flutter 最基础的状态更新方式,用于在 StatefulWidget 的 State 类中通知框架状态已变更。
基本用法
import 'package:flutter/material.dart';
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
void _increment() {
// ─── ★ setState ──────────────
setState(() {
_count++; // 在回调中修改状态
});
// ─── ☆ setState ──────────────
// setState 调用后,build() 会被重新执行
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('setState 示例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$_count', style: const TextStyle(fontSize: 48)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _increment,
child: const Text('+1'),
),
],
),
),
);
}
}工作原理
用户操作 → 调用 setState() → 框架标记dirty → 框架调用 build() → UI 更新注意事项
不要在 setState 中执行耗时操作:
import 'package:flutter/material.dart';
// ❌ 错误
void _loadData() {
setState(() {
final data = http.get(Uri.parse(url)); // 耗时操作!
_items = data;
});
}
// ✅ 正确
void _loadData() async {
setState(() => _isLoading = true); // 先显示 loading
final data = await http.get(Uri.parse(url));
if (!mounted) return;
setState(() {
_items = data;
_isLoading = false;
});
}不要在 build 中调用 setState:
// ❌ 错误:会导致无限循环
@override
Widget build(BuildContext context) {
setState(() { _count++; }); // 在 build 中调用 setState → 无限循环!
return Text('$_count');
}异步操作后检查 mounted:
import 'package:flutter/material.dart';
class DataPage extends StatefulWidget {
const DataPage({super.key});
@override
State<DataPage> createState() => _DataPageState();
}
class _DataPageState extends State<DataPage> {
String _data = '';
void _fetchData() async {
final result = await Future.delayed(const Duration(seconds: 2));
if (!mounted) return; // 组件已被销毁
setState(() => _data = result.toString());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('mounted 检查')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_data.isEmpty ? '暂无数据' : _data),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _fetchData,
child: const Text('获取数据'),
),
],
),
),
);
}
}setState 的局限性
| 局限 | 说明 |
|---|---|
| 仅限当前 State | 无法在兄弟/跨层级组件间共享状态 |
| 每次全量重建 | build() 整体重建,大型 Widget 树性能较差 |
| 状态传递麻烦 | 深层传递需要层层回调(callback hell) |
二、InheritedWidget — 跨层级数据共享
InheritedWidget 是 Flutter 实现数据从上向下共享的基础机制。Provider 等状态管理方案都是基于它封装的。
手动实现示例
// 1. 创建 InheritedWidget
class CounterData extends InheritedWidget {
const CounterData({
super.key,
required this.count,
required this.onIncrement,
required super.child,
});
final int count;
final VoidCallback onIncrement;
// 静态方法:让子组件方便获取数据
static CounterData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterData>()!;
}
// 决定是否通知依赖的子组件重建
@override
bool updateShouldNotify(CounterData oldWidget) {
return count != oldWidget.count;
}
}
// 2. 在 Widget 树中提供数据
class CounterProvider extends StatefulWidget {
final Widget child;
const CounterProvider({super.key, required this.child});
@override
State<CounterProvider> createState() => _CounterProviderState();
}
class _CounterProviderState extends State<CounterProvider> {
int _count = 0;
void _increment() {
setState(() => _count++);
}
@override
Widget build(BuildContext context) {
return CounterData(
count: _count,
onIncrement: _increment,
child: widget.child,
);
}
}
// 3. 在子组件中使用
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
final data = CounterData.of(context); // 获取数据
return Text('计数: ${data.count}');
}
}核心方法
dependOnInheritedWidgetOfExactType:注册依赖,数据变化时会重建该组件getInheritedWidgetOfExactType:不注册依赖,数据变化时不会重建(只读取一次)
实际使用建议
手写 InheritedWidget 比较繁琐。在实际项目中,推荐直接使用 Provider,它是对 InheritedWidget 的优雅封装。理解 InheritedWidget 的原理有助于理解 Provider 的工作方式。
三、Provider — 官方推荐的状态管理
安装
flutter pub add provider核心思路(3 步)
1️⃣ 创建数据模型 → 2️⃣ 在顶层注入 → 3️⃣ 在子组件中读取第一步:创建数据模型
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // 告诉所有监听者:数据变了
}
void decrement() {
_count--;
notifyListeners();
}
}⚠️ 忘记写
notifyListeners()是新手最常见的错误——数据改了但 UI 不更新。
第二步:在顶层注入
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}第三步:在子组件中读取
// watch — 显示数据(数据变了会自动重建)
class CounterText extends StatelessWidget {
const CounterText({super.key});
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Text('当前计数:${counter.count}', style: const TextStyle(fontSize: 24));
}
}
// read — 调用方法(不监听变化,不会触发重建)
class IncrementButton extends StatelessWidget {
const IncrementButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Text('+1'),
);
}
}watch vs read(最重要)
watch | read | |
|---|---|---|
| 用途 | 读取数据并监听变化 | 调用方法 / 一次性读取 |
| 数据变了会重建? | ✅ 会 | ❌ 不会 |
| 典型场景 | 显示文本、列表等 | 按钮点击回调 |
记住这条规则:要显示数据 → watch,要调用方法 → read
Consumer — 精确控制重建范围
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// ... 假设 CounterModel 和 ChangeNotifierProvider 已定义
class CounterConsumerPage extends StatelessWidget {
const CounterConsumerPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Consumer 示例')),
body: Center(
child: Consumer<CounterModel>(
builder: (context, counter, child) {
// 只有这里会重建,其他部分不会
return Text(
'计数:${counter.count}',
style: const TextStyle(fontSize: 48),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
);
}
}MultiProvider — 注入多个数据模型
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => UserModel()),
ChangeNotifierProvider(create: (_) => CartModel()),
ChangeNotifierProvider(create: (_) => ThemeModel()),
],
child: const MyApp(),
),
);
}Provider 类型速查
| 类型 | 适用场景 |
|---|---|
ChangeNotifierProvider | 需要修改和监听的状态(最常用) |
Provider | 只读的静态值 |
FutureProvider | 异步获取的数据 |
StreamProvider | 持续的数据流 |
ProxyProvider | 依赖其他 Provider 的值 |
完整示例:购物车
// ========== 数据模型 ==========
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();
}
void removeAt(int index) {
_items.removeAt(index);
notifyListeners();
}
}
// ========== 顶层注入 ==========
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MyApp(),
),
);
}
// ========== 显示购物车数量 ==========
class CartBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cart = context.watch<CartModel>();
return Text('购物车 (${cart.count})');
}
}
// ========== 添加商品按钮 ==========
class AddToCartButton extends StatelessWidget {
final String item;
const AddToCartButton({super.key, required this.item});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<CartModel>().add(item),
child: const Text('加入购物车'),
);
}
}常见错误排查
| 现象 | 原因 | 解决 |
|---|---|---|
| 数据改了但 UI 没更新 | 忘记调用 notifyListeners() | 在修改数据的方法末尾加上 |
报错 ProviderNotFoundException | 没在顶层注入,或类型写错了 | 检查 ChangeNotifierProvider 是否在父级 |
| 按钮点击后整个页面重建 | 在回调中用了 watch | 改用 read |
四、Riverpod — Provider 的升级版
Riverpod 是 Provider 的「升级版」,由 Provider 的同一作者开发。它修复了 Provider 的设计缺陷,提供了更强的类型安全和更灵活的状态管理能力。
Riverpod = River + Pod,是 Provider 的字母重排,意思是「更好的 Provider」。
安装与配置
flutter pub add flutter_riverpod配置 riverpod_lint(推荐)——在编写代码时实时检测常见错误:
# analysis_options.yaml
include: package:flutter_lints/flutter.yaml
plugins:
riverpod_lint: 3.1.3三个核心概念
| 概念 | 作用 | 类比 |
|---|---|---|
| Provider | 定义一个状态(数据 + 计算逻辑) | 变量的「定义」 |
| ref | 读取或监听其他 Provider | 变量的「引用」 |
| ProviderScope | 在顶层包裹整个 App | 状态的「容器」 |
快速上手:计数器
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope( // Riverpod 的根容器
child: MyApp(),
),
);
}
// 定义 Notifier 类(Riverpod 3.x 推荐写法)
class CounterNotifier extends Notifier<int> {
@override
int build() => 0; // 初始值
void increment() => state++;
}
// 声明 Provider 变量
final counterProvider = NotifierProvider<CounterNotifier, int>(
CounterNotifier.new,
);
// 在 Widget 中使用
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // 监听变化
return Scaffold(
appBar: AppBar(title: const Text('Riverpod 计数器')),
body: Center(
child: Text('计数: $count', style: const TextStyle(fontSize: 48)),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}核心规则:watch vs read
| 场景 | 用什么 | 原因 |
|---|---|---|
在 build 中显示数据 | ref.watch() | 数据变了需要重建 UI |
| 在按钮点击中修改数据 | ref.read() | 不需要重建按钮本身 |
| 在事件回调中读取一次值 | ref.read() | 只需要当前值 |
一句话记忆
watch 用于「看」,read 用于「做」。
ConsumerWidget vs ConsumerStatefulWidget
| Riverpod Widget | 对应 Flutter Widget | 何时使用 |
|---|---|---|
ConsumerWidget | StatelessWidget | 只需要读取共享状态 |
ConsumerStatefulWidget | StatefulWidget | 既有共享状态,又有自己的内部状态 |
Provider 类型速查
| Provider 类型 | 适用场景 | 典型用例 |
|---|---|---|
Provider | 只读值、计算值 | 主题配置、格式化数据 |
NotifierProvider | 同步状态 + 业务逻辑 | 计数器、购物车 |
AsyncNotifierProvider | 异步初始化的状态 | 从本地存储加载配置 |
FutureProvider | 一次性异步数据 | 从 API 获取数据 |
StreamProvider | 持续产生的数据流 | WebSocket、计时器 |
NotifierProvider — 同步状态管理
class CartNotifier extends Notifier<List<Product>> {
@override
List<Product> build() => []; // 初始状态
void add(Product product) => state = [...state, product];
void remove(int index) => state = [...state]..removeAt(index);
void clear() => state = [];
}
final cartProvider = NotifierProvider<CartNotifier, List<Product>>(
CartNotifier.new,
);不可变更新
在 Notifier 中更新状态时,必须创建新对象(state = [...state, item]),而不是直接修改(state.add(item) ❌)。直接修改不会触发 UI 刷新。
AsyncNotifierProvider — 异步初始化状态
class SettingsNotifier extends AsyncNotifier<ThemeMode> {
@override
Future<ThemeMode> build() async {
await Future.delayed(const Duration(seconds: 1));
return ThemeMode.system;
}
Future<void> setTheme(ThemeMode mode) async {
state = AsyncData(mode);
}
}
final settingsProvider = AsyncNotifierProvider<SettingsNotifier, ThemeMode>(
SettingsNotifier.new,
);
// 使用 AsyncValue.when() 处理三种状态
class SettingsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final settingsState = ref.watch(settingsProvider);
return settingsState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Text('加载失败: $err'),
data: (mode) => SwitchListTile(
title: const Text('深色模式'),
value: mode == ThemeMode.dark,
onChanged: (isDark) {
ref.read(settingsProvider.notifier).setTheme(
isDark ? ThemeMode.dark : ThemeMode.light,
);
},
),
);
}
}修饰符
autoDispose — 自动释放:
当没有 Widget 监听时,自动销毁 Provider 并释放资源。
final searchProvider = FutureProvider.autoDispose<List<String>>((ref) async {
final query = ref.watch(queryProvider);
return apiSearch(query);
});family — 接收参数:
让 Provider 根据外部参数生成不同的实例:
final articleDetailProvider = FutureProvider.autoDispose.family<String, String>(
(ref, articleId) async {
return '这是 $articleId 的详情内容';
},
);
// 使用时传入参数
final detail = ref.watch(articleDetailProvider('article_001'));组合使用顺序:先 autoDispose,再 family。
Provider 之间互相依赖
final productsProvider = Provider<List<Product>>((ref) => [...]);
final cartProvider = NotifierProvider<CartNotifier, List<Product>>(CartNotifier.new);
// 总价 Provider —— 依赖购物车,自动计算
final totalPriceProvider = Provider<int>((ref) {
final cart = ref.watch(cartProvider);
return cart.fold<int>(0, (sum, item) => sum + item.price);
});代码生成方式(进阶)
Riverpod 支持代码生成写法,用注解代替手写模板代码:
part 'counter.g.dart';
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
}
// counterProvider 自动生成在 counter.g.dart 中安装:
flutter pub add riverpod_annotation
flutter pub add dev:riverpod_generator
flutter pub add dev:build_runner生成代码:
dart run build_runner build # 一次性生成
dart run build_runner watch # 监听变化,自动重新生成Riverpod vs Provider 快速对比
| 对比项 | Provider | Riverpod |
|---|---|---|
| 定义状态 | 继承 ChangeNotifier | Provider / NotifierProvider |
| 注入方式 | Widget 树中用 Provider 包裹 | 顶层 ProviderScope,Provider 定义在外部 |
| 读取方式 | context.watch() / context.read() | ref.watch() / ref.read() |
| Widget 基类 | StatelessWidget / StatefulWidget | ConsumerWidget / ConsumerStatefulWidget |
| 依赖 BuildContext | 是 | 否 |
| 编译时安全 | 否 | 是 |
| 同类型多个 Provider | 冲突 | 通过名称区分 |
五、GetX — 追求极简的全能框架
GetX 是一个多功能框架,把状态管理、路由导航、依赖注入三大功能打包在一起,主打快——代码量极少,上手极简。
安装
flutter pub add get将 MaterialApp 替换为 GetMaterialApp:
import 'package:get/get.dart';
void main() {
runApp(const GetMaterialApp(home: MyApp()));
}两种响应式方式
| 方式 | 核心思路 | 适用场景 |
|---|---|---|
.obs + Obx | 数据变了自动刷新 | 简单状态,需要精细更新 |
GetBuilder + update() | 手动调用 update() 刷新 | 不需要自动监听,手动控制更新 |
响应式状态管理(.obs)
用 .obs 把普通变量变成「可观察的」,Obx 会自动响应变化:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// ─── ★ 定义控制器 ──────────────
class CounterController extends GetxController {
final count = 0.obs; // .obs 把 int 变成可观察的响应式变量
void increment() => count++; // 修改后自动通知 UI
}
// ─── ☆ 定义控制器 ──────────────
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
final controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('GetX 计数器')),
body: Center(
// ─── ★ Obx 监听变化 ──────────────
child: Obx(() => Text(
'计数: ${controller.count.value}',
style: const TextStyle(fontSize: 48),
)),
// ─── ☆ Obx 监听变化 ──────────────
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
);
}
}.obs 类型与读写
// 基本类型
final name = ''.obs; // RxString
final count = 0.obs; // RxInt
final price = 0.0.obs; // RxDouble
final isActive = false.obs; // RxBool
// 集合类型
final items = <String>[].obs; // RxList<String>
final user = <String, dynamic>{}.obs; // RxMap
// 读取值:用 .value
print(count.value); // 0
// 修改值
count.value = 10; // 直接赋值
count++; // RxInt 支持直接运算
items.add('苹果'); // RxList 支持直接操作自定义对象必须用 .value 或 update 修改
// ❌ 错误:修改属性不会触发更新
final user = User(name: '张三', age: 25).obs;
user.value.name = '李四'; // UI 不会更新!
// ✅ 正确:创建新对象赋值
user.value = User(name: '李四', age: 25);
// ✅ 正确:使用 update 方法
user.update((val) { val!.name = '李四'; });简单状态管理(GetBuilder)
GetBuilder 不会自动监听,需要手动调用 update():
class CounterController extends GetxController {
int count = 0; // 普通 int,不是 .obs
void increment() {
count++;
update(); // 👈 手动通知 UI 刷新
}
}
// 在 Widget 中使用
GetBuilder<CounterController>(
builder: (ctrl) => Text('计数: ${ctrl.count}'),
),update() 支持条件刷新,传入 ID 只刷新对应的 GetBuilder:
// 控制器中
update(['name']); // 只刷新监听 'name' 的 GetBuilder
// Widget 中
GetBuilder<UserController>(
id: 'name', // 👈 只响应 update(['name'])
builder: (ctrl) => Text('姓名: ${ctrl.name}'),
),Obx vs GetBuilder
Obx | GetBuilder | |
|---|---|---|
| 触发方式 | .obs 变量变化自动触发 | 手动调用 update() |
| 数据类型 | 必须用 .obs 包装 | 普通变量即可 |
| 性能 | 略低(响应式有额外开销) | 略高(无自动监听开销) |
| 适用 | 数据频繁变化、需要自动响应 | 不频繁变化、手动控制刷新 |
一句话选择
数据经常变、想让 UI 自动跟着变 → Obx;想自己控制什么时候刷新 → GetBuilder。
GetxController 生命周期
class MyController extends GetxController {
@override
void onInit() {
super.onInit();
// 控制器初始化时调用(类似 initState)
}
@override
void onReady() {
super.onReady();
// Widget 渲染完成后调用,适合做数据请求
}
@override
void onClose() {
// 控制器被销毁时调用(类似 dispose)
super.onClose();
}
}获取控制器的方式
| 方式 | 何时创建 | 实例数量 | 适用场景 |
|---|---|---|---|
Get.put | 立即 | 单例 | 大多数场景 |
Get.lazyPut | 首次 find 时 | 单例 | 不确定是否会用到 |
Get.find | 不创建,只查找 | — | 获取已注册的控制器 |
Get.create | 每次获取时 | 多实例 | 需要独立实例的场景 |
页面级绑定(推荐)
使用 Bindings 让控制器与页面绑定——页面打开时创建,关闭时销毁:
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<HomeController>(() => HomeController());
}
}
// 在路由中绑定
Get.to(() => const HomePage(), binding: HomeBinding());GetView 简化
如果一个页面只使用一个控制器,可以用 GetView 省去手动获取:
// ─── ★ GetView 简化 ──────────────
class HomePage extends GetView<HomeController> {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
// 直接用 controller,不需要 Get.put 或 Get.find
return Obx(() => Text('计数: ${controller.count.value}'));
}
}
// ─── ☆ GetView 简化 ──────────────GetX 路由导航
GetX 自带路由管理,不依赖 BuildContext:
// 跳转到新页面
Get.to(() => const SecondPage());
// 带参数跳转
Get.to(() => const DetailPage(), arguments: {'id': 42});
// 在目标页面获取参数
final args = Get.arguments; // {'id': 42}
// 返回上一页
Get.back();
// 替换当前页面
Get.off(() => const LoginPage());
// 清空所有页面并跳转(适合登录后跳主页)
Get.offAll(() => const HomePage());完整示例:待办事项 App
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(const GetMaterialApp(home: TodoPage()));
}
// ============ 数据模型 ============
class Todo {
final String title;
final bool done;
const Todo({required this.title, this.done = false});
Todo copyWith({String? title, bool? done}) =>
Todo(title: title ?? this.title, done: done ?? this.done);
}
// ============ 状态管理 ============
class TodoController extends GetxController {
final todos = <Todo>[].obs;
int get uncompletedCount => todos.where((t) => !t.done).length;
void add(String title) => todos.add(Todo(title: title));
void toggle(int index) {
todos[index] = todos[index].copyWith(done: !todos[index].done);
}
void remove(int index) => todos.removeAt(index);
}
// ============ UI ============
class TodoPage extends StatelessWidget {
const TodoPage({super.key});
final controller = Get.put(TodoController());
final _inputController = TextEditingController();
void _addTodo() {
final text = _inputController.text.trim();
if (text.isEmpty) return;
controller.add(text);
_inputController.clear();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Obx(() => Text('待办事项 (${controller.uncompletedCount} 未完成)')),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: TextField(
controller: _inputController,
decoration: const InputDecoration(
hintText: '输入待办事项...',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _addTodo(),
),
),
const SizedBox(width: 8),
ElevatedButton(onPressed: _addTodo, child: const Text('添加')),
],
),
),
Expanded(
child: Obx(() {
final todos = controller.todos;
if (todos.isEmpty) {
return const Center(child: Text('暂无待办事项'));
}
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
leading: Checkbox(
value: todo.done,
onChanged: (_) => controller.toggle(index),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.done
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => controller.remove(index),
),
);
},
);
}),
),
],
),
);
}
}常见错误排查
| 现象 | 原因 | 解决 |
|---|---|---|
Obx 内没用 .obs 变量报错 | Obx builder 中必须至少使用一个 .obs 变量 | 如果不需要响应式,直接用普通 Widget |
| 控制器获取报错 "Controller not found" | 还没注册就获取 | 先 Get.put() 再 Get.find() |
| 自定义对象修改后 UI 不更新 | 只修改了属性,没有整体赋值 | 用 .value = 新对象 或 .update() |
| 页面退出后控制器还在内存 | 没有使用 Bindings 或手动删除 | 使用页面级绑定 Get.to(..., binding: ...) |
状态管理方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| setState | 简单直接,无需额外依赖 | 无法跨组件共享,层层回调麻烦 |
| InheritedWidget | Flutter 内置,理解原理重要 | 手写繁琐,实际项目不推荐直接使用 |
| Provider | 官方推荐,轻量易学 | 需要理解 BuildContext,大型项目不够强 |
| Riverpod | Provider 的改进版,编译时安全,不依赖 Context | 学习曲线稍高,API 变化较多 |
| Bloc / Cubit | 严格的架构模式,适合团队协作 | 样板代码多,学习成本高 |
| GetX | 开发速度快,功能全面,样板代码极少 | 不够规范,长期维护不确定 |
核心原则
不要过度设计。如果 setState 能解决问题,就不要引入 Provider。如果 Provider 够用,就不需要 Riverpod。如果追求开发速度,GetX 是最快的选择,但要注意规范性。
下一步
- 导航与路由 — Navigator / go_router
