[译]Flutter for Android Developers - Async UI

写在前面

为了帮助理解本篇的内容,先简单介绍下Dart的运行机制。html

isolate

Dart是基于单线程模型的语言。在Dart中有一个很重要的概念叫isolate,它其实就是一个线程或者进程的实现,具体取决于Dart的实现。默认状况下,咱们用Dart写的应用都是运行在main isolate中的(能够对应理解为Android中的main thread)。固然咱们在必要的时候也能够经过isolate API建立新的isolate,多个isolate能够更好的利用多核CPU的特性来提升效率。可是要注意的是在Dart中isolate之间是没法直接共享内存的,不一样的isolate之间只能经过isolate API进行通讯。关于isolate更多详情能够参阅官方文档android

event loop

同Android相似的是在Dart运行环境中也是靠事件驱动的,经过event loop不停的从队列中获取消息或者事件来驱动整个应用的运行。可是不一样点在于一个Dart编写的app中通常有两个队列,一个叫作event queue,另外一个叫作microtask queue。而在Android中一般只有一个message queue。另外,因为isolate之间不能直接共享内存,因此每一个isolate内的event loop,event queue和microtask queue也是各自独享的。 为何须要两个队列呢?咱们看一张图就明白了:git

这张图以main isolate为例,描述了app运行时一个isolate中的正常运行流程。

  1. 启动app。
  2. 首先执行main方法。
  3. 在main方法执行完后,开始处理microtask queue,从中取出microtask执行,直到microtask queue为空。这里能够看到event loop在运行时是优先处理microtask queue的。
  4. 当microtask queue为空才会开始处理event queue,若是event queue不为空则从中取出一个event执行。这里要注意的是event queue并不会一直遍历完,而是一次取出一个event执行,执行完后就回到前面去从新判断microtask queue是否为空。因此这里能够看到microtask queue存在的一个重要意义是由它的运行时机决定的,当咱们想要在处理当前的event以后,而且在处理下一个event以前作一些事情,或者咱们想要在处理全部event以前作一些事情,这时候能够将这些事情放到microtask queue中。
  5. 当microtask queue和event queue都为空时,app能够正常退出。

Note: 当event loop在处理microtask queue时,会阻塞住event queue。绘制和交互等任务是做为event存放在event queue中的,因此当microtask queue中任务太多或处理时长太长,将会致使应用的绘制和交互等行为被卡住。github

关于Dart中event loop的更多详情能够参阅官方文档web

future

Future是Dart中提供的一个类,它用于封装一段在未来会被执行的代码逻辑。构造一个Future就会向event queue中添加一条记录。若是把event queue类比Android中的message queue的话,那么能够简单的把Future类比为Android中的Message。只不过Future中包含了须要完成的整个操做。而且利用Future的then和whenComplete方法能够指定在完成Future包含的操做后立马执行另外一段逻辑。 关于Future的更多详情能够参阅官方文档json

async and await

在Android中咱们能够利用Java API本身来管理线程,经过建立新的线程完成异步的操做。 在Flutter中,虽然Dart是基于单线程模型的,可是这并不意味着咱们无法完成异步操做。在Dart中咱们能够经过async关键字来声明一个异步方法,异步方法会在调用后当即返回给调用者一个Future对象(但这个逻辑存在一些漏洞,在Dart2中有一些改变,详见synchronous async start discussion),而异步方法的方法体将会在后续被执行(应该也是经过协程的方式实现)。在异步方法中可使用await表达式挂起该异步方法中的某些步骤从而实现等待某步骤完成的目的,await表达式的表达式部分一般是一个Future类型,即在await处挂起后交出代码的执行权限直到该Future完成。在Future完成后将包含在Future内部的数据类型做为整个await表达式的返回值,接着异步方法继续从await表达式挂起点后继续执行。 在后面开始介绍Async UI in Flutter时会看到不少使用async和await的例子。api

Note:数组

  1. async修饰的异步方法须要声明返回一个Future类型,若是方法体内没有主动的返回一个Future类型,系统会将返回值包含到一个Future中返回。
  2. await表达式的表达式部分须要返回一个Future对象。
  3. await表达式须要在一个async修饰的方法中使用才会生效。 关于async和await的更多详情能够参阅官方文档

