MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter中的Future与async/await:异步编程的核心

2021-03-216.1k 阅读

Flutter中的异步编程基础

在Flutter开发中,异步编程是非常重要的一部分。随着应用程序功能越来越复杂,涉及到网络请求、文件读取等I/O操作时,同步执行这些操作会导致UI线程阻塞,造成应用程序卡顿,用户体验不佳。而异步编程则可以在执行这些耗时操作时,让UI线程继续响应用户交互,保持应用程序的流畅性。

为什么需要异步编程

想象一下,如果在Flutter应用中,每次进行网络请求(比如获取用户信息、加载图片等)都是同步进行的。当网络请求发生时,UI线程会被阻塞,直到请求完成。在这个过程中,用户无法点击按钮、滑动屏幕等,整个应用就像“卡死”了一样。这在现代的移动应用开发中是完全不能接受的。

例如,假设有一个简单的Flutter应用,它需要从服务器获取一张图片并显示在屏幕上。如果使用同步方式获取图片:

// 同步获取图片(仅为示意,实际网络请求不能这样同步写)
ImageProvider getImageSync() {
  // 模拟网络请求获取图片数据
  var imageData = await NetworkAssetBundle(Uri.parse('http://example.com/image.jpg')).load('http://example.com/image.jpg');
  return MemoryImage(imageData.buffer.asUint8List());
}

在实际的Flutter应用中,这样的同步代码会直接报错,因为await只能在异步函数中使用。但即使不考虑这个语法问题,这种同步操作会阻塞UI线程,直到图片数据获取完成,这期间应用无法响应用户操作。

而异步编程允许在进行网络请求等耗时操作时,UI线程继续处理其他任务,比如用户的点击事件、动画更新等。当耗时操作完成后,再通知UI线程更新界面显示获取到的图片。

Future简介

在Flutter中,Future是异步编程的核心概念之一。Future表示一个异步操作的结果,它可能在未来某个时刻完成。Future可以处于以下几种状态:

  1. 未完成(uncompleted):异步操作还在进行中。
  2. 已完成(completed):异步操作成功完成,此时Future包含操作的结果。
  3. 已出错(error):异步操作过程中发生错误,此时Future包含错误信息。

创建一个Future很简单,例如:

Future<int> calculateSquare() {
  return Future.delayed(const Duration(seconds: 2), () {
    return 4 * 4;
  });
}

在上述代码中,Future.delayed函数创建了一个Future,它会在延迟2秒后执行回调函数,并返回4 * 4的结果。这里calculateSquare函数返回一个Future<int>,表示这个Future最终会返回一个int类型的值。

我们可以通过then方法来处理Future完成后的结果:

void main() {
  calculateSquare().then((square) {
    print('The square is: $square');
  });
}

在这个例子中,calculateSquare返回的Future完成后,会调用then方法中的回调函数,并将Future的结果(即16)作为参数传递给回调函数,然后打印出结果。

Future的链式调用

Future支持链式调用,这使得我们可以方便地进行一系列的异步操作。例如,假设我们有一个需要先获取用户ID,然后根据用户ID获取用户详细信息的场景:

Future<String> getUserId() {
  return Future.delayed(const Duration(seconds: 1), () {
    return '12345';
  });
}

Future<String> getUserDetails(String userId) {
  return Future.delayed(const Duration(seconds: 1), () {
    return 'User details for $userId';
  });
}

void main() {
  getUserId()
    .then((userId) {
      return getUserDetails(userId);
    })
    .then((userDetails) {
      print(userDetails);
    });
}

在上述代码中,首先调用getUserId获取用户ID,getUserId返回的Future完成后,将用户ID作为参数传递给getUserDetailsgetUserDetails返回的Future完成后,将用户详细信息打印出来。这种链式调用的方式使得异步操作的流程清晰明了。

Future的错误处理

在异步操作过程中,难免会发生错误。Future提供了方便的错误处理机制。我们可以通过catchError方法来捕获Future执行过程中的错误:

Future<int> divideNumbers(int a, int b) {
  return Future.delayed(const Duration(seconds: 1), () {
    if (b == 0) {
      throw 'Cannot divide by zero';
    }
    return a ~/ b;
  });
}

