【译】Dart/Flutter中的异步编程

1,213 阅读10分钟

背景

好久没有更文了,为避免让广大朋友产生我消失了的错觉,所以就又水一文,能帮助一个是一个。 本来想自己写写文章,后来发现翻译也不错。这里是英文原文

简述

final myFuture = http.get("https://example.com");

正如上面的代码,很多Dart异步API都是返回的FutureFuture也是Dart异步编程中最基础的一个概念。总地来说,Dart中的Future和其他编程语言中的future或者promise大同小异。

本文将围绕Future背后的概念以及如何使用Future展开。也会讲Flutter中的FutureBuilder控件,这个控件会基于Future状态来帮助你异步地更新Flutter UI

得益于Dartasync-await这样的特性,我们可能永远不会直接使用Future这个API。但是我们也很难避免在Dart代码中遇到Future,毕竟我们可能要创建Future或者阅读一些和Future有关的代码。

如何理解Future

我们可以简单地将数据比喻成礼物。现在有个朋友要送给我们一个礼盒,当礼盒装好的那一刻,这个过程就开始了。一段时间后我们需要打开这个神秘盒子,里面的礼物可能完好无损也可能是损坏了,也就是说当一个Future完成后,对应的结果可能是我们期望的数据,也可能是一个错误。

我们可以这个过程归纳成三种状态:

  • 未完成:礼盒封装好了。
  • 完成了并且得到了对应的值:礼盒打开,并且我们的礼物(data)已经准备好了。
  • 完成了但发生了错误:礼盒打开,但礼物却损坏了(error)。

绝大部分情况下,我们无非都是围绕着这三种状态进行一些处理。当我们接收一个Future时,我们会一直等到我们打开礼盒,然后我们才会决定如何处理,比如说可以正常接收到值时,我们应该如何处理,又或者说,当发生错误的时候我们又要怎么做。我们可以经常看到1-2-3这样的过程:

说到Future我们不得不提到Event-Loop(如下图所示,也可以在上面的系列视频中学习)。关于Future,我们需要知道,Future只是一个帮助我们可以更简单使用Event-Loop的API。

Event-Loop示意图

我们写的Dart代码是由单一线程执行的。当我们的应用在运行时,这个线程一直在不停地运行啊运行,然后不停得从事件队列(Event Queue)中拾取事件并对事件进行处理。

为了更好解释Event-LoopFuture,我们看个简单的例子。

假如说,我们要实现一个下载功能,当用户点击了按钮,程序会自动下载一下图片。,我们用RaisedButton简单实现一下:

RaisedButton(
  onPressed: () {
    final myFuture = http.get('https://my.image.url');
    myFuture.then((resp) {
      setImage(resp);
    });
  },
  child: Text('Click me!'),
)

我们一起理理这个过程。 首先,触发点击按钮事件。Event Loop捕获了点击事件,然后调用了点击监听器(就是在RaisedButton构造函数中传入的onPressed)。我们的onPressed使用了http库进行了一次HTTP请求(http.get()),并且这个请求返回了一个Future(myFuture)。

现在我们已经得到了我们的礼盒--myFuture。现在礼盒已经装好了。为了监听打开礼盒的回调,我们要使用then()

一旦我们装好了礼盒,我们就需要等待。也话在这个期间有其他的事件进入Event-Loop,用户做了一些其他的事情,你的礼盒还放在那里的同时,Event-Loop依然保持运行。

最终,图片数据被下载下来,然后http库会告诉我们“好极了,我已经拿了Future”。然后它把数据装进礼盒中并把礼盒打开,这时就触发了我们的回调。

现在,then()中的代码片断就被执行了,并向用户显示图片。

整个过程中,不管有什么其他的任务正在进行或者有其他任务进入,我们的代码从来也没有直接接触Event-Loop。这个过程不需要关心有什么其他任务在进行,或者有什么其他事件进入。我们所要作的就是从http库得到Future,然后告诉程序在Futuer完成后需要做什么。

在现实的代码中,我们还要关注错误。我们稍后涉及到这点。


现在我们更近一步地了解一下FutureAPI,有些正是我们刚刚看到的。

第一个问题,我们怎么得到一个Future实例?大部分情况下,我们不会直接创建Future。因为大部分常见的异步编程任务已经有对应的库了,这些库可以为我们生成Future