在Flutter中runOnUiThread等价于什么

  • in Android网络

    • 基于Java,线程的管理彻底由开发者决定,咱们没法在非main thread更新UI,因此能够经过在非main thread中利用Activity.runOnUiThread方法向main thread的message queue中post一个更新界面的消息实现界面刷新。
  • in Flutterapp

    • Dart是基于单线程模型的,因此除非咱们主动建立一个isolate,不然咱们的Dart代码都是运行在main isolate(类比Android的main thread)而且由event loop来驱动的。

经过协程实现的异步调用其实也是运行在main isolate的,因此其实在Flutter中并不须要runOnUiThread相似方法的存在,咱们下面看一个例子,咱们能够直接在main isolate执行网络请求而不卡住界面和交互:

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = JSON.decode(response.body);
  });
}
复制代码

这里首先将loadData方法声明为异步方法,而后用await表达式在http.get(dataURL)处挂起等待,http是Dart提供的一个网络请求库。在请求完成时会返回一个Future<http.Response>对象,因此await表达式的表达式部分返回的是一个Future<http.Response>类型,整个await表达式返回的就是一个http.Response类型。接下来就如FFAD-Views中说的那样,经过setState改变一个StatefulWidget的State来触发系统从新调用其build方法更新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);
    });
  }
}
复制代码

界面展现的部分与FFAD-Views中介绍的StatefulWidget的展现没有太大区别,咱们声明的loadData异步方法在_SampleAppPageState的initState方法中调用,因而触发异步加载数据,await表达式挂起整个异步操做,直到http.get(dataURL)返回时经过setState更新widgets成员变量,进而触发build方法从新调用以更新ListView中的item。

经过协程实现的异步方法一般可以帮助咱们在main isolate去执行一些耗时操做而且不会阻塞界面更新。可是有时候咱们须要处理大量的数据,就算咱们将该操做声明为异步方法依然可能会致使阻塞界面更新,由于经过协程来实现的异步方法说到底仍是运行于一个线程之上,在一个线程上去调度执行毕竟算力有限。

这时候咱们能够利用多核CPU的优点去完成这些耗时的或CPU密集型的操做。这正是经过前面介绍的isolate来实现。下面的例子展现了如何建立一个isolate,而且如何在建立的isolate和main isolate之间通讯来将数据传递回main isolate进而更新界面:

loadData() async {
    ReceivePort receivePort = new ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends it's 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;
  }
复制代码

简单解释一下,这段代码主要声明了三个方法。

loadData被声明为一个异步方法,其内部的代码运行于main isolate中。该方法首先声明了一个用于main isolate从其余isolate接受消息的ReceivePort。接着经过spawn命名构造方法生成了一个isolate,为了后续描述简单这里姑且叫它x isolate。该isolate将会以构造时传入的第一个参数dataLoader方法做为运行的入口函数。即生成x isolate后,在x isolate中会开始执行dataLoader方法。构造x isolate时传入的第二个参数是经过main isolate中的ReceivePort得到的一个SendPort,这个SendPort会在dataLoader被执行时传递给它。在x isolate中能够用该SendPort向main isolate发送消息进行通讯。 接下来经过receivePort.first获取x isolate发送过来的消息,这里获取到的实际上是一个x isolate的SendPort对象,在main isolate中能够利用这个SendPort对象向x isolate中发送消息。 接下来调用sendReceive方法并传入刚刚得到的x isolate的SendPort对象和一个字符串做为参数。 最后调用setState方法触发界面更新。

dataLoader也被声明为一个异步方法,其内部的代码运行于x isolate中。在构建了x isolate后该方法开始在x isolate中执行,要注意的是dataLoader方法的参数是一个SendPort类型的对象,这正是前面构造x isolate时传入的第二个参数,也就是说,前面经过Isolate.spawn命名构造方法构造一个isolate时,传入的第二个参数的用途就是将其传递给第一个参数所表示的入口函数。在这里该参数表示的是main isolate对应的SendPort,经过它就能够在x isolate中向main isolate发送消息。 在dataLoader方法中首先生成了一个x isolate的ReceivePort对象,而后就用main isolate对应的SendPort向main isolate发送了一个消息,该消息其实就是x isolate对应的SendPort对象,因此回过头去看loadData方法中经过receivePort.first获取到的一个SendPort就是这里发送出去的。在main isolate中接收到这个SendPort后,就能够利用该SendPort向x isolate发送消息了。 接下来dataLoader方法则挂起等待x isolate的ReceivePort接受到消息。

