📚 全栈开发学习系列
阶段一:编程基础 ✅ 已完成
01-08 路线总览 → Python → C语言 → 数据结构 → Git → Linux
阶段二:Web 全栈 ✅ 已完成
09-16 HTML → CSS → JavaScript → HTTP → FastAPI → PostgreSQL → 认证授权 → 博客系统
阶段三:前端深化 ✅ 已完成
17-25 React → TypeScript → Next.js → Tailwind → 工程化 → 测试 → API测试 → 状态管理
阶段四:跨平台 App
✓ 26 跨平台开发:React Native 与 Expo
✓ 27 React Native 进阶:导航与状态管理
✓ 28 Flutter 跨平台开发:Dart 语言与 Widget 体系
29 Flutter 进阶:状态管理与网络请求(当前篇)
30 Uni-app:小程序开发入门
阶段五:扩展 + 部署
Tauri(桌面)→ Docker Compose → CI/CD → 域名部署
Flutter 进阶:状态管理与网络请求
难度:高级 | 技术栈:Flutter 3.47.2 / Dart 3.13.2 / Riverpod 3.4.3 / dio 5.11.1 | 阶段四第 4 篇
读完本篇你将能:理解 Flutter 状态管理的分层架构与 setState 的局限性,使用 Provider 和 InheritedWidget 实现依赖注入,用 Riverpod 3.4 的 Provider/Notifier 体系管理应用状态,用 http 包和 dio 拦截器封装网络请求层,通过 FutureBuilder 和 StreamBuilder 实现数据驱动的 UI 更新,并掌握跨框架状态管理的心智模型迁移。
上一篇我们搭建了 Flutter 的地基——Dart 语言和 Widget 体系。那个计数器应用只有一个小问题:setState() 只能在拥有 State 的那个 Widget 内部触发重建。如果计数值需要在页面 A 修改、页面 B 显示、页面 C 持久化,setState 就无能为力了——数据被锁在了单个 Widget 的 State 里,像一间没有窗户的房间。
这正是状态管理框架要解决的问题:把数据从 Widget 树中抽出来,放在一个共享的"仓库"中,任何 Widget 都可以读取和监听。同时,一个真实的应用不可能只有本地数据——你需要从服务器拉取列表、提交表单、缓存图片,这就涉及网络请求层的设计。本篇将同时攻克这两座山:状态管理和网络请求。
📑 本文目录
01状态管理概述:setState 的边界与分层架构
02InheritedWidget 与 Provider:依赖注入基础
03Riverpod 3.4:新一代状态管理框架
04网络请求:http 包与 dio 拦截器
05FutureBuilder 与 StreamBuilder:数据驱动 UI
06常见错误与动手练习
状态管理概述:setState 的边界与分层架构
setState 的工作原理与天花板
回顾上一篇的计数器:setState() 做了两件事——标记当前 State 关联的 Element 为 dirty,然后在下一帧调用 build() 重建子树。这套机制对"自包含"的 Widget 完美适用——状态产生和消费在同一个地方。
但当应用长大,问题浮现:用户登录状态需要在 5 个页面共享,购物车数量需要在 AppBar 和 BottomBar 同时显示,深嵌套的 Widget 需要读取主题色——如果把所有状态都塞进顶层 StatefulWidget,你需要把回调函数一层层往下传,这就是 Flutter 社区常说的"prop drilling"——和 React 中一模一样的痛点。
💡 小贴士
Flutter 的 Widget 树是"不可变"的——每次 rebuild 都生成全新的 Widget 对象。State 对象才是可变的,它依附在 Element 上跨越多次 rebuild 存活。状态管理框架的本质就是:把 State 从单个 Widget 的私有属性提升为"全局可访问的共享对象",让数据流动不再受 Widget 嵌套层级的束缚。
Flutter 状态管理分层模型
Flutter 官方文档把状态分为三类,不同类型应该用不同层级的方案:
| 状态类型 |
示例 |
推荐方案 |
| 局部状态 |
按钮点击动画、输入框文本 |
setState |
| 共享状态 |
登录状态、购物车 |
Provider / Riverpod |
| 服务端状态 |
API 返回的用户列表 |
FutureBuilder + Riverpod |
这个分层和 React Native 中的对应关系很清晰:useState = setState,Zustand = Provider/Riverpod,TanStack Query = FutureBuilder + Riverpod AsyncValue。理解这个映射,你已有的前端状态管理知识可以直接迁移到 Flutter。
InheritedWidget 与 Provider:依赖注入基础
在讲第三方框架之前,必须先理解 Flutter 框架自带的状态共享机制——InheritedWidget。它是 Flutter 中所有状态管理方案的基石——Provider 封装了它,Riverpod 替代了它,但核心思路都是"从上往下传递数据,子树中按需取用"。
InheritedWidget 原理
InheritedWidget 是一种特殊的 Widget——它在树中注册自己后,下方所有子 Widget 都能通过 context.dependOnInheritedWidgetOfExactType() 获取到它的数据。当 InheritedWidget 的数据变化时,所有依赖它的子 Widget 会自动重建。
Dart - InheritedWidget 原始写法
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class ThemeData extends InheritedWidget {
final Color accentColor;
final Widget child;
const ThemeData({
required this.accentColor,
required this.child,
super.key,
});
@override
bool updateShouldNotify(ThemeData old) =>
accentColor != old.accentColor;
static ThemeData of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType();
}
|
这段代码做了三件事:定义数据(accentColor)、实现 updateShouldNotify 决定何时通知子树重建、提供 of() 静态方法让子 Widget 获取数据。样板代码太多了——每个共享数据类型都要写一个这样的类。
Provider 6.1:InheritedWidget 的语法糖
Provider(当前版本 6.1.5)是 Flutter 官方推荐的最轻量状态管理方案,它把 InheritedWidget 的样板代码压缩到了两行:
Dart - Provider 用法
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import 'package:provider/provider.dart';
class Counter extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() { _count++; notifyListeners(); }
}
// 在顶层注入
ChangeNotifierProvider(
create: (_) => Counter(),
child: MyApp(),
)
// 在子树中读取
final counter = context.read<Counter>();
|
ChangeNotifier 是 Flutter SDK 自带的观察者基类——调用 notifyListeners() 会通知所有监听者重建。Provider 在底层用 InheritedWidget 把这个 ChangeNotifier 实例散播到子树,子 Widget 通过 context.read<T>() 读一次(不监听变化)或 context.watch<T>() 持续监听(变化时自动重建)。
💡 小贴士
context.read vs context.watch 是 Provider 的核心区分:read 在回调中调用(如 onPressed),获取当前值但不监听变化;watch 在 build 方法中调用,数据变化时触发重建。在回调中误用 watch 会导致"在 build 之外监听"的运行时错误。
Riverpod 3.4:新一代状态管理框架
Provider 解决了样板代码问题,但保留了 InheritedWidget 的两个根本缺陷:依赖 BuildContext(测试时需要 mock context)、类型安全靠泛型但不防 typo。Riverpod(当前 3.4.3)由 Provider 的作者 remi_rousselet 重新设计,彻底脱离 BuildContext,用独立的 ProviderScope 管理状态生命周期。
Riverpod 3.0 的重大变化
Riverpod 3.0 是一次重大重构,3.4.3 是 3.x 系列的最新稳定版。核心变化包括:
| 特性 |
Riverpod 2.x |
Riverpod 3.4 |
| 状态类 |
StateNotifierProvider |
NotifierProvider(推荐) |
| 异步状态 |
FutureProvider + AsyncValue |
AsyncNotifierProvider(统一) |
| 代码生成 |
riverpod_generator(可选) |
@riverpod 注解(主推) |
| 自动销毁 |
需手动 keepAlive |
自动管理 + ref.onDispose |
NotifierProvider:声明式状态
Riverpod 3.4 推荐用 Notifier 类替代旧版 StateNotifier,语法更简洁:
Dart - Riverpod NotifierProvider
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import 'package:riverpod/riverpod.dart';
// 1. 定义 Provider
final counterProvider = NotifierProvider<
Counter, int>(Counter.new);
// 2. 定义 Notifier 类
class Counter extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
void reset() => state = 0;
}
// 3. 在 Widget 中使用
class CounterView extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
|
关键区别:Notifier<T> 替代了 ChangeNotifier——不再需要手动调 notifyListeners(),直接修改 state 属性,Riverpod 自动通知监听者。ConsumerWidget 替代 StatelessWidget,额外提供 WidgetRef ref 参数——这是 Riverpod 访问状态的唯一入口,不依赖 BuildContext。
AsyncNotifierProvider:异步状态管理
Riverpod 3 把异步状态统一到了 AsyncNotifierProvider,搭配 AsyncValue 类型处理 loading/data/error 三态——这和 TanStack Query 的设计如出一辙:
Dart - AsyncNotifier + when 模式匹配
AsyncValue<List<User>> users = ref.watch(userListProvider);
return users.when(
data: (users) => ListView(children: users.map(UserTile.new).toList()),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('$err'),
);
AsyncValue.when 强制你处理三种状态——编译器会报错如果遗漏任何一个分支。这比 React Native 中手动判断 isLoading 更安全。Riverpod 还提供 .value 快捷属性(只取 data,其他返回 null)和 .unwrapPrevious() 用于保留旧数据在刷新时显示。
💡 小贴士
Riverpod 3.4 的 ref.watch 和 ref.read 的区分与 Provider 的 watch/read 完全一致:watch 在 build 中调用建立监听关系,read 在回调中调用获取当前值。但 Riverpod 的 ref 不依赖 BuildContext——这意味着你可以在 Notifier 的 build 方法中 watch 其他 Provider,实现状态间的依赖组合。
网络请求:http 包与 dio 拦截器
状态管理解决了"数据在哪"的问题,但数据从哪来?从服务器。Flutter 网络请求有两套方案:Dart 官方的 http 包和社区生态最成熟的 dio(当前 5.11.1)。http 轻量但功能基础,dio 提供拦截器、取消、重试、文件上传等企业级特性。
http 包:轻量级请求
Dart 标准库的 http 包(版本 1.6.0)提供最基本的 GET/POST/PUT/DELETE 方法,适合简单场景:
Dart - http 包 GET 请求
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List<Map>> fetchUsers() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return List<Map>.from(data);
}
throw Exception('Failed: ${response.statusCode}');
}
|
http 包的 API 简单直观——http.get() 返回 Future<Response>,你手动检查 statusCode 和 body。但当你需要统一添加 Authorization 头、拦截 401 跳转登录、全局超时、请求取消时,http 包就捉襟见肘了——每个请求都得重复写 header 逻辑。
dio 5.11:拦截器架构
dio 把网络请求层设计成"洋葱模型"——请求从外到内穿过一层层拦截器,响应从内到外原路返回。这和 axios 的拦截器思路完全一致,也和 React Native 中 Axios 的用法高度对应。
Dart - dio 拦截器封装
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 15),
));
// 认证拦截器:自动注入 token
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
final token = AuthStorage.token;
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
// 401 自动刷新 token
onError: (e, handler) async {
if (e.response?.statusCode == 401) {
await AuthStorage.refreshToken();
return handler.resolve(
await dio.fetch(e.requestOptions)
);
}
handler.reject(e);
},
));
// 日志拦截器
dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));
|
这段代码实现了两个拦截器:认证拦截器在每个请求发出前自动注入 Bearer Token,遇到 401 时自动刷新 token 并重发原请求;日志拦截器打印请求和响应体用于调试。整个应用只需要配置一次,所有请求自动走拦截链。
请求取消:CancelToken
用户快速切换页面时,上一个页面的网络请求还没回来——如果不取消,响应到达时页面已销毁,setState 会抛异常。dio 的 CancelToken 就是干这件事的:
Dart - CancelToken 请求取消
class UserListPage extends ConsumerStatefulWidget {
final cancelToken = CancelToken();
@override
void dispose() {
cancelToken.cancel('page disposed');
super.dispose();
}
}
在 dispose() 中调用 cancelToken.cancel(),所有携带该 token 的在途请求会被立即中止,dio 抛出 DioException,你可以用 CancelToken.isCancel(error) 判断是否是主动取消,避免误报为真实错误。
💡 小贴士
dio 和 http 的选择标准不是"功能多少"而是"场景匹配"。只做两三个简单 GET 请求的工具应用,http 够用且零配置。但凡涉及认证、统一错误处理、请求取消、文件上传下载——直接上 dio,拦截器架构省去大量重复代码。一个常见误区是先选 http 然后手动封装 header——你本质上就是在重新发明拦截器。
FutureBuilder 与 StreamBuilder:数据驱动 UI
有了状态管理和网络请求,还差一座桥把它们连到 UI 上——FutureBuilder 和 StreamBuilder 是 Flutter SDK 自带的数据驱动 Widget,它们监听异步数据源并自动重建 UI。
FutureBuilder:一次性异步
当你有一个 Future<T>(比如一个 HTTP 请求),用 FutureBuilder 把它"展开"为三种 UI 状态:
Dart - FutureBuilder 基本用法
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
FutureBuilder<User>(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return UserCard(user: snapshot.data!);
},
)
// 等价于 Riverpod AsyncValue.when,但 FutureBuilder
// 不强制处理 error 分支——容易遗漏错误处理
|
FutureBuilder 通过 snapshot.connectionState 判断请求阶段(waiting/done),用 snapshot.hasError 和 snapshot.data 获取结果。这是 Flutter 最原始的异步 UI 方案,但有一个经典陷阱——如果把 future 直接写在 build 方法里,每次 rebuild 都会重新发起请求。
StreamBuilder:持续数据流
Stream 是 Dart 的异步序列——不像 Future 只产生一个值,Stream 可以持续产生多个值。聊天消息、WebSocket 推送、传感器数据都是 Stream。StreamBuilder 监听 Stream 并在每次新值到达时重建:
Dart - StreamBuilder 实时计数器
StreamBuilder<int>(
stream: stopwatch.tick, // Stream<int> 每秒产生一个值
builder: (context, snapshot) {
return Text('${snapshot.data ?? 0}s');
},
)
StreamBuilder 的 API 和 FutureBuilder 几乎一样——builder 接收 AsyncSnapshot<T>,只是数据源从 Future 换成了 Stream。区别在于 StreamBuilder 会在 Stream 的每次新值时触发 rebuild,而 FutureBuilder 只在 Future 完成时触发一次。
| 对比维度 |
FutureBuilder |
StreamBuilder |
Riverpod AsyncValue |
| 数据源 |
Future(单值) |
Stream(多值序列) |
Provider(任意) |
| 触发时机 |
Future 完成(一次) |
每次 Stream 新值 |
Provider 状态变化 |
| 错误处理 |
非强制 |
非强制 |
强制(when 三分支) |
| 缓存/刷新 |
无 |
无 |
自动缓存 + invalidate 刷新 |
💡 小贴士
FutureBuilder 有一个经典陷阱:future 参数不要在 build 方法中直接赋值新 Future。因为 build 会被多次调用,每次都创建新 Future 导致重复请求。正确做法是在 State 的 initState 中初始化 Future 字段,或者在 Riverpod 中用 AsyncNotifierProvider 管理异步状态——后者自带缓存和刷新机制,不需要手动管理 Future 生命周期。
常见错误与动手练习
高频踩坑清单
| 错误现象 |
根因 |
修复 |
| setState() called after dispose() |
异步回调完成时 Widget 已销毁 |
用 CancelToken 取消请求 或 mounted 判断 |
| FutureBuilder 重复请求 |
future 在 build 中新建 |
initState 中赋值或用 Riverpod |
| Riverpod Provider not found |
缺少 ProviderScope 包裹 |
main() 中顶层包裹 ProviderScope |
| dio 请求被截断无报错 |
CancelToken 触发但未捕获 |
catch 中用 CancelToken.isCancel 过滤 |
| context.watch 在 build 外调用 |
在 onPressed 回调中用了 watch |
回调中改用 context.read |
⚠️ 常见错误
setState after dispose 是 Flutter 最高频崩溃之一。场景:页面发起网络请求,用户快速返回上一页,请求回来时调用 setState() 但 Widget 树已销毁。报错信息:setState() called after dispose()。修复方式有两种:在 State 中检查 if (mounted) 再 setState,或更彻底地在 dispose 中取消网络请求。
动手练习
练习 1:基础(Provider 计数器)
用 Provider 6.1 实现一个计数器:创建 ChangeNotifier 子类 Counter,在顶层注入 ChangeNotifierProvider,在两个不同页面的 Widget 中分别读取和修改计数值。验证修改后两个页面都能实时更新。
练习 2:进阶(Riverpod + dio 用户列表)
用 Riverpod 3.4 的 AsyncNotifierProvider + dio 创建一个用户列表应用:AsyncNotifier 负责调用 dio 获取 https://jsonplaceholder.typicode.com/users 数据,UI 用 AsyncValue.when 处理 loading/data/error 三态。添加下拉刷新功能(ref.invalidate 刷新 Provider)。
练习 3:挑战(完整状态架构)
实现一个待办事项应用,包含以下功能:用 Riverpod NotifierProvider 管理 Todo 列表状态,用 dio 拦截器实现增删改查的 API 调用(带 Bearer Token 认证),用 CancelToken 在页面退出时取消请求,用 StreamBuilder 实现搜索框的实时过滤(输入防抖 500ms)。目标:整合本篇所有知识点。
🏷️ 知识回顾
setStateInheritedWidgetChangeNotifierProvider 6.1context.watch/read
Riverpod 3.4NotifierProviderAsyncNotifierProviderAsyncValue.when
ConsumerWidgetWidgetRefhttp 1.6dio 5.11
拦截器CancelTokenFutureBuilderStreamBuildermounted
下一篇预告
本篇攻克了 Flutter 的状态管理与网络请求两座大山。下一篇将进入小程序开发领域:Uni-app:小程序开发入门——学习 Uni-app 跨端框架的基本架构、Vue 3 语法在小程序中的适配、条件编译实现多端差异化、以及从 Flutter/RN 跨平台思维向小程序生态的思维转换。从移动原生到小程序,跨端开发的全景即将补齐最后一块拼图。