比如说,网络请求返回了一个Future

final myFuture = http.get('http://example.com');

得到一个shared preferences也返回一个Future

final myFuture = SharedPreferences.getInstance();

当然了,我们也可以通过Future的构造方法创建Future

Future构造方法

最简单的Future构造方法是Future(), 这个构造方法的参数是一个函数,并且返回一个和该函数返回值类型一样的Future。过一会这个函数会异步地执行,并且这个Future完成时会返回该函数的值。看一下Future()的例子:

void main() {
  final myFuture = Future(() {
    return 12;
  });
}

让我们加入一些打印语句,这样会让异步部分更加明显:

void main() {
  final myFuture = Future(() {
    print('Creating the future.'); // Prints second.
    return 12;
  });
  print('Done with main().'); // Prints first.
}

如果我们在DartPad上运行这段代码,整个main函数会在传给Future()构造方法的函数结束前结束。这是因为Future()的构造方法恰好先返回了一个未完成的Future。这意味着,“这是个盒子。你现在需要拿着他,然后过一会我会执行你的函数并且把一些数据装进去”,我们再看一下之前代码的输出结果:

Done with main().
Creating the future.

另一个构造方法是Future.value(),它是用来处理你已经知道Future返回值的。这个构造方法在我们构建使用了缓存的服务时很有用。有的时候我们已经持有我们需要地值了,所以我们可以直接返回:

final myFuture = Future.value(12);

Future.value()还一个相对立构造方法,这个构造方法在完成时会返回一个错误。它是Future.error(),而它的工作原理也基本相同,但是这个构造方法会承载一个错误对象和一个可选的stacktrace

final myFuture = Future.error(ArgumentError.notNull('input'));

最方便的构造方法可能是Future.delay()了。它和Future()一样,只不过它先会等待一段指定的时间后,再执行传入的函数然后再完成Future

Future.delay()的一种使用场景就是当我们在测试时需要mock网络服务。如果我们确保加载指示器可以正确显示,那么这个Future.delay()就有用武之地了:

final myFuture = Future.delayed(
  const Duration(seconds: 5),
  () => 12,
);

使用Future

我们现在已经对Future有个基本地了解,现在我们要学习一下怎么使用。正好我们之前所说,使用Future基本上就是围绕着三种状态:未完成完成了并且得到了对应的值完成了但发生了错误

下面的代码使用Future.delay()创建了一个Future,功能是在3s后Future完成并返回100。

void main() {
  Future.delayed(
    const Duration(seconds: 3),
    () => 100,
  );
  print('Waiting for a value...');
}

当这段代码执行时,main()从上到下执行,创建一个Future并打印Waiting for a value...,这个时候Future还没完成。再过三秒钟它也不会完成。

为了使用Future返回的值,我们可以使用then()。我们可以通过then()注册一个回调,通过这个回调我们可以获取Future完成时的数据。我们给then()传入一个函数,这个函数只有一个参数,参数的类型和Future的返回值类型一样。一旦Future完成了返回并返回数据,这个函数就会被调用并将对应的数据传递过去:

void main() {
  Future.delayed(
    const Duration(seconds: 3),
    () => 100,
  ).then((value) {
    print('The value is $value.'); // Prints later, after 3 seconds.
  });
  print('Waiting for a value...'); // Prints first.
}

让我们看一下输出日志:

Waiting for a value... (3 seconds pass until callback executes)
The value is 100.

除了执行我们的代码,then()本身也会返回自己的Future,与我们提供的函数的返回值一样。所以如果我们需要进行一连串的异步调用,我们可以选择链式调用他们,尽管他们的返回类型不同:

_fetchNameForId(12)
    .then((name) => _fetchCountForName(name))
    .then((count) => print('The count is $count.'));

回到我们第一个例子中,如果初始化的Future完成时没有对应的数据会发生什么--也就是说如果发生了错误什么如何?而then()方法是需要一个值的。这时我们需要注册另一个回调来处理发生错误的情况。

