「快速上手Flutter開發系列教程」之線程和異步UI開發指南

在這篇文章中,將向你們分享在Flutter中:html

  • 怎麼編寫異步的代碼?
  • 怎麼把工做放到後臺線程執行?
  • 如何進行網絡請求?
  • 如何爲長時間運行的任務添加一個進度指示器?

這些Flutter開發的實用技能。數據庫

怎麼編寫異步的代碼?

Dart有一個單線程執行模型,支持Isolate(一種在另外一個線程上運行Dart代碼的方法),一個事件循環和異步編程。除非你本身建立一個 Isolate ,不然你的 Dart 代碼永遠運行在主UI 線程,並由 event loop 驅動。Flutter 的 event loop 和 iOS 中的 main loop 類似:Looper 是附加在主線程上的。編程

Dart 的單線程模型,並不意味着你寫的代碼必定要做爲阻塞操做的方式運行,從而卡住 UI。相反,可使用 Dart 語言提供的異步工具,例如 async / await,來實現異步操做。json

舉個例子,你可使用 async / await來讓 Dart 幫你作一些繁重的工做,編寫網絡請求代碼而不會掛起 UI:安全

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}
複製代碼

上面作法等價於Android中的runOnUiThread。 以上代碼片斷的完整部分能夠在課程源碼中查找。bash

一旦 await 的網絡請求完成,經過調用 setState() 來更新 UI,這會觸發 widget 子樹的重建,並更新相關數據。網絡

下面的例子展現了異步加載數據,並用 ListView 展現出來:app

loadData-async

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();

    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
          itemCount: widgets.length,
          itemBuilder: (BuildContext context, int position) {
            return getRow(position);
          }));
  }

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: 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 是單線程而且跑着一個 event loop(就像 Node.js),所以你沒必要擔憂線程管理或生成後臺線程。若是你正在作 I/O 操做,如訪問磁盤或網絡請求,能夠安全地使用 async / await來完成。若是你須要作讓 CPU 執行繁忙的計算密集型任務,你須要使用 Isolate 來避免阻塞 event loop。less

在Android中,當你想訪問一個網絡資源時,你一般會建立一個AsyncTask,當你須要一個耗時的後臺任務時,你一般須要IntentService,在Flutter中則不須要這麼繁瑣。異步

對於 I/O 操做,經過關鍵字 async把方法聲明爲異步方法,而後經過await關鍵字等待該異步方法執行完成:

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}
複製代碼

在Android中,當你繼承AsyncTask時,一般會覆蓋3個方法,OnPreExecute、doInBackground和onPostExecute。 在Flutter中沒有這種模式的等價物,由於你只需await函數執行完成,而Dart的事件循環將負責其他的事情。

以上就是對諸如網絡請求、數據庫訪問等,I/O 操做的典型作法。

然而,有時候你須要處理大量的數據,這會致使你的 UI 掛起。在 Flutter 中,使用 Isolate 來發揮多核心 CPU 的優點來處理那些長期運行或是計算密集型的任務。

Isolate 是分離的運行線程,而且不和主線程的內存堆共享內存。這意味着你不能訪問主線程中的變量,或者使用 setState() 來更新 UI。正如它們的名字同樣,Isolate 不能共享內存。

下面的例子展現了一個簡單的Isolate是如何把數據返回給主線程來更新 UI 的:

import 'dart:isolate';
...
loadData() async {
    // 打開ReceivePort以接收傳入的消息
    ReceivePort receivePort = ReceivePort();
    //建立並生成與當前Isolate共享相同代碼的Isolate
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // 流的第一個元素
    SendPort sendPort = await receivePort.first;
    // 流的第一個元素被收到後監聽會關閉,因此須要新打開一個ReceivePort以接收傳入的消息
    ReceivePort response = ReceivePort();
    //經過此發送端口向其對應的「ReceivePort」①發送異步[消息],這個「消息」指的是發送的參數②。
    sendPort.send(
        ["https://jsonplaceholder.typicode.com/posts", response.sendPort]);
    // 獲取端口發送來的數據③
    List msg = await response.first;

    setState(() {
      widgets = msg;
    });
  }

  // isolate的入口函數,該函數會在新的Isolate中調用,Isolate.spawn的message參數會做爲調用它時的惟一參數
  static dataLoader(SendPort sendPort) async {
    // 打開ReceivePort①以接收傳入的消息
    ReceivePort port = ReceivePort();

    // 通知其餘的isolates,本isolate 所監聽的端口
    sendPort.send(port.sendPort);
    // 獲取其餘端口發送的異步消息 msg② -> ["https://jsonplaceholder.typicode.com/posts", response.sendPort]
    await for (var msg in port) {
      //等價於List msg= await port.first;
      String data = msg[0];
      SendPort replyTo = msg[1];

      String dataURL = data;
      http.Response response = await http.get(dataURL);
      // 其對應的「ReceivePort」發送解析出來的JSON數據③
      replyTo.send(json.decode(response.body));
    }
  }
