异步 UI
异步 UI
在 Flutter 中有什么是跟 Device.BeginOnMainThread() 方法是相等的?
Dart 拥有一个单线程执行模型,支持“隔离”(在另一个线程上运行Dart代码的方法)、事件循环和异步编程。除非生成一个“隔离”,否则您的Dart代码会在主UI线程中运行,并由一个事件循环来驱动。
Dart 的单线程模型并不意味着需要以会导致UI冻结的阻塞操作方式来运行所有内容。更多地像 Xamarin.Forms 一样需要让 UI 线程保持空闲。您将使用“async”/“wait”来执行任务,其中必须等待响应。
在 Flutter 中,使用 Dart 语言提供的异步工具(也称为 async/await)来执行异步工作。这跟 C# 很像,并且对于 Xamarin.Forms 开发者来说应该是非常容易使用的。
例如,您可以使用 async/await 运行网络请求代码,而不会导致UI挂起,并让Dart完成繁重的工作:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
一旦完成等待的网络调用后,通过调用 setState() 更新UI,这将触发 widget 子树的重新构建并更新数据。
下面的实例异步加载数据并在一个 ListView 中显示:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(new SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => new _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
}));
}
Widget getRow(int i) {
return new Padding(
padding: new EdgeInsets.all(10.0),
child: new Text("Row ${widgets[i]["title"]}")
);
}
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
有关在后台工作、以及 Flutter 与 Android 的不同之处的更多信息,请参考下一节。
如何将工作转移到后台线程?
因为 Flutter 是单线程的,并且运行一个事件循环,所以您不必担心线程管理或产生后台线程。这一点与 Xamarin.Forms 非常相似。如果您正在做 I/O 密集型的工作,比如磁盘访问或网络调用,那么您可以安全地使用 async/await,这样就一切就绪了。
另一方面,如果您需要做计算密集型的工作,使CPU保持忙碌,那么您希望将它移动到“隔离”状态,以避免阻塞事件循环,就像您将任何类型的工作放在主线程之外一样。这类似于通过 Xamarin.Forms 中的 Task.Run() 将内容移动到另一个线程。
对于 I/O 密集型的工作,将函数声明为一个 异步 函数,并在函数内部 等待 长时间运行的任务:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
这是您通常执行网络或数据库调用的方式,它们都是I/O操作。
然而,有时您可能正在处理大量数据而UI挂起了。在 Flutter 中,使用隔离来利用多个CPU内核来执行长时间运行或计算密集型任务。
隔离线程是独立的执行线程,不与主执行内存堆共享任何内存。这是与 Task.Run() 的区别。这意味着您不能从主线程访问变量,也不能通过调用 setState() 更新UI。
下面的示例以简单的方式展示了如何将数据共享回主线程以更新UI。
loadData() async {
ReceivePort receivePort = new ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
SendPort sendPort = await receivePort.first;
List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");
setState(() {
widgets = msg;
});
}
// The entry point for the isolate.
static dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
ReceivePort port = new ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (var msg in port) {
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(json.decode(response.body));
}
}
Future sendReceive(SendPort port, msg) {
ReceivePort response = new ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
在这里,dataLoader() 是在它自己单独的执行线程中运行的隔离。在隔离中,您可以执行更多的CPU密集型处理(例如,解析大型JSON),或者执行计算密集型数学,如加密或信号处理。
你可以运行下面这个完整的例子:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:isolate';
void main() {
runApp(new SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => new _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
showLoadingDialog() {
if (widgets.length == 0) {
return true;
}
return false;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getListView();
}
}
getProgressDialog() {
return new Center(child: new CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: getBody());
}
ListView getListView() => new ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});
Widget getRow(int i) {
return new Padding(padding: new EdgeInsets.all(10.0), child: new Text("Row ${widgets[i]["title"]}"));
}
loadData() async {
ReceivePort receivePort = new ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
SendPort sendPort = await receivePort.first;
List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");
setState(() {
widgets = msg;
});
}
// the entry point for the isolate
static dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
ReceivePort port = new ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (var msg in port) {
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(json.decode(response.body));
}
}
Future sendReceive(SendPort port, msg) {
ReceivePort response = new ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
}
如何发送一个网络请求?
在 Xamarin.Forms 中,你可以使用 HttpClient。当您使用流行的 http package 包时,在 Flutter 中进行网络调用就很容易了。这将抽象出许多您通常可能自己实现的网络,从而使网络调用变简化。
要使用 http 包,请将它添加到 pubspec.yaml 文件中的依赖项中:
dependencies:
...
http: ^0.11.3+16
To make a network request, call await on the async function http.get():
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
如何显示长时间运行的任务的进度?
在 Xamarin.Forms 中常会创建一个加载指示器,可以直接在XAML中创建,也可以通过第三方插件创建,比如 AcrDialogs。
在 Flutter 中,使用一个 加载指示器( ProgressIndicator)widget。通过一个布尔标志控制何时渲染来以编程方式显示进度。告诉 Flutter 在长时间运行的任务开始之前更新它的状态,并在任务结束后隐藏它。
在下面的示例中,build 函数被分成三个不同的函数。如果 showLoadingDialog() 是 true (即当widgets.length == 0时)就会渲染出 进度指示器。另一方面,用网络调用返回的数据渲染 列表视图(ListView)。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(new SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => new _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
showLoadingDialog() {
return widgets.length == 0;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getListView();
}
}
getProgressDialog() {
return new Center(child: new CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: getBody());
}
ListView getListView() => new ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});
Widget getRow(int i) {
return new Padding(padding: new EdgeInsets.all(10.0), child: new Text("Row ${widgets[i]["title"]}"));
}
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
