下面把 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 拉网络。

如果你有具体页面/交互(比如“搜索结果页:关键字变化去请求,同时保留上次结果并防抖”或“详情页进入即并发请求多个接口再整合”),我可以按你的场景给一份可直接拷贝的模板。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