答案是使用[catchError()](https://api.dart.dev/stable/2.8.2/dart-async/Future/catchError.html)。它和then()一样,只不过catchError()传递的不是数据,当Future执行过程中如果发生了错误,这个方法会被调用。就像then()一样,catchError()本身也返回一个自己的Future,所以我们可以构建一个then()catchError()的调用链,这两个方法可以相互待。

笔记:如果你在代码中使用了async-await,我们就不必使用then()catchError()了。因为我们可以使用await直接获取对应的值,可以用try-catch-finally来处理错误。更详细的资料,看一下Dart官方文档中关于异步支持的章节吧

下面的例子展示了如何用catchError()处理Futuer中的错误:

void main() {
  Future.delayed(
    Duration(seconds: 3),
    () => throw 'Error!', // Complete with an error.
  ).then((value) {
    print(value);
  }).catchError((err) {
    print('Caught $err'); // Handle the error.
  });
  print('Waiting for a value...');
}

我们甚至可以给catchError()一个测试函数,这个函数可以在回调调用前对错误进行测试。通过这种方式,我们可以拥有多个catchError()函数,每个函数检查一种不同类型的错误。下面的例子展示了如何使用一个检测函数对错误进行测试,catchError()中的参数test是可选的。

void main() {
  Future.delayed(
    Duration(seconds: 3),
    () => throw 'Error!',
  ).then((value) {
    print(value);
  }).catchError((err) {
    print('Caught $err');
  }, test: (err) { // Optional test parameter.
    return err is String;
  });
  print('Waiting for a value...');
}

现在文章你已经看到这里了,希望你已经理解了Future的三种状态,已经理解了三种状态是怎么在代码中体现的。在上面的例子中分三块:

  1. 第一块创建了一个未完成的Future
  2. Future执行完成并返回了对应数据,调用了then()里的回调。
  3. Future执行完成了但发生错误,调用了catchError()里的回调。

还有另一个方法你也可能想使用:whenComplete()。正如方法名字一样,这个方法会在Future完成时被调用,不管Future是返回了数据还是抛出了错误。

这有点像try-catch-finally中的finally。无论是代码是正确执行了,还是出现了错误,它都会被执行。

在Flutter中使用Future

我们刚刚一直在说如何创建Future,还有如何使用Future中的数据。现在我们要说说如何在Flutter中实践。

假如说我们一个网络服务,这个网络服务会返回JSON数据,我们想要展示这些数据。我们可以当然使用StatefulWidget,然后通过创建Future,根据Future的执行情况,然后调用setState(),所有的这些都要我们手动控制。

当然了我们也可以使用FutureBuilder。这是一个Flutter SDK中的一个控件。我们传一个Future和一个builder函数,当Future完成时,它会自动重构它的子控件。

FutureBuilder会调用builder函数,这个函数有两个参数,一个是context,另一个是snapshot,它是当前Future的状态。

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Use a FutureBuilder.
    return FutureBuilder<String>(
      future: _fetchNetworkData(),
      builder: (context, snapshot) {},
    );
  }
}

我们可以通过检查snapshot来看看Future是否发生了错误:

 return FutureBuilder<String>(
      future: _fetchNetworkData(5),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          // Future completed with an error.
          return Text(
            'There was an error',
          );
        }
        throw UnimplementedError("Case not handled yet");
      },
    );

我们也可以通过检查hasData查看是否返回了数据:

return FutureBuilder<String>(
      future: _fetchNetworkData(5),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          // Future completed with an error.
          return Text(
            'There was an error',
          );
        } else if (snapshot.hasData) {
          // Future completed with a value.
          return Text(
            json.decode(snapshot.data)['field'],
          );
        }
        throw UnimplementedError("Case not handled yet");
      },
    );

如果hasErrorhasData都不是true,那么我们就知道Future还在执行中,我们还需要继续等待,这时我们也可以输出一些其他的信息:

    return FutureBuilder<String>(
      future: _fetchNetworkData(5),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          // Future completed with an error.
          return Text(
            'There was an error',
          );
        } else if (snapshot.hasData) {
          // Future completed with a value.
          return Text(
            json.decode(snapshot.data)['field'],
          );
        } else {
          // Uncompleted.
          return Text(
            'No value yet!',
          );
        }
      },
    );

即使在Flutter代码中我们依然可以看到三种状态是如何体现的。

总结

本文阐述了Future是如何呈现的以及怎么样使用FutureFutureBuilder相关API创建Future,并且使用Future中的数据。

如果你想学习更多关于如何使用Future,可以通过运行示例代码和交互练习来测试你对Future的理解--去codelab上练一下futures,aysnc,await吧。