Kodokon kodokon.com

Dart 中的异步:Future、async/await、FutureBuilder

掌握 Future、async/await 和 FutureBuilder,既不阻塞 UI,也不会意外地重复触发你的请求。

10 分钟 · 3 题

在 Kodokon 中打开本课

Dart 在单个线程上运行你的应用,由一个事件循环驱动。Future 并不是线程:它是对一个即将到来的值的承诺。await 从不阻塞那个线程 - 它会挂起当前函数,把控制权交回给事件循环(UI 继续运行),然后在结果到达时恢复执行。由此可以得出:对于纯计算,async/await 并不会并行化任何东西;对于 CPU 密集型的工作,你需要一个 isolate。

DART
Future<double> fetchRate() async {
  await Future<void>.delayed(
    const Duration(milliseconds: 300),
  );
  return 1.08;
}

Future<void> main() async {
  try {
    final rate = await fetchRate();
    print('EUR/USD rate: $rate');
  } on Exception catch (error) {
    print('Rate loading failed: $error');
  }
}
一个 async 函数要用 await 和 try/catch 来消费

两个连续的 await串行执行:第二个只有在第一个完成之后才开始。当这些操作彼此独立时,用 Future.wait 把它们一起启动。需要知道的取舍是:Future.wait 是快速失败的 - 第一个错误就会让整体失败,其他 future 的结果即便成功也会丢失。如果你需要部分结果,就在每个 future 内部处理它的错误。

DART
Future<String> fetchProfile() async => 'profile';

Future<String> fetchOrders() async => 'orders';

Future<void> main() async {
  final [profile, orders] = await Future.wait(
    [fetchProfile(), fetchOrders()],
  );
  print('$profile / $orders');
}
用 Future.wait 和 Dart 3 的解构实现并行化

在 UI 一侧,FutureBuilder 通过一个 snapshotFuture 连接到 build。实战中头号陷阱是:直接在 build创建 future。这样一来,每一次重建 - 来自父组件的一个简单 setState 就足够了 - 都会重新触发请求。请把 future 存进 initState,并且始终在检查数据之前先检查错误。

DART
import 'package:flutter/material.dart';

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({super.key});

  @override
  State<ProfileScreen> createState() =>
      _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  late final Future<String> _nameFuture;

  @override
  void initState() {
    super.initState();
    _nameFuture = _loadName();
  }

  Future<String> _loadName() async {
    await Future<void>.delayed(
      const Duration(seconds: 1),
    );
    return 'Ada Lovelace';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _nameFuture,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        if (!snapshot.hasData) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
        return Text(snapshot.data!);
      },
    );
  }
}
future 只在 initState 中创建一次

知识检测

确认你已牢记本课的重点内容。

  1. await 在主线程上实际做了什么?
    • 它阻塞线程,直到 Future 完成。
    • 它自动把计算移到一个次要线程上。
    • 它挂起当前函数,并把控制权交回给事件循环:UI 继续运行。
  2. 对于 Future.wait([a(), b()]),如果 a() 失败会发生什么?
    • Future.wait 会以 a() 的错误失败;b() 的结果会丢失。
    • Future.wait 会等 b() 完成,然后单独返回它的结果。
    • 只要 b() 成功,错误就会被忽略。
  3. 为什么要避免在 FutureBuilder 中把 future: fetchUser() 直接写在 build 里?
    • build 里不能包含异步调用,代码无法编译。
    • 每一次重建都会创建一个新的 Future,从而重新触发网络请求。
    • snapshot 会一直卡在等待状态。