「快速上手Flutter开发系列教程」之线程和异步UI开发指南

Flutter异步编程

在这篇文章中,将向大家分享在Flutter中:

  • 怎么编写异步的代码?
  • 怎么把工作放到后台线程执行?
  • 如何进行网络请求?
  • 如何为长时间运行的任务添加一个进度指示器?

这些Flutter开发的实用技能。

怎么编写异步的代码?

Dart有一个单线程执行模型,支持Isolate(一种在另一个线程上运行Dart代码的方法),一个事件循环和异步编程。除非你自己创建一个 Isolate ,否则你的 Dart 代码永远运行在主UI 线程,并由 event loop 驱动。Flutter 的 event loop 和 iOS 中的 main loop 相似:Looper 是附加在主线程上的。

Dart 的单线程模型,并不意味着你写的代码一定要作为阻塞操作的方式运行,从而卡住 UI。相反,可以使用 Dart 语言提供的异步工具,例如 async / await,来实现异步操作。

举个例子,你可以使用 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
以上代码片段的完整部分可以在课程源码中查找。

一旦 await 的网络请求完成,通过调用 setState() 来更新 UI,这会触发 widget 子树的重建,并更新相关数据。

下面的例子展示了异步加载数据,并用 ListView 展示出来:

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。

在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项目结构、资源、依赖和本地化

推荐学习资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容