void main() {
  divideNumbers(10, 0)
    .then((result) {
      print('Result: $result');
    })
    .catchError((error) {
      print('Error: $error');
    });
}

在上述代码中,divideNumbers函数模拟了一个可能会抛出错误(除数为0时)的异步除法操作。当Future执行过程中抛出错误时,catchError方法中的回调函数会被调用,我们可以在这个回调函数中处理错误,比如打印错误信息。

async/await语法糖

虽然Future提供了强大的异步编程能力,但是使用thencatchError进行链式调用在处理复杂的异步操作时,代码可能会变得难以阅读和维护,出现所谓的“回调地狱”。为了解决这个问题,Dart语言引入了asyncawait关键字,它们是基于Future构建的语法糖,使得异步代码看起来更像同步代码,大大提高了代码的可读性和可维护性。

async函数

一个函数如果使用了async关键字进行修饰,那么这个函数就成为了一个异步函数。异步函数总是返回一个Future。例如:

async Future<int> calculateCube() {
  return 3 * 3 * 3;
}

在上述代码中,calculateCube函数被声明为异步函数,它返回一个Future<int>。虽然函数体中没有显式地创建Future,但是Dart会自动将返回值包装成一个已完成的Future

await关键字

await关键字只能在async函数内部使用,它用于暂停异步函数的执行,直到await后面的Future完成。例如:

async Future<void> printSquareAndCube() {
  var squareFuture = calculateSquare();
  var square = await squareFuture;
  print('Square: $square');

  var cubeFuture = calculateCube();
  var cube = await cubeFuture;
  print('Cube: $cube');
}

在上述代码中,printSquareAndCube是一个异步函数。首先创建了calculateSquare返回的Future,然后使用await暂停函数执行,直到这个Future完成并获取其结果赋值给square。接着对calculateCube返回的Future进行同样的操作。这样的代码看起来就像同步代码一样,非常直观。

结合async/await处理错误

使用async/await时,错误处理也变得更加简洁。我们可以使用try-catch块来捕获异步操作过程中的错误:

async Future<void> handleDivisionError() {
  try {
    var result = await divideNumbers(10, 0);
    print('Result: $result');
  } catch (error) {
    print('Error: $error');
  }
}

在上述代码中,handleDivisionError函数使用try-catch块来捕获divideNumbers函数执行过程中可能抛出的错误。如果divideNumbers执行成功,会打印结果;如果发生错误,会在catch块中打印错误信息。

复杂异步场景下的Future与async/await

在实际的Flutter应用开发中,经常会遇到一些复杂的异步场景,比如并发执行多个异步操作、等待多个异步操作全部完成等。Futureasync/await提供了丰富的方法来处理这些场景。

并发执行多个异步操作

有时候我们需要同时执行多个异步操作,而不是顺序执行。例如,我们有一个应用需要同时从不同的API获取用户信息和用户的订单列表:

Future<String> getUserInfo() {
  return Future.delayed(const Duration(seconds: 2), () {
    return 'User information';
  });
}

Future<String> getOrderList() {
  return Future.delayed(const Duration(seconds: 3), () {
    return 'Order list';
  });
}

async Future<void> fetchDataConcurrently() {
  var userInfoFuture = getUserInfo();
  var orderListFuture = getOrderList();

  var userInfo = await userInfoFuture;
  var orderList = await orderListFuture;

  print('User info: $userInfo');
  print('Order list: $orderList');
}

在上述代码中,fetchDataConcurrently函数同时启动了getUserInfogetOrderList两个异步操作。然后分别使用await获取它们的结果。这样可以节省时间,因为两个操作是并发执行的,而不是顺序执行。

Future.wait:等待多个异步操作全部完成

Future.wait方法可以用于等待多个Future全部完成,并返回一个包含所有Future结果的新Future。例如,我们有多个任务需要执行,并且需要在所有任务完成后进行一些汇总操作:

Future<int> task1() {
  return Future.delayed(const Duration(seconds: 1), () {
    return 10;
  });
}

Future<int> task2() {
  return Future.delayed(const Duration(seconds: 2), () {
    return 20;
  });
}

Future<int> task3() {
  return Future.delayed(const Duration(seconds: 3), () {
    return 30;
  });
}

async Future<void> performTasksAndSum() {
  var results = await Future.wait([task1(), task2(), task3()]);
  var sum = results.fold(0, (acc, value) => acc + value);
  print('Sum of task results: $sum');
}