複製代碼

以上代碼片斷的完整部分能夠在課程源碼中查找。

這裏,dataLoader() 是一個運行於本身獨立執行線程上的 Isolate。在 Isolate 裏,你能夠執行 CPU 密集型任務(例如解析一個龐大的 json,解析json也是很耗時的哦),或是計算密集型的數學操做,如加密或信號處理等。

你能夠運行下面的完整例子:

import 'dart:convert';
import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _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 Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row ${widgets[i]["title"]}"));
  }

  loadData() async {
    // 打開ReceivePort以接收傳入的消息
    ReceivePort receivePort = ReceivePort();
    //建立並生成與當前Isolate共享相同代碼的Isolate
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // 流的第一個元素
    SendPort sendPort = await receivePort.first;
    // 流的第一個元素被收到後監聽會關閉,因此須要新打開一個ReceivePort以接收傳入的消息
    ReceivePort response = ReceivePort();
    //經過此發送端口向其對應的「ReceivePort」①發送異步[消息],這個「消息」指的是發送的參數②。
    sendPort.send(
        ["https://jsonplaceholder.typicode.com/posts", response.sendPort]);
    // 獲取端口發送來的數據③
    List msg = await response.first;

    setState(() {
      widgets = msg;
    });
  }

  // isolate的入口函數,該函數會在新的Isolate中調用,Isolate.spawn的message參數會做爲調用它時的惟一參數
  static dataLoader(SendPort sendPort) async {
    // 打開ReceivePort①以接收傳入的消息
    ReceivePort port = ReceivePort();

    // 通知其餘的isolates,本isolate 所監聽的端口
    sendPort.send(port.sendPort);
    // 獲取其餘端口發送的異步消息 msg② -> ["https://jsonplaceholder.typicode.com/posts", response.sendPort]
    await for (var msg in port) {
      //等價於List msg= await port.first;
      String data = msg[0];
      SendPort replyTo = msg[1];

      String dataURL = data;
      http.Response response = await http.get(dataURL);
      // 其對應的「ReceivePort」發送解析出來的JSON數據③
      replyTo.send(json.decode(response.body));
    }
  }
}
複製代碼

關於Flutter的更多異步編程知識,能夠學習《Flutter從入門到進階-實戰攜程網App》

如何進行網絡請求?

在 Flutter 中,使用流行的 http package 作網絡請求很是簡單。它把你可能須要本身作的網絡請求操做抽象了出來,讓發起請求變得簡單。

要使用 http 包,在 pubspec.yaml 中添加以下依賴:

dependencies:
  ...
  http: ^0.12.0+1
複製代碼

發起網絡請求,在 http.get() 這個 async 方法中使用 await

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);
    });
  }
}
複製代碼

以上代碼片斷的完整部分能夠在課程源碼中查找。

一旦得到結果後,你能夠經過調用setState來告訴Flutter更新其狀態,setState將使用網絡調用的結果更新UI。

關於網絡請求的更多內容和實戰技巧可學習《基於Http實現網絡操做》部分的課程。

如何爲長時間運行的任務添加一個進度指示器?

  • 在 iOS 中,在後臺運行耗時任務時咱們一般會使用 UIProgressView。
  • 在 Android 中,在後臺運行耗時任務時咱們一般會使用 ProgressBar。

那麼,在Flutter也有與之對應的widget叫ProgressIndicator。經過一個布爾 flag 來控制是否展現進度。在任務開始時,告訴 Flutter 更新狀態,並在結束後隱藏。

在下面的例子中,build 函數被拆分紅三個函數。若是 showLoadingDialog()true (當 widgets.length == 0 時),則渲染 ProgressIndicator。不然,當數據從網絡請求中返回時,渲染 ListView

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _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 Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(padding: EdgeInsets.all(10.0), child: 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入門基礎知識
  • Flutter主題和文字處理
  • Flutter什麼是聲明式UI
  • Flutter佈局與列表
  • Flutter手勢檢測及觸摸事件處理
  • Flutter狀態管理d
  • Flutter線程和異步UI
  • Flutter表單輸入與富文本
  • Flutter認識視圖(Views)md
  • Flutter調用硬件、第三方服務以及平臺交互、通知
  • Flutter路由與導航
  • Flutter項目結構、資源、依賴和本地化

推薦學習資料

相關文章
相關標籤/搜索