sendReceive被声明为一个普通方法,该方法运行于main isolate中,它是在loadData中被调用的。调用sendReceive时传入的第一个参数就是在main isolate中从x isolate接收到的其对应的SendPort对象,因此在sendReceive方法中利用x isolate对应的这个SendPort对象就能够在main isolate中向x isolate发送消息。在这里发送的消息是一个数组[msg, response.sendPort]。消息发送后在dataLoader方法中await挂起的代码就会开始唤醒继续执行,取出传递过来的参数,因而在x isolate中开始执行网络请求的逻辑。 接着将请求结果再经过main isolate对应的SendPort传递给main isolate。因而在sendReceive方法中经过response.first获取到x isolate传递过来的网络请求结果。 最终在setState方法中使用网络请求回来的结果更新数据集触发界面更新。

完整的例子代码:

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 it's 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;
  }

}
复制代码

小结:

  1. 在Flutter中通常状况下不须要runOnUiThread,AsyncTask,IntentService等相似的概念,由于Dart是基于单线程模型的。异步方法的执行也是经过协程实现的,其实际也仍是运行于main isolate中。
  2. Dart中的代码都是运行在isolate中的,各个isolate之间的内存是无法直接共享的。可是能够经过ReceivePort和SendPort来实现isolate之间的通讯。每一个isolate都有本身对应的ReceivePort和SendPort,ReceivePort用于接受其余isolate发送过来的消息,SendPort则用于向其余isolate发送消息。关于ReceivePort和SendPort更多详情能够参阅官方文档

在Flutter中OkHttp等价于什么

  • in Android

    • 咱们有不少相似OkHttp之类的网络库使用。
  • in Flutter

    • 咱们使用http package来简单的完成一个网络请求调用。

虽然http package没有实现OkHttp已经实现的全部功能,可是它实现了不少经常使用的网络请求功能,帮助咱们更简单的完成一个网络请求调用。关于http package的更多信息能够参阅官方文档。 在使用http package以前咱们须要先在pubspec.yaml文件中配置依赖:

dependencies:
  ...
 http: '>=0.11.3+12'
复制代码

而后就能够简单的发起一个网络请求调用:

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);
    });
  }
}
复制代码

代码很简单,其实前面的分析中咱们就已经看到了http package的身影。在这里是直接调用http.get(dataURL)方法发起一个get请求,参数是一个url,该方法返回的是一个Future<http.Response>类型,因此最终整个await表达式返回的就是一个http.Response类型。一旦请求完成获取到了数据咱们就能够调用setState方法来触发系统更新界面。

小结:

在Flutter中咱们使用http package来帮助咱们更简单的实现网络请求调用。

在Flutter中怎样在一个任务正在运行时显示一个Loading Dialog

  • in Android

    • 咱们能够在运行一个耗时任务时展现一个Loading Dialog,可使用Dialog或者其余自定义的View来实现。
  • in Flutter

    • 咱们能够用一个Progress Indicator Widget来实现一个Loading Dialog。

下面咱们能够看一个例子:

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() {
    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 {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = JSON.decode(response.body);
    });
  }
}
复制代码

上面的例子其实跟FFAD-Views中介绍过的一些例子套路很像,咱们主要须要关注的是_SampleAppPageState类的build方法。这里看到构造Scaffold时的body参数传递的是一个方法getBody,这个方法内部又根据成员变量widgets的数量是否为0来判断返回的Widget具体是一个怎样的Widget。当widgets的数量为0时返回一个由Center包裹的CircularProgressIndicator Widget。不然返回一个ListView Widget。成员变量widgets同时又做为一个State在数据加载完成时经过setState方法来更新。因此咱们看到的效果就是应用刚启动时因为数据未加载完成显示CircularProgressIndicator的Loading过程,当异步函数loadData加载完成数据后经过setState触发界面更新,此时显示ListView展现的数据界面。

小结:

在Flutter中有几个内置的ProgressIndicator供咱们用来实现Loading Dialog的效果,本例中使用的是CircularProgressIndicator。结合StatefulWidget就能够实如今耗时任务执行完成前显示Loading Dialog,在耗时任务执行完成以后更新界面的效果。

英文原版传送