FutureBuilder全面解析:用法、坑与替代方案
期望“取消”请求:Future 本身不可取消。应缓存到 state 中。如果你有具体页面/交互(比如“搜索结果页:关键字变化去请求,同时保留上次结果并防抖”或“详情页进入即并发请求多个接口再整合”),我可以按你的场景给一份可直接拷贝的模板。当 future 改变时,新的初始快照是 waiting,但会“带着前一次的 data”(如果有),这样可以在刷新时保留旧数据避免闪烁。FutureBuilde
下面把 FutureBuilder 讲全一点:它的职责、工作机制、推荐用法、进阶场景、常见坑与替代方案。
一、它是干什么的
-
FutureBuilder<T> 用来“监听一个 Future 的生命周期”,根据其状态构建不同的 UI(加载中/完成/错误)。
-
适用场景:一次性请求(网络、磁盘、初始化)、按钮触发的异步动作、页面首次加载。
二、工作机制(AsyncSnapshot)
-
builder 会拿到一个 AsyncSnapshot<T>,常用字段:
-
connectionState: none | waiting | done(FutureBuilder基本不会有 active)
-
hasData / data
-
hasError error stackTrace
-
-
行为要点:
-
初次没有 future 时是 none。
-
设置了 future 且未完成时是 waiting。
-
完成后是 done,带 data 或 error。
-
当 future 改变时,新的初始快照是 waiting,但会“带着前一次的 data”(如果有),这样可以在刷新时保留旧数据避免闪烁。
-
FutureBuilder 会忽略旧 future 的迟到结果(不会把更早的返回覆盖当前 UI)。
-
三、最常用写法(避免在 build 里创建 Future)
class UserPage extends StatefulWidget {
final String userId;
const UserPage({super.key, required this.userId});
@override
State<UserPage> createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
late Future<User> _future;
@override
void initState() {
super.initState();
_future = _fetch(widget.userId); // 仅创建一次
}
@override
void didUpdateWidget(covariant UserPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId) {
// 依赖变了,换一个新的 future(老结果会被忽略)
_future = _fetch(widget.userId);
setState(() {}); // 触发重建
}
}
Future<User> _fetch(String id) async {
await Future.delayed(const Duration(milliseconds: 500));
return User(id: id, name: 'User $id');
}
@override
Widget build(BuildContext context) {
return FutureBuilder<User>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// 首次加载显示骨架;后续刷新时 snapshot 仍可能带有旧 data,可选择用它
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text('出错:${snapshot.error}'),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => setState(() => _future = _fetch(widget.userId)),
child: const Text('重试'),
),
]),
);
}
final user = snapshot.data!;
return Center(child: Text('Hello, ${user.name}'));
},
);
}
}
四、依赖变化与“刷新”的几种姿势
-
依赖变化(如 userId 改变):在 didUpdateWidget 比较旧新值,重新赋值 future 并 setState。
-
手动刷新(下拉/按钮):给 FutureBuilder 绑定一个 state 中的 future,每次刷新重新创建并赋值。
-
下拉刷新的简例:
RefreshIndicator( onRefresh: () async { setState(() => _future = _fetch(widget.userId)); await _future; // 等待刷新完成(可选) }, child: FutureBuilder<List<Item>>( future: _future, builder: ... ), )
-
-
保留旧数据边加载:
-
当 future 切换到新的时,snapshot 会是 waiting 且可能还带着旧 data。
-
你可以在 waiting 时优先展示 snapshot.data,然后叠加一个轻量的 loading 提示。
if (snapshot.connectionState == ConnectionState.waiting && snapshot.hasData) { return Stack( children: [ ListView(children: buildItems(snapshot.data!)), const Align(alignment: Alignment.topCenter, child: LinearProgressIndicator()), ], ); }
-
-
如果你想“完全从零开始”而不是带着旧数据,给 FutureBuilder 增加一个 Key(例如 ValueKey(widget.userId)),强制其丢弃旧快照。
五、多个 Future 的组合
-
并发请求用 Future.wait,然后把结果解构给 UI。
Future<(User, List<Post>)> _fetchAll(String userId) async { final results = await Future.wait([fetchUser(userId), fetchPosts(userId)]); return (results[0] as User, results[1] as List<Post>); } FutureBuilder<(User, List<Post>)>( future: _futureTuple, builder: (context, s) { ... } )
-
想边界更清晰,也可以把组合逻辑放到仓库/UseCase 层,FutureBuilder 只收一个 DTO。
六、错误处理与降级
-
有错误界面就给重试按钮(重置未来)。
-
给网络请求加超时和兜底:
_future = fetchData().timeout( const Duration(seconds: 8), onTimeout: () => const FallbackData(), );
-
统一日志上报:在仓库层 catch 记录,在 UI 层只负责展示。
七、常见坑
-
在 build 里直接写 future: fetch() 会导致每次重建都触发请求(包括父组件 setState、MediaQuery 变化等)。应缓存到 state 中。
-
在列表的每个 item 里放 FutureBuilder 拉网络:会产生N个并发请求且难以缓存。应上移到列表层统一请求或做缓存(如 LRU、Provider/Riverpod 缓存)。
-
在 builder 里 setState:会形成重建回路。builder 只负责“根据快照返回 Widget”,不要产生副作用。
-
期望“取消”请求:Future 本身不可取消。切换 future 后,旧 future 的回调会被忽略,但底层 I/O 不会被真正取消。需要取消要用底层客户端(如 http 的 Client.close)或外部的 CancelableOperation。
-
重活卡顿:FutureBuilder 不会把重计算挪到后台线程。CPU 密集型任务用 compute/Isolate。
八、保留上次成功数据的模板(常用于“刷新不抖动”)
Widget build(BuildContext context) {
return FutureBuilder<Data>(
future: _future,
builder: (context, s) {
final loading = s.connectionState == ConnectionState.waiting;
final hasData = s.hasData;
final hasError = s.hasError;
if (!hasData && loading) {
return const SkeletonList(); // 首次加载骨架
}
if (hasError && !hasData) {
return ErrorView(error: s.error, onRetry: () {
setState(() => _future = load());
});
}
// 有数据(可能在 loading 中),展示数据并按需叠加 loading/错误条
final data = s.data!;
return Stack(
children: [
DataView(data: data),
if (loading) const Positioned(top: 0, left: 0, right: 0, child: LinearProgressIndicator()),
if (hasError) Positioned(
bottom: 16, left: 16, right: 16,
child: BannerToast(message: '更新失败,显示旧数据'),
),
],
);
},
);
}
九、与状态管理的边界
-
FutureBuilder 很适合“一次性数据加载 + 简单三态”的场景。
-
如果需要:
-
多处复用同一数据/缓存
-
增量更新/复杂交互
-
合并多个来源(本地缓存 + 网络 + 轮询)
则更推荐把异步逻辑放到状态管理(Provider/Bloc/Riverpod等),UI 层只读状态。此时 FutureBuilder 可能被替换为 Consumer/BlocBuilder 等。
-
十、性能与可测试性
-
避免在深层级频繁创建 FutureBuilder;尽量合并到较高层,或拆成小组件各自缓存 future。
-
网络结果做缓存(内存/磁盘),避免返回上一页再进入就重新请求。
-
测试:WidgetTest 里
-
await tester.pump(); 触发一帧
-
await tester.pump(const Duration(milliseconds: 600)); 模拟时间推进使 Future 完成
-
await tester.pumpAndSettle(); 等待所有动画/微任务结束
-
十一、迷你速查表
-
放哪里创建 future:initState;依赖变了在 didUpdateWidget 重建。
-
保留旧数据:等待态 + snapshot.hasData 时显示旧数据;必要时加 Key 完全重置。
-
刷新:重新赋值 _future 并 setState。
-
多个请求:Future.wait。
-
错误/重试:hasError 分支 + 重试按钮重置 future。
-
不要:在 build 里 new Future;在 builder 里 setState;在海量 item 里用 FutureBuilder 拉网络。
如果你有具体页面/交互(比如“搜索结果页:关键字变化去请求,同时保留上次结果并防抖”或“详情页进入即并发请求多个接口再整合”),我可以按你的场景给一份可直接拷贝的模板。
更多推荐
所有评论(0)