在上述代码中,Future.wait接受一个Future列表,它会返回一个新的Future,这个新Future会在所有传入的Future都完成后完成,并且其结果是一个包含所有传入Future结果的列表。然后我们使用fold方法对结果列表进行求和操作。

Future.any:等待任意一个异步操作完成

Future.wait相反,Future.any方法会返回一个新的Future,这个新Future会在传入的Future列表中任意一个Future完成后就完成,并且其结果是第一个完成的Future的结果。例如,假设我们有多个数据源可以获取数据,只要其中一个数据源获取到数据就可以使用:

Future<String> dataSource1() {
  return Future.delayed(const Duration(seconds: 3), () {
    return 'Data from source 1';
  });
}

Future<String> dataSource2() {
  return Future.delayed(const Duration(seconds: 1), () {
    return 'Data from source 2';
  });
}

Future<String> dataSource3() {
  return Future.delayed(const Duration(seconds: 2), () {
    return 'Data from source 3';
  });
}

async Future<void> getFirstAvailableData() {
  var data = await Future.any([dataSource1(), dataSource2(), dataSource3()]);
  print('First available data: $data');
}

在上述代码中,Future.any会等待dataSource1dataSource2dataSource3中任意一个Future完成,由于dataSource2最快完成,所以最终打印出Data from source 2

在Flutter UI开发中应用异步编程

在Flutter的UI开发中,异步编程无处不在。例如,从网络加载图片、获取JSON数据并更新UI等操作都需要异步处理,以避免阻塞UI线程。

从网络加载图片

Flutter提供了CachedNetworkImage等插件来方便地从网络加载图片,并且这些插件内部就是基于异步编程实现的。如果我们自己手动实现一个简单的图片加载功能,可以使用Image.network,它内部也是通过异步方式获取图片数据:

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

  @override
  State<ImageLoader> createState() => _ImageLoaderState();
}

class _ImageLoaderState extends State<ImageLoader> {
  ImageProvider? _imageProvider;

  @override
  void initState() {
    super.initState();
    loadImage();
  }

  async void loadImage() {
    try {
      var imageData = await NetworkAssetBundle(Uri.parse('http://example.com/image.jpg')).load('http://example.com/image.jpg');
      setState(() {
        _imageProvider = MemoryImage(imageData.buffer.asUint8List());
      });
    } catch (error) {
      print('Error loading image: $error');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Image Loader'),
      ),
      body: Center(
        child: _imageProvider != null? Image(image: _imageProvider!) : const CircularProgressIndicator(),
      ),
    );
  }
}

在上述代码中,loadImage函数是一个异步函数,它使用await来等待网络图片数据的获取。获取成功后,通过setState更新UI显示图片。如果发生错误,会打印错误信息。在build方法中,根据_imageProvider是否为空来决定是显示图片还是显示加载指示器。

获取JSON数据并更新UI

在实际应用中,经常需要从服务器获取JSON数据并更新UI。假设我们有一个API返回用户列表数据,我们可以这样实现:

class User {
  final String name;
  final int age;

  User({required this.name, required this.age});

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

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

  @override
  State<UserList> createState() => _UserListState();
}

class _UserListState extends State<UserList> {
  List<User> _users = [];

  @override
  void initState() {
    super.initState();
    fetchUsers();
  }

  async void fetchUsers() {
    try {
      var response = await http.get(Uri.parse('http://example.com/api/users'));
      if (response.statusCode == 200) {
        var jsonData = json.decode(response.body);
        var userList = (jsonData as List).map((e) => User.fromJson(e)).toList();
        setState(() {
          _users = userList;
        });
      } else {
        print('Failed to fetch users. Status code: ${response.statusCode}');
      }
    } catch (error) {
      print('Error fetching users: $error');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User List'),
      ),
      body: ListView.builder(
        itemCount: _users.length,
        itemBuilder: (context, index) {
          var user = _users[index];
          return ListTile(
            title: Text(user.name),
            subtitle: Text('Age: ${user.age}'),
          );
        },
      ),
    );
  }
}

在上述代码中,fetchUsers函数使用http库发送HTTP GET请求获取用户列表数据。使用await等待请求完成,然后解析JSON数据并转换为User对象列表。最后通过setState更新UI显示用户列表。在build方法中,使用ListView.builder来构建用户列表。

