[Dart翻译]Dart异步编程。未来

427 阅读10分钟

原文地址:medium.com/dartlang/da…

原文作者:medium.com/@kathyw_392…

发布时间:2019年9月18日 - 8分钟阅读

image.png

许多异步的Dart API都会返回Future。

Dart异步编程最基本的API之一是期货--Future类型的对象。在大多数情况下,Dart的期货与其他语言中的未来或承诺API非常相似。

本文讨论了Dart期货背后的概念,并告诉你如何使用Future API。它还讨论了Flutter FutureBuilder小部件,它可以帮助你根据未来的状态,异步更新Flutter UI。

多亏了像async-await这样的Dart语言特性,你可能永远不需要直接使用Future API。但你几乎可以肯定在你的Dart代码中会遇到期货。而且你可能想要创建期货或读取使用Future API的代码。

本文是基于Flutter in Focus视频系列《Dart中的异步编程》的第二篇文章。第一篇文章《Isolates和事件循环》介绍了Dart支持后台工作的基础。

如果你喜欢通过看或听来学习,本文中的所有内容都会在下面的视频中涉及。

www.youtube.com/watch?v=OTS…

Andrew Brogdon的视频是本文的灵感来源。


你可以把期货想象成数据的小礼盒。有人递给你一个这样的礼盒,一开始是封闭的。一会儿盒子就弹开了,里面不是有价值就是有错误。

所以一个未来可以处于3种状态之一。

  1. 未完成 礼盒已关闭
  2. 已完成,有一个值。盒子打开了,你的礼物(数据)已经准备好了。
  3. 有错误的完成。盒子打开了,但出了点问题。

你将要看到的大部分代码都是围绕着处理这三种状态的。你收到了一个未来,你需要决定在盒子打开之前要做什么,当它打开时有一个值时要做什么,以及如果出现错误时要做什么。你会经常看到这种1-2-3的模式。

image.png

未来的3种状态

你可能还记得我们关于Dart事件循环的文章中的事件循环(如下图)。关于期货的一个好东西是,它们实际上只是为了让使用事件循环更容易而构建的一个API。

image.png

Dart事件循环一次只处理一个事件。

你编写的Dart代码由一个线程执行。在你的应用程序运行的整个过程中,那个小线程一直在不停地转来转去,从事件队列中获取事件并处理它们。

假设你有一些下载按钮的代码(下面实现为RaisedButton)。用户点击后,你的按钮开始下载图片。

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

首先发生点击事件。事件循环获取该事件,并调用你的tap处理程序(你使用RaisedButton构造函数的onPressed参数设置)。你的处理程序使用 http 库发出请求 (http.get()),并得到一个未来的回报 (myFuture)。

所以现在你已经有了你的小盒子,myFuture。它开始时是关闭的。为了在它打开时注册一个回调,你可以使用 then()。

一旦你有了你的礼物盒,你就等待。也许会有一些其他事件进来,用户做了一些事情,而你的小盒子就坐在那里,而事件循环一直在转。

最终,图片的数据被下载,http库说:"太好了!我已经把未来的东西放在这里了。"很好,我在这里找到了这个未来" 它把数据放进盒子里,然后弹开,这就触发了你的回调。

现在,你交给then()的那段小代码执行了,它显示了图像。

在整个过程中,你的代码从来没有直接接触过事件循环。不管还有什么事情在进行,或者有什么其他的事件进来。你需要做的只是从http库中获取未来,然后说当未来完成时要做什么。

在真实的代码中,你还需要处理错误。我们稍后会告诉你如何做到这一点。


让我们仔细看看Future的API,有些你刚刚看到的使用。

好的,第一个问题:你如何获得一个Future的实例?大多数情况下,你不会直接创建期货。这是因为许多常见的异步编程任务已经有库为你生成期货。

例如,网络通信会返回一个未来。

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

获取对共享偏好的访问也会返回一个未来。

final myFuture = SharedPreferences.getInstance();

但你也可以使用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(dartpad.dev)中运行这段代码,整个主函数在给Future()构造函数的函数之前完成。这是因为Future()构造函数一开始只是返回一个未完成的未来。它说:"这是这个盒子。你先拿着它,待会我去运行你的函数,给你放一些数据进去。" 这是前面代码的输出。

Done with main().
Creating the future.

