Flutter训练营(六)-异步编程

这是我参与更文挑战的第13天,活动详情查看: 更文挑战

前言

我们所熟悉的前端开发框架大都是事件驱动的。事件驱动意味着你的程序中必然存在事件循环和事件队列。事件循环会不停的从事件队列中获取和处理各种事件。也就是说你的程序必然是支持异步的。
在Android中这样的结构是Looper/Handler;在iOS中是RunLoop;在JavaScript中是Event Loop。
Dart 有一个单线程执行模型,支持 Isolate(一种在另一个线程上运行Dart代码的法),一个事件循环和异步编程。除非你自己创建一个 Isolate ,否则你的 Dart 代码永远运行在主UI 线程,并由 event loop 驱动。Flutter 的 event loop 和 iOS 中的 main loop 相似:Looper 是附加在主线程上的。

一、Flutter异步代码

Dart有一个单线程执行模型,支持Isolate(一种在另一个线程上运行Dart代码的方法),一个事件循环和异步编程。除非你自己创建一个 Isolate ,否则你的 Dart 代码永远运行在主UI 线程,并由 event loop 驱动。Flutter 的 event loop 和 iOS 中的 main loop 相似:Looper 是附加在主线程上的。
Dart的事件循环如下图所示。和JavaScript的基本一样。循环中有两个队列。一个是微任务队列(MicroTask queue),一个是事件队列(Event queue)。

事件队列包含外部事件,例如I/O, Timer,绘制事件等等。

微任务队列则包含有Dart内部的微任务,主要是通过scheduleMicrotask来调度。

image.png

1.1 异步执行

那么在Dart中如何让你的代码异步执行呢?很简单,把要异步执行的代码放在微任务队列或者事件队列里就行了。
可以调用scheduleMicrotask来让代码以微任务的方式异步执行。

    scheduleMicrotask((){
        print('a microtask');
    });
复制代码

可以调用Timer.run来让代码以Event的方式异步执行。

   Timer.run((){
       print('a event');
   });
复制代码

好了,现在你知道怎么让你的Dart代码异步执行了。看起来并不是很复杂,但是你需要清楚的知道你的异步代码执行的顺序。这也是很多前端面试时候会问到的问题。举个简单的例子,请问下面这段代码是否会输出”executed”?

main() {
     Timer.run(() { print("executed"); });  
      foo() {
        scheduleMicrotask(foo);  
      }
      foo();
    }
复制代码

答案是不会,因为在始终会有一个foo存在于微任务队列。导致Event Loop没有机会去处理事件队列。还有更复杂的一些例子会有大量的异步代码混合嵌套起来然后问你执行顺序是什么样的,这都需要按照上述Event Loop规则仔细去分析。
和JS一样,仅仅使用回调函数来做异步的话很容易陷入“回调地狱(Callback hell)”,为了避免这样的问题,JS引入了Promise。同样的, Dart引入了Future。

1.2 Dart异步

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 子树的重建,并更新相关数据。

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异步执行方式

flutter中的异步机制涉及到的关键字有Future、async、await、async、sync、Iterator、Iterable、Stream、Timer等。比较常用的为async、await、Future搭配。

2.1 Future

future里面有几个函数:

  • then:异步操作逻辑在这里写。

  • whenComplete:异步完成时的回调。

  • catchError:捕获异常或者异步出错时的回调。

在我们平时开发中我们是这样用的,首先给我们的函数后面加上async关键字,表示异步操作,然后函数返回值写成Future,然后我们可以new一个Future,逻辑前面加上一个await关键字,然后可以使用future.then等操作。下面是一个示例操作,为了方便演示,这里的返回值的null。平时开发中如果请求网络返回的是json,我们可以把泛型写成String;泛型也可能是实体类(entity/domain),不过要做json转实体类相关操作。

   // 举个例子
   // 模拟异步加载用户信息
  Future _getUserInfo() async{
    await new Future.delayed(new Duration(milliseconds: 3000));
    return "我是公众号测试用的例子";
  }

  // 加载用户信息,顺便打印时间看看顺序
  Future _loadUserInfo() async{
    print("_loadUserInfo:${new DateTime.now()}");
    print(await _getUserInfo());
    print("_loadUserInfo:${new DateTime.now()}");
  }


  @override
  void initState(){
    // 初始化状态,加载用户信息
    print("initState:${new DateTime.now()}");
    _loadUserInfo();
    print("initState:${new DateTime.now()}");

    super.initState();
  }
复制代码

一个Future表示一件“将来”会发生的事情,将来可以从Future中取到一个值。当一个方法返回一个Future的事情,发生两件事情:
这个方法将某件事情排队,返回一个未完成的Future

当这件事情完毕之后,Future的状态会变成已完成,这个时候就可以取到这件事情的返回值了。

要取到这个“返回值”,有两种方式:

  • 使用async配合await

  • 使用Future提供的api

async await 这两个关键字太熟悉不过了,几乎在每个面向对象语言都能看到它,同时它也是dart语言的特性,能让你写出看起来像是“同步”的“异步”代码,用法不变,这里不再赘述。

2.2 Flutter异步请求

由于 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 中,使用流行的 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。

四、添加请求进度指示器

在 Android 中,在后台运行耗时任务时我们通常会使用 ProgressBar。在 iOS 中,在后台运行耗时任务时我们通常会使用 UIProgressView。
而在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);
    });
  }
}
复制代码

总结

综上所述,Dart异步编程有两种方式:FutureStream
Future相当于40米大砍刀,Stream相当于一捆40米大砍刀。dart提供了关键字async(异步)和await(延迟执行),相当于普通的便捷的小匕首,而小匕首是我们平时经常用到的,async、await对于前端来说再熟悉不过了,不再赘述。

当遇到有需要延迟的运算(async)时,将其放入到延迟运算的队列(await)中去,把不需要延迟运算的部分先执行掉,最后再来处理延迟运算的部分。
在请求方法中直接 return await .. .的时候,实际上返回的是一个延迟计算的Future对象。这里注意两点:await关键字必须在async函数内部使用;调用async函数必须使用await关键字。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享