[譯]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,在耗時任務執行完成以後更新界面的效果。

英文原版傳送

相關文章
相關標籤/搜索