Dart用于异步编程的最基本的API之一是futures—Future类型的对象。大多数情况下,Dart的未来与其他语言中的future或promise API非常相似。
本文讨论Dart futures背后的概念,并告诉您如何使用Future API。本文还讨论了Flutter FutureBuilder小部件,它帮助您根据未来的状态异步更新Flutter UI。
由于Dart语言的特性,如async await,您可能永远不需要直接使用未来的API。但您几乎肯定会在Dart代码中遇到Future。您可能希望创建Future或读取使用FutureAPI的代码。
你可以把期货想象成数据的小礼盒。有人递给你一个这样的礼盒,一开始是关着的。过了一会儿,盒子突然打开,里面要么有一个值,要么有一个错误。
因此,Future可能处于以下三种状态之一:
-
未完成:礼品盒已关闭。
-
用值完成:盒子打开,您的礼物(数据)准备就绪。
-
完成时出现错误:框已打开,但出现了问题。
您将要看到的大部分代码都围绕着处理这三种状态。你收到了一个Future,你需要决定在盒子打开之前该做什么,当盒子打开时该做什么,如果出现错误该做什么。你会经常看到1-2-3模式。
您可能还记得我们关于Dart事件循环的文章中的事件循环(如下图所示)。了解futures的一件好事是,它们实际上只是一个API,用于简化事件循环的使用。
您编写的Dart代码由一个线程执行。在你的应用程序运行的整个过程中,一个小线程一直在循环,从事件队列中提取事件并处理它们。
假设您有一些用于下载按钮的代码(下面实现为RaisedButton)。用户点击,您的按钮开始下载图片。
RaisedButton(
onPressed: () {
final myFuture = http.get('https://my.image.url');
myFuture.then((resp) {
setImage(resp);
});
},
child: Text('Click me!'),
)
首先发生tap事件。事件循环获取事件,并调用tap处理程序(使用onPressed参数将其设置为RaisedButton构造函数)。您的处理程序使用http库发出请求(http.get()),并获得一个未来作为回报(myFuture)。
现在你有了你的小盒子,我的Future。它一开始是封闭的。要在回调打开时为其注册回调,请使用then()。
一旦你有了礼物盒,你就要等待。也许还有其他一些事件,用户做了一些事情,而你的小盒子就在那里,而事件循环一直在循环。
最终,图像的数据被下载,http库说,“太好了!我就在这里拥有了这个未来。”它将数据放入框中并将其打开,从而触发回调。
现在,您交给then()的那一小段代码将执行,并显示图像。
在整个过程中,您的代码不必直接接触事件循环。不管发生了什么,也不管发生了什么其他事件。您所需要做的就是从http库中获取Future,然后在Future完成时说要做什么。在实际代码中,还需要注意错误。稍后我们将向您演示如何执行此操作。
让我们更仔细地看一下未来的API,其中一些API刚刚在使用中。 好的,第一个问题:如何获得Future的实例?大多数时候,你不会直接创造Future。这是因为许多常见的异步编程任务已经有了为您生成未来的库。
例如,网络通信返回一个Future:
final myFuture = http.get('http://example.com');
访问共享首选项还将返回一个Future:
final myFuture = SharedPreferences.getInstance();
但您也可以使用Future构造函数来创建Future。
Future constructors
最简单的构造函数是Future(),它接受函数并返回与函数返回类型匹配的Future。稍后,函数异步运行,Future将使用函数的返回值完成。下面是使用Future()的示例:
void main() {
final myFuture = Future(() {
return 12;
});
}
让我们添加两个print语句,以明确异步部分:
void main() {
final myFuture = Future(() {
print('Creating the future.'); // Prints second.
return 12;
});
print('Done with main().'); // Prints first.
}
如果在DartPad(DartPad.dev)中运行该代码,则整个主函数将在赋予Future()构造函数的函数之前完成。这是因为Future()构造函数首先返回一个未完成的Future。它说,“这是这个盒子。你先拿着它,稍后我会运行你的函数并为你放一些数据。”下面是前面代码的输出:
Done with main().
Creating the future.
另一个构造器,Future.value(),在您已经知道Future的value时非常方便。当您构建使用缓存的服务时,此构造函数非常有用。有时,您已经拥有了所需的值,因此可以直接将其弹出:
final myFuture = Future.value(12);
Future.value()构造函数有一个用于以错误完成的对应项。这叫做Future.error(),其工作方式基本相同,但采用错误对象和可选堆栈跟踪:
final myFuture = Future.error(ArgumentError.notNull('input'));
最方便的未来构造函数可能是Future.delayed()。它的工作原理与Future()类似,只是它在运行函数和完成Future之前等待指定的时间长度。
final myFuture = Future.delayed(
const Duration(seconds: 5),
() => 12,
);
Using futures
既然你知道了futures的来源,让我们来谈谈如何使用它们。正如我们前面提到的,使用futures主要是考虑它可能处于的三种状态:未完成、带值完成或带错误完成。
下面的代码使用Future.delayed()创建一个值为100的3秒后完成的Future。
void main() {
Future.delayed(
const Duration(seconds: 3),
() => 100,
);
print('Waiting for a value...');
}
当执行此代码时,main()自上而下运行,创建未来并打印“等待值…”整个过程中,Future都是未完成的。再过3秒钟它就完成不了了。
要使用完成的值,可以使用then()。这是每个future上的实例方法,您可以使用它在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()还返回它自己的Future,与给定的任何函数的返回值相匹配。因此,如果需要进行几个异步调用,可以将它们链接在一起,即使它们具有不同的返回类型。
_fetchNameForId(12)
.then((name) => _fetchCountForName(name))
.then((count) => print('The count is $count.'));
回到我们的第一个例子,如果最初的Future没有以一个值完成会发生什么?如果它以一个错误完成会发生什么?then()方法需要一个值。您需要一种在发生错误时注册另一个回调的方法。
注意:如果使用async-await 语言功能,则不需要调用then()或catchError()。相反,您需要等待完成的值,然后使用try-catch-finally来处理错误。有关详细信息,请参阅Dart语言教程的异步支持部分。
下面是一个使用catchError()处理future以错误完成的情况的示例:
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()指定测试函数的示例:
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的三种状态是如何通过代码的结构反映出来的。上例中有三个块
-
第一个区块创建了一个未完成的Future。
-
如果future以一个值完成,则需要调用一个函数。
-
如果future完成时出现错误,则需要调用另一个函数。
您可能还想使用另一种方法:whenComplete()。您可以在将来完成时使用它来执行函数,无论它是带有值还是带有错误。
这有点像“最后一次尝试”中的“最后一次拦截”。如果一切正常,就会执行代码、错误代码和不管发生什么都可以运行的代码。
Using futures in Flutter
这就是你如何创造Future,以及如何利用它们的价值。现在,让我们谈谈如何让他们在Flutter中工作。
假设您有一个将返回一些JSON数据的网络服务,并且您希望显示该数据。您可以创建一个StatefulWidget,它创建未来、检查完成或错误、调用setState(),并通常手动处理所有连接。
也可以使用FutureBuilder。这是Flutter SDK附带的小部件。您给它一个future和一个builder函数,它会在future完成时自动重建其子对象。
FutureBuilder小部件通过调用其builder函数来工作,该函数获取上下文和未来当前状态的快照。
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都不是真的,那么您知道您还在等待,并且您也可以为此输出一些东西。
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代码中,您也可以看到这三种状态是如何不断弹出的:未完成、已完成(带值)和已完成(带错误)。
Summary
本文讨论了futures代表什么,以及如何使用Future和FutureBuilder API创建futures并使用它们的完整值。
如果您想了解有关使用futures的更多信息,可以选择使用可运行示例和交互式练习来测试您的理解,请查看futures、async和await上的异步代码库。