1.10 异步编程:Future与async/await

基础知识

在现代应用程序开发中,异步操作是不可避免的。例如,网络请求、文件读写、数据库操作等都可能需要较长时间才能完成。如果在主线程(UI 线程)中执行这些耗时操作,会导致 UI 阻塞,用户界面无响应,从而影响用户体验。Dart 语言提供了强大的异步编程支持,主要通过 Future 对象和 async/await 关键字来实现。

1. Future 对象

Future 对象代表一个异步操作的最终结果或错误。当一个异步操作开始时,它会立即返回一个 Future 对象。这个 Future 对象在操作完成时会“完成”(complete),并携带一个值(成功结果)或一个错误(失败结果)。

  • then() 方法:用于注册一个回调函数,当 Future 完成并携带值时执行。then() 方法本身也会返回一个新的 Future,可以链式调用。
  • catchError() 方法:用于注册一个错误处理回调函数,当 Future 完成并携带错误时执行。
  • whenComplete() 方法:无论 Future 成功还是失败,都会执行的回调函数,通常用于清理资源。
dart
复制代码
Future<String> fetchUserData() {
  return Future.delayed(Duration(seconds: 2), () {
    // 模拟网络请求延迟
    // throw Exception("网络请求失败"); // 模拟错误
    return "User Data Loaded";
  });
}

void main() {
  print("开始获取用户数据...");

  fetchUserData().then((data) {
    print("数据获取成功: $data");
  }).catchError((error) {
    print("数据获取失败: $error");
  }).whenComplete(() {
    print("数据获取操作完成。");
  });

  print("继续执行其他任务...");
}
// 预期输出:
// 开始获取用户数据...
// 继续执行其他任务...
// (2秒后)
// 数据获取成功: User Data Loaded
// 数据获取操作完成。

2. asyncawait 关键字

asyncawait 是 Dart 异步编程的语法糖,它们使得异步代码看起来和同步代码一样,大大提高了可读性和可维护性。

  • async:用于标记一个函数是异步的。一个 async 函数总是返回一个 Future 对象。如果 async 函数没有显式返回 Future,Dart 会自动将其返回值包装在一个 Future 中。
  • await:只能在 async 函数内部使用。它会暂停当前 async 函数的执行,直到 await 后面的 Future 完成。一旦 Future 完成,await 表达式的值就是 Future 完成时携带的值。如果 Future 完成时携带错误,await 表达式会抛出该错误。
dart
复制代码
Future<String> downloadFile(String url) async {
  print("开始下载文件: $url");
  await Future.delayed(Duration(seconds: 3)); // 模拟下载延迟
  // throw Exception("下载失败"); // 模拟下载失败
  print("文件下载完成: $url");
  return "File Content from $url";
}

Future<void> processData() async {
  try {
    String content1 = await downloadFile("http://example.com/file1.txt");
    print("处理内容1: $content1");

    String content2 = await downloadFile("http://example.com/file2.txt");
    print("处理内容2: $content2");

    print("所有文件处理完毕。");
  } catch (e) {
    print("处理数据时发生错误: $e");
  }
}

void main() {
  print("主程序开始。");
  processData();
  print("主程序继续执行,不等待异步操作。");
}
// 预期输出:
// 主程序开始。
// 开始下载文件: http://example.com/file1.txt
// 主程序继续执行,不等待异步操作。
// (3秒后)
// 文件下载完成: http://example.com/file1.txt
// 处理内容1: File Content from http://example.com/file1.txt
// 开始下载文件: http://example.com/file2.txt
// (3秒后)
// 文件下载完成: http://example.com/file2.txt
// 处理内容2: File Content from http://example.com/file2.txt
// 所有文件处理完毕。

3. Future.wait()

当需要等待多个 Future 都完成时,可以使用 Future.wait()。它接收一个 Future 列表,并返回一个新的 Future,当列表中所有 Future 都成功完成时,这个新的 Future 才会完成,并携带一个包含所有结果的列表。如果其中任何一个 Future 失败,Future.wait() 也会立即失败。

dart
复制代码
Future<String> fetchAsset(String assetName, int delaySeconds) {
  return Future.delayed(Duration(seconds: delaySeconds), () => "Loaded $assetName");
}

void main() async {
  print("开始加载所有资源...");
  try {
    List<String> results = await Future.wait([
      fetchAsset("Image", 2),
      fetchAsset("Audio", 1),
      fetchAsset("Video", 3),
    ]);
    print("所有资源加载完成: $results");
  } catch (e) {
    print("加载资源时发生错误: $e");
  }
  print("主程序结束。");
}
// 预期输出:
// 开始加载所有资源...
// (3秒后)
// 所有资源加载完成: [Loaded Image, Loaded Audio, Loaded Video]
// 主程序结束。

