📚 全栈开发学习系列
阶段一:编程基础 ✅ 已完成
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 跨平台思维向小程序生态的思维转换。从移动原生到小程序,跨端开发的全景即将补齐最后一块拼图。