另一个构造函数Future.value(),当你已经知道未来的值时,这个构造函数很方便。当你在构建使用缓存的服务时,这个构造函数很有用。有时你已经有了你需要的值,所以你可以直接把它放在那里。

final myFuture = Future.value(12);

Future.value() 构造函数有一个对应的函数来完成错误的处理。它叫做Future.error(),工作方式基本相同,但接收一个错误对象和一个可选的堆栈跟踪。

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

最方便的未来构造函数可能是Future.delayed()。它的工作原理和Future()一样,只是它在运行函数和完成未来之前会等待一个指定的时间长度。

使用Future.delayed()的一种方法是当你在创建模拟网络服务进行测试时。如果您需要确保您的加载微调器正确显示,延迟的未来是您的朋友。

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

使用future

现在你知道了期货的来历,我们来谈谈如何使用期货。正如我们前面所提到的,使用future主要是为了核算它可能处于的三种状态:未完成、带值完成或带错误完成。

下面的代码使用Future.delayed()来创建一个3秒后完成的未来,其值为100。

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

当这段代码执行时,main()从上到下运行,创建未来并打印 "等待值..." 那整个时间,未来都没有完成。再过3秒就不完成了。

要使用完成的值,你可以使用then()。那是每个future上的一个实例方法,你可以用它来注册一个回调,当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()还会返回一个自己的未来,匹配你给它的任何函数的返回值。因此,如果你需要进行几个异步调用,你可以把它们链在一起,即使它们有不同的返回类型。

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

回到我们的第一个例子,如果那个初始的未来没有完成一个值,会发生什么情况--如果它完成时出现错误怎么办?then()方法期望得到一个值。你需要一种方法来注册另一个回调,以备出现错误。

答案是使用 catchError()。它的工作原理和 then()一样,只是它接收的是一个错误而不是一个值,并且如果未来以错误完成,它就会执行。就像 then()一样,catchError()方法也会返回一个自己的未来,所以你可以建立一个完整的 then()和 catchError()方法的链子,互相等待。

注意:如果你使用async-await语言特性,你不需要调用then()或catchError()。取而代之的是等待完成的值,并使用try-catch-finally来处理错误。详情请看Dart语言之旅的异步支持部分

这里有一个使用 catchError()来处理未来完成错误的情况的例子。

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...');
}

现在你已经走到了这一步,希望你能看到未来的三种状态是如何经常通过代码的结构反映出来的。在前面的例子中,有三个块。

  1. 第一个块是创建一个未完成的未来。
  2. 然后有一个函数要在未来完成时调用一个值。
  3. 然后是另一个函数,如果未来完成时出现错误,则调用该函数。

还有一个你可能想使用的方法:whenComplete()。你可以用它来执行一个函数,当未来完成时,无论它是以一个值还是一个错误完成。

它有点像 try-catch-finally 中的 finally 块。有代码执行,如果一切顺利,错误的代码,和代码,运行无论什么。

在Flutter中使用期货

所以这就是你如何创建期货,以及如何使用它们的价值的一点。现在让我们来谈谈把它们放在Flutter中工作。

假设你有一个网络服务要返回一些JSON数据,你想显示这些数据。你可以创建一个StatefulWidget,它可以创建未来,检查完成或错误,调用setState(),并通常手动处理所有的布线。

或者你可以使用FutureBuilder。它是Flutter SDK自带的一个小部件。你给它一个未来和一个构建函数,当未来完成时,它就会自动重建它的子程序。

FutureBuilder小组件通过调用它的构建函数来工作,它需要一个上下文和未来当前状态的快照。

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

你可以检查快照来查看未来是否以错误完成。

    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");
      },
    );

如果hasError和hasData都不是true, 那你就知道你还在等待, 你也可以输出一些东西.

    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和FutureBuilder API来创建期货并使用它们的完成值。

如果你想了解更多关于使用期货的知识--可以选择使用可运行的例子和交互式练习来测试你的理解--请查看关于期货、异步和等待的异步代码实验室。

或者继续看下一个Dart系列中的异步编程视频。它讲的是流,它和期货很像,因为它们可以提供值或错误。但是期货只给你一个结果就停止了,而流则一直在继续。

www.youtube.com/watch?v=nQB…

非常感谢Andrew Brogdon,他创建了本文所基于的视频。