官方文档链接

Flutter 开发中的应用案例

异步编程在 Flutter 应用中至关重要,因为大多数实际应用都需要与网络、文件系统或数据库进行交互,这些都是耗时操作。Futureasync/await 是 Flutter 中处理这些操作的标准方式,它们确保了 UI 的流畅性和响应性。

案例:从网络加载数据并显示

我们将创建一个 Flutter 应用,从一个模拟的网络 API 加载用户列表,并在加载完成后显示在界面上。这个案例将演示 async/await 在网络请求和 UI 更新中的应用。

dart
复制代码
import 'package:flutter/material.dart';
import 'dart:convert'; // 用于 JSON 解码
import 'package:http/http.dart' as http; // 引入 http 包,需要在 pubspec.yaml 中添加依赖

// 模拟的用户数据模型
class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
}

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

  @override
  State<UserListScreen> createState() => _UserListScreenState();
}

class _UserListScreenState extends State<UserListScreen> {
  List<User> _users = [];
  bool _isLoading = false;
  String? _error;

  @override
  void initState() {
    super.initState();
    _fetchUsers(); // 页面初始化时加载用户数据
  }

  Future<void> _fetchUsers() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      // 模拟网络请求,使用一个公共的测试 API
      final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));

      if (response.statusCode == 200) {
        // 请求成功,解析 JSON 数据
        List<dynamic> userJson = jsonDecode(response.body);
        setState(() {
          _users = userJson.map((json) => User.fromJson(json)).toList();
          _isLoading = false;
        });
      } else {
        // 请求失败
        setState(() {
          _error = 'Failed to load users: ${response.statusCode}';
          _isLoading = false;
        });
      }
    } catch (e) {
      // 捕获网络错误或其他异常
      setState(() {
        _error = 'Error fetching users: $e';
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('用户列表'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: _isLoading ? null : _fetchUsers, // 正在加载时禁用刷新按钮
          ),
        ],
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator()) // 加载中显示进度指示器
          : _error != null
              ? Center(child: Text('错误: $_error')) // 显示错误信息
              : ListView.builder(
                  itemCount: _users.length,
                  itemBuilder: (context, index) {
                    final user = _users[index];
                    return Card(
                      margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
                      child: ListTile(
                        leading: CircleAvatar(child: Text(user.id.toString())),
                        title: Text(user.name),
                        subtitle: Text(user.email),
                      ),
                    );
                  },
                ),
    );
  }
}

void main() {
  runApp(const MaterialApp(home: UserListScreen()));
}

案例分析:

  • Future<void> _fetchUsers() async { ... }_fetchUsers 函数被标记为 async,这意味着它是一个异步函数,并且会返回一个 Future<void>。函数内部可以使用 await 关键字。
  • await http.get(Uri.parse('...')):这里使用了 await 关键字来等待 http.get 方法返回的 Future 完成。http.get 是一个异步操作,它会发送一个 HTTP GET 请求并等待响应。await 会暂停 _fetchUsers 函数的执行,直到网络请求完成并返回响应。
  • try { ... } catch (e) { ... }:异步操作中经常会发生错误(如网络连接失败、服务器返回错误状态码)。使用 try-catch 块可以捕获这些错误,从而避免应用崩溃,并向用户显示友好的错误信息。
  • setState(() { _isLoading = true; ... });:在异步操作开始前,我们通过 setState 更新 _isLoading 状态为 true,从而在 UI 上显示加载指示器。当操作完成后(无论成功或失败),再次调用 setState 更新 _isLoadingfalse,并显示数据或错误信息。
  • User.fromJson(Map<String, dynamic> json):这是一个工厂构造函数,用于将 JSON 数据解析成 User 对象。在处理网络数据时,通常需要将原始的 JSON 字符串转换为 Dart 对象,以便于操作。
  • CircularProgressIndicator():在数据加载过程中,我们显示一个圆形进度指示器,提升用户体验。

这个案例清晰地展示了 Futureasync/await 在 Flutter 应用中处理网络请求的关键作用。通过它们,我们可以编写出非阻塞、响应迅速且易于理解的异步代码,从而构建出高性能和用户友好的移动应用程序。

注意: 运行此案例需要在 pubspec.yaml 文件中添加 http 依赖:

yaml
复制代码
dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.1 # 添加此行

然后运行 flutter pub get 获取依赖。