异步编程中的常见问题及解决方法

在使用Futureasync/await进行异步编程时,可能会遇到一些常见问题,下面我们来分析这些问题并提供相应的解决方法。

内存泄漏问题

在异步操作中,如果不注意,可能会导致内存泄漏。例如,在一个StatefulWidget中启动了一个异步任务,但是在State被销毁时,异步任务还没有完成,并且这个异步任务持有对State的引用,就可能导致State无法被垃圾回收,从而造成内存泄漏。

解决方法是在Statedispose方法中取消未完成的异步任务。例如:

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

  @override
  State<AsyncTaskWidget> createState() => _AsyncTaskWidgetState();
}

class _AsyncTaskWidgetState extends State<AsyncTaskWidget> {
  late Future<void> _asyncTask;
  late CancelToken _cancelToken;

  @override
  void initState() {
    super.initState();
    _cancelToken = CancelToken();
    _asyncTask = performAsyncTask(_cancelToken);
  }

  async Future<void> performAsyncTask(CancelToken cancelToken) {
    try {
      for (int i = 0; i < 10; i++) {
        if (cancelToken.isCancelled) {
          return;
        }
        await Future.delayed(const Duration(seconds: 1));
        print('Task progress: $i');
      }
    } catch (error) {
      print('Task error: $error');
    }
  }

  @override
  void dispose() {
    _cancelToken.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Async Task'),
      ),
      body: const Center(
        child: Text('Async task is running...'),
      ),
    );
  }
}

class CancelToken {
  bool _isCancelled = false;

  bool get isCancelled => _isCancelled;

  void cancel() {
    _isCancelled = true;
  }
}

在上述代码中,_asyncTask是一个异步任务,_cancelToken用于取消这个任务。在initState中初始化任务和取消令牌,在performAsyncTask中每次循环检查取消令牌,如果已取消则返回。在dispose方法中调用_cancelToken.cancel()来取消任务,这样在State被销毁时,未完成的异步任务会被正确处理,避免内存泄漏。

竞争条件问题

竞争条件是指当多个异步操作同时访问和修改共享资源时,可能会导致不可预测的结果。例如,多个异步任务同时更新同一个计数器:

class Counter {
  int value = 0;

  void increment() {
    value++;
  }
}

async Future<void> updateCounter(Counter counter) {
  for (int i = 0; i < 1000; i++) {
    counter.increment();
  }
}

void main() {
  var counter = Counter();
  List<Future<void>> tasks = [];
  for (int i = 0; i < 10; i++) {
    tasks.add(updateCounter(counter));
  }

  Future.wait(tasks).then((_) {
    print('Final counter value: ${counter.value}');
  });
}

在上述代码中,理论上10个任务每个任务对计数器增加1000次,最终计数器的值应该是10000。但是由于竞争条件,实际结果可能小于10000。这是因为多个任务同时访问和修改counter.value,可能会出现某个任务读取的值还没来得及更新,就被其他任务覆盖了。

解决方法是使用锁机制,例如Mutex。在Dart中,可以使用package:mutex库来实现:

import 'package:mutex/mutex.dart';

class Counter {
  int value = 0;
  final Mutex _mutex = Mutex();

  async void increment() async {
    await _mutex.protect(() {
      value++;
    });
  }
}

async Future<void> updateCounter(Counter counter) {
  for (int i = 0; i < 1000; i++) {
    await counter.increment();
  }
}

void main() {
  var counter = Counter();
  List<Future<void>> tasks = [];
  for (int i = 0; i < 10; i++) {
    tasks.add(updateCounter(counter));
  }

  Future.wait(tasks).then((_) {
    print('Final counter value: ${counter.value}');
  });
}

在上述代码中,Mutexprotect方法确保了increment方法中的操作是线程安全的,每次只有一个任务可以进入protect的回调函数,从而避免了竞争条件。

通过深入理解Futureasync/await,以及掌握在各种复杂场景下的应用和常见问题的解决方法,开发者能够在Flutter开发中高效地进行异步编程,打造出流畅、响应性好的应用程序。无论是简单的网络请求,还是复杂的多任务并发处理,都能游刃有余地应对。