分享

Google跨平台技术Flutter介绍

 doink 2022-03-23

今天我们聊聊跨平台解决方案,通过此文,我们可以了解到

  • 跨平台技术的主流解决方案,对比
  • flutter的原理、优势
  • Dart语法
  • 用flutter,搭建一个基础app,涵盖各种常见操作

跨平台技术简介

“一次编写,多处运行”。

对于跨平台开发,业界一直都在努力寻找好的解决方案。时至今日,已经有很多成熟的解决方案,根据其原理,我们主要分为三类:

  • H5+原生(Cordova、Ionic)
  • JavaScript开发+原生渲染 (React Native、Weex)
  • 自绘UI+原生(Flutter)

接下来我们逐个来看看这三种类型的原理及优缺点。

H5+原生

将APP的某些页面,通过WebView (Android)或WKWebView(IOS)加载网页地址实现功能。

因为WebView实质上就是一个浏览器内核,其JavaScript依然运行在一个权限受限的沙箱中,所以对于大多数系统能力都没有访问权限,如无法访问文件系统、不能使用蓝牙等。对于H5不能实现的功能,都需要原生去做,因此我们会实现一些访问原生能力的API, 通过JavaScript 暴露给网页调用。


项目中一些原生跳转、访问原生UI信息,暴露给js
  • 优点
    接入简单
  • 缺点
    性能表现一般,在复杂的UI样式下,不流畅。如果没做离线缓存,耗费流量,页面载入速度比原生慢。

JavaScript开发+原生渲染

此类框架将UI节点映射成原生控件,并通过JS控制,其最终产品是一个“原生”的移动应用。
比如 React Native,他的控件ScrollView、Image,映射到IOS是 UIScrollView、UIImageView;安卓则是 ScrollView、ImageView。
因此,从使用感受上和用Objective-C或Java编写的原生应用相近。

React Native
FaceBook的 React Native

React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的JS框架 React 在原生移动应用平台的衍生产物,目前支持iOS和Android两个平台。RN使用Javascript语言,类似于HTML的JSX,以及CSS来开发移动应用,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域。

由于RN现在比较火热,并且Flutter也是受React启发,很多思想也都是相通的,我们有必要深入了解一下React原理。React是一个响应式的Web框架,我们先了解一下两个重要的概念:Dom树响应式编程

DOM树

Html的DOM树

文档对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展标志语言的标准编程接口,一种独立于平台和语言的方式访问和修改一个文档的内容和结构。
简单来说,DOM就是文档树,与用户界面控件树对应,在前端开发中通常指HTML对应的渲染树,但广义的DOM也可以指Android中的XML布局文件对应的控件树,而术语DOM操作就是指直接来操作渲染树(或控件树), 因此,可以看到其实DOM树和控件树是等价的概念,只不过前者用于Web开发中,而后者常用于原生开发中。


安卓控件树

响应式编程
响应式编程是与异步数据流交互的一种编程范式。
React中提出一个重要思想:状态改变则UI随之自动改变,而React框架本身就是响应用户状态改变的事件而执行重新构建用户界面的工作,这就是典型的响应式编程范式,下面我们总结一下React中响应式原理:

开发者只需关注状态转移(数据),当状态发生变化,React框架会自动根据新的状态重新构建UI。
React框架在接收到用户状态改变通知后,会根据当前渲染树,结合最新的状态改变,通过Diff算法,计算出树中变化的部分,然后只更新变化的部分(DOM操作),从而避免整棵树重构,提高性能。
值得注意的是,在第二步中,状态变化后React框架并不会立即去计算并渲染DOM树的变化部分,相反,React会在DOM的基础上建立一个抽象层,即虚拟DOM树,对数据和状态所做的任何改动,都会被自动且高效的同步到虚拟DOM,最后再批量同步到真实DOM中,而不是每次改变都去操作一下DOM。为什么不能每次改变都直接去操作DOM树?这是因为在浏览器中每一次DOM操作都有可能引起浏览器的重绘或回流

如果DOM只是外观风格发生变化,如颜色变化,会导致浏览器重绘界面。如果DOM树的结构发生变化,如尺寸、布局、节点隐藏等导致,浏览器就需要回流(及重新排版布局)。而浏览器的重绘和回流都是比较昂贵的操作,如果每一次改变都直接对DOM进行操作,这会带来性能问题,而批量操作只会触发一次DOM更新。

上文已经提到React Native 是React 在原生移动应用平台的衍生产物,那两者主要的区别是什么呢?其实,主要的区别在于虚拟DOM映射的对象是什么?React中虚拟DOM最终会映射为浏览器DOM树,而RN中虚拟DOM会通过 JavaScriptCore 映射为原生控件树。

JavaScriptCore 是一个JavaScript解释器,它在React Native中主要有两个作用:

  • 为JavaScript提供运行环境。
  • 是JavaScript与原生应用之间通信的桥梁,作用和JsBridge一样,事实上,在iOS中,很多JsBridge的实现都是基于 JavaScriptCore 。

而RN中将虚拟DOM映射为原生控件的过程中分两步:

  • 布局消息传递; 将虚拟DOM布局信息传递给原生;
  • 原生根据布局信息通过对应的原生控件渲染控件树;

至此,React Native 便实现了跨平台。 相对于混合应用,由于React Native是原生控件渲染,所以性能会比混合应用中H5好很多,同时React Native是Web开发技术栈,也只需维护一份代码,同样是跨平台框架。

Weex
阿里巴巴的WEEX

Weex 致力于使开发者能基于通用跨平台的 Web 开发语言和开发经验,来构建 Android、iOS 和 Web 应用。简单来说,在集成了 WeexSDK 之后,你可以使用 JavaScript 语言和前端开发经验来开发移动应用。

Weex 渲染引擎与 DSL 语法层是分开的,Weex 并不强依赖任何特定的前端框架。目前 Vue.jsRax 这两个前端框架被广泛应用于 Weex 页面开发,同时 Weex 也对这两个前端框架提供了最完善的支持。Weex 的另一个主要目标是跟进流行的 Web 开发技术并将其和原生开发的技术结合,实现开发效率和运行性能的高度统一。在开发阶段,一个 Weex 页面就像开发普通网页一样;在运行时,Weex 页面又充分利用了各种操作系统的原生组件和能力。

JavaScript开发+原生渲染的方式优缺点如下:
优点:

  • 采用Web开发技术栈,社区庞大、上手快、开发成本相对较低。
  • 原生渲染,性能相比H5提高很多。

不足:

  • 渲染时需要JavaScript和原生之间通信,在有些场景如拖动可能会因为通信频繁导致卡顿。
  • 由于渲染依赖原生控件,不同平台的控件需要单独维护,并且当系统更新时,社区控件可能会滞后;除此之外,其控件系统也会受到原生UI系统限制。
  • JavaScript为脚本语言,执行时需要JIT,执行效率和AOT代码有差距。

自绘UI+原生

我们看看最后一种跨平台技术:自绘UI+原生。这种技术的思路是,通过在不同平台实现一个统一接口的渲染引擎来绘制UI,而不依赖系统原生控件,所以可以做到不同平台UI的一致性。因为技术难度比较大,目前成熟且流行的代表作就是Google出品的Flutter,也是我们今天所要介绍的。

Flutter是什么

Flutter 是 Google推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。
开发者通过 Dart语言开发 App,一套代码可以同时运行在 iOS 和 Android平台。 Flutter提供了丰富的组件、接口,开发者可以很快地为 Flutter添加 native扩展。同时 Flutter还使用 Native引擎渲染视图,这无疑能为用户提供良好的体验。

flutter正逐步被众多厂家采用

这种平台技术的优点如下:

  • 性能高;由于自绘引擎是直接调用系统API来绘制UI,所以性能和原生控件不相上下。
  • 灵活、组件库易维护、UI外观保真度和一致性高;由于UI渲染不依赖原生控件,也就不需要根据不同平台的控件单独维护一套组件库,所以代码容易维护。由于组件库是同一套代码、同一个渲染引擎,所以在不同平台,组件显示外观可以做到高保真和高一致性;另外,由于不依赖原生控件,也就不会受原生布局系统的限制,这样布局系统会非常灵活。

不足:

  • 涉及到硬件层功能(定位、缓存文件位置等)的访问,仍然需要用原生代码实现接入,期待第三方库发展成熟。
  • 热更新能力,相比 H5、React有所欠缺 ,《闲鱼动态下发解决方案》

为什么选flutter

不仅仅是跨平台的UI

除了一套丰富的跨平台UI解决方案,Dart给我们提供了网络访问、文件读写功能。
同时Flutter支持第三方包的引用,并提供了一个官方平台供社区使用。可以想象,等待Flutter流行后,会有更多优秀的库出现在此平台上供开发者使用。

跨平台自绘引擎

Flutter与用于构建移动应用程序的其它大多数框架不同,因为Flutter既不使用WebView,也不使用操作系统的原生控件。 相反,Flutter使用自己的高性能渲染引擎来绘制widget。这样不仅可以保证在Android和iOS上UI的一致性,而且也可以避免对原生控件依赖而带来的限制及高昂的维护成本。

Flutter使用Skia作为其2D渲染引擎,Skia是Google的一个2D图形处理函数库,包含字型、坐标转换,以及点阵图都有高效能且简洁的表现,Skia是跨平台的,并提供了非常友好的API,目前Google Chrome浏览器和Android均采用Skia作为其绘图引擎,值得一提的是,由于Android系统已经内置了Skia,所以Flutter在打包APK(Android应用安装包)时,不需要再将Skia打入APK中,但iOS系统并未内置Skia,所以构建iPA时,也必须将Skia一起打包,这也是为什么Flutter APP的Android安装包比iOS安装包小的主要原因。

高性能

Flutter高性能主要靠两点来保证:
首先,Flutter APP采用Dart语言开发。Dart在 JIT(即时编译)模式下,速度与 JavaScript基本持平。但是 Dart支持 AOT,当以 AOT模式运行时,JavaScript便追不上了。速度的提升对高帧率下的视图数据计算很有帮助。
其次,Flutter使用自己的渲染引擎来绘制UI,布局数据等由Dart语言直接控制,所以在布局过程中不需要像RN那样要在JavaScript和Native之间通信,这在一些滑动和拖动的场景下具有明显优势,因为在滑动和拖动过程往往都会引起布局发生变化,所以JavaScript需要和Native之间不停的同步布局信息,这和在浏览器中要JavaScript频繁操作DOM所带来的问题是相同的,都会带来比较可观的性能开销。

采用Dart语言开发
Dart语言

这是一个很有意思,但也很有争议的问题,在了解Flutter为什么选择了 Dart而不是 JavaScript之前我们先来介绍两个概念:JIT和AOT。

目前,程序主要有两种运行方式:静态编译与动态解释。
静态编译的程序在执行前全部被翻译为机器码,通常将这种类型称为AOT (Ahead of time)即 “提前编译”;而解释执行的则是一句一句边翻译边运行,通常将这种类型称为JIT(Just-in-time)即“即时编译”。

AOT程序的典型代表是用C/C++开发的应用,它们必须在执行前编译成机器码,而JIT的代表则非常多,如JavaScript、python等,事实上,所有脚本语言都支持JIT模式。但需要注意的是JIT和AOT指的是程序运行方式,和编程语言并非强关联的,有些语言既可以以JIT方式运行也可以以AOT方式运行,如Java、Python,它们可以在第一次执行时编译成中间字节码、然后在之后执行时可以直接执行字节码。

现在我们看看Flutter为什么选择Dart语言?笔者根据官方解释以及自己对Flutter的理解总结了以下几条(由于其它跨平台框架都将JavaScript作为其开发语言,所以主要将Dart和JavaScript做一个对比):

  • 开发效率高

Dart运行时和编译器支持Flutter的两个关键特性的组合:

基于JIT的快速开发周期:Flutter在开发阶段采用,采用JIT模式,这样就避免了每次改动都要进行编译,极大的节省了开发时间;

基于AOT的发布包: Flutter在发布时可以通过AOT生成高效的ARM代码以保证应用性能。而JavaScript则不具有这个能力。

  • 高性能

Flutter旨在提供流畅、高保真的的UI体验。为了实现这一点,Flutter中需要能够在每个动画帧中运行大量的代码。这意味着需要一种既能提供高性能的语言,而不会出现会丢帧的周期性暂停,而Dart支持AOT,在这一点上可以做的比JavaScript更好。

  • 快速内存分配

Flutter框架使用函数式流,这使得它在很大程度上依赖于底层的内存分配器。因此,拥有一个能够有效地处理琐碎任务的内存分配器将显得十分重要,在缺乏此功能的语言中,Flutter将无法有效地工作。当然Chrome V8的JavaScript引擎在内存分配上也已经做的很好,事实上Dart开发团队的很多成员都是来自Chrome团队的,所以在内存分配上Dart并不能作为超越JavaScript的优势,而对于Flutter来说,它需要这样的特性,而Dart也正好满足而已。

  • 类型安全

由于Dart是类型安全的语言,支持静态类型检测,所以可以在编译前发现一些类型的错误,并排除潜在问题,这一点对于前端开发者来说可能会更具有吸引力。与之不同的,JavaScript是一个弱类型语言,也因此前端社区出现了很多给JavaScript代码添加静态类型检测的扩展语言和工具,如:微软的TypeScript以及Facebook的Flow。相比之下,Dart本身就支持静态类型,这是它的一个重要优势。

  • Dart团队就在你身边

Dart、Flutter都隶属于Google,因此Dart团队的积极投入,Flutter团队可以获得更多、更方便的支持,正如Flutter官网所述“我们正与Dart社区进行密切合作,以改进Dart在Flutter中的使用。例如,当我们最初采用Dart时,该语言并没有提供生成原生二进制文件的工具链(这对于实现可预测的高性能具有很大的帮助),但是现在它实现了,因为Dart团队专门为Flutter构建了它。同样,Dart VM之前已经针对吞吐量进行了优化,但团队现在正在优化VM的延迟时间,这对于Flutter的工作负载更为重要。”

在Dart2.0版本有个特性:可选的 new 和 const ,即在声明对象时,关键字 new 和 const 不再是必须的。此特性将极大方便嵌套新建对象代码的书写,如: Flutter 中的层层嵌套的组件

Flutter框架结构

我们先了解下Flutter的结构,由引擎层(c++)和框架层(Dart)构成。


framework.png
  • Flutter Framework

这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上,我们来简单介绍一下:

底下两层(Foundation和Animation、Painting、Gestures)在Google的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。

Rendering层,这一层是一个抽象的布局层,它依赖于dart UI层,Rendering层会构建一个UI树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM。Rendering层可以说是Flutter UI框架最核心的部分,它除了确定每个UI元素的位置、大小之外还要进行坐标变换、绘制(调用底层dart:ui)。

Widgets层是Flutter提供的的一套基础组件库,在基础组件库之上,Flutter还提供了 Material 和Cupertino两种视觉风格的组件库。而我们Flutter开发的大多数场景,只是和这两层打交道。

  • Flutter Engine

这是一个纯 C++实现的 SDK,其中包括了 Skia引擎、Dart运行时、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。

Flutter实战

下面,我们来实现一个计数器项目,通过这个项目,我们可以

  • 了解Flutter项目构成
  • 了解Dart基本语法
前言

Flutter 的核心设计思想便是一切即Widget。在flutter的世界里,包括view、view controllers、layouts等在内的概念都建立在Widget之上。widget是Flutter功能的抽象描述,不同的widget组合成了完整的页面。
所以掌握Flutter的基础就是学会使用widget开始。目前Flutter的widget涵盖了下述类型

Flutter的widget

阶段一:基础功能实现

UI
flutter计数器.png
代码

为了阅读方便,直接把解释写在代码里

import 'package:flutter/material.dart';

程序入口
void main() => runApp(MyApp());


MyApp类代表Flutter应用,它继承了 StatelessWidget类,这也就意味着应用本身也是一个widget
在Flutter中,大多数东西都是widget,包括对齐(alignment)、填充(padding)和布局(layout)。
Flutter在构建页面时,会调用组件的build方法。
widget的主要工作是提供一个build()方法来描述如何构建UI界面(通常是通过组合、拼装其它基础widget)。
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {


MaterialApp 是Material库中提供的Flutter APP框架,通过它可以设置应用的名称、
主题、语言、首页及路由列表等。MaterialApp也是一个widget。
    return MaterialApp(
      //应用首页路由
      home: MyHomePage(title: "Flutter计数器"),
    );
  }
}

MyHomePage 是应用的首页,它继承自StatefulWidget类,表示它是一个有状态的widget(Stateful widget)
class MyHomePage extends StatefulWidget {
  final String title;

  MyHomePage({Key key, this.title}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _HomePageState();
}

class _HomePageState extends State<MyHomePage> {
  int _counter = 0;

  void increaseCount() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {

Scaffold 是Material库中提供的页面脚手架,它包含导航栏和Body以及FloatingActionButton(如果需要的话)。 
本书后面示例中,路由默认都是通过Scaffold创建。
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      //水平对齐
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[Text('你点击了按钮:$_counter次')],
        ),
      ),
      floatingActionButton: FloatingActionButton(
          onPressed: increaseCount, child: Icon(Icons.add)),
    );
  }
}
StatefulWidget、StatelessWidget介绍

StatefulWidget和StatelessWidget这两大类,是我们Flutter开发中经常用到的。用我直观的感受概括来说,StatefulWidget创建的Widget是界面可变的Widget,而StatelessWidget创建的Widget则为界面不可变的Widget。
(Widget可以理解为Flutter提供给我们选择使用的组件,使用Flutter开发的APP就是用一个接着一个的Widget嵌套、组装而成,有点类似与HTML语法。)
StatefulWidget在整个生命周期可以改变很多次,在StatefulWidget的Widget可以在运行的过程中变换多次进行逻辑交互,以传达作者想要展示的信息。例如改变文字、改变颜色、改变大小、改变形状、改变图片等等我们经常能够见到的变化。StatelessWidget在初始化之后就无法再改变。想要使用StatelessWidget进行逻辑交互,通过改变某些变量以改变Widget的样式是不可行的,使用前应当注意。

Dart的字符串,用单引号、双引号包裹都可以,$符号是特殊的符号后面可直接跟变量名。如果想要显示该符号,记得转义\$

阶段二:路由、本地图片资源、第三方包

这一步我们加入了一个新的页面,里面显示一个随机的英语单词、一个本地的图片


新页面.png

准备工作

1. 添加第三方库,显示单词

随机英语单词,我们在第三方仓库Dart Packages里,找到一个库
^3.1.5 表示:大于等于3.1.5最新的版本

image.png

把库、版本添加到根目录的pubspec.yaml文件,同时点击右上角的Packages get,这会将依赖包安装到我们的项目
image.png

完成后,我们使用import 'package:english_words/english_words.dart';引入此包
在根目录可以看到包的版本、源码

image.png
2. 显示本地图片

在根目录新建imgs的文件夹,插入一张图片

项目结构,lib库就是我们的flutter开发库

在根目录pubspec.yaml里,声明该图片

image.png

代码

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';

//新页面
class NewRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('路由页面')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RandomWordsWidget(),
            Image(
              image: AssetImage('imgs/icon_no_one.png'),
            ),
            Image(
              image: NetworkImage(
                  "https://avatars2./u/20411648?s=460&v=4"),
              width: 100,
              height: 100,
            )
          ],
        ),
      ),
    );
  }
}

//随机文字
class RandomWordsWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final wordPair = WordPair.random();
    return Padding(
      padding: EdgeInsets.all(8.0),
      child: Text(
        wordPair.toString(),
        style: TextStyle(fontSize: 30, color: Colors.green),
      ),
    );
  }
}

接入新页面

改造后的主页如下

import 'package:flutter/material.dart';
import 'package:flutter_app/NewRoute.dart'; //**1**

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //应用首页路由
      home: MyHomePage(title: "Flutter计数器"),
      routes: {"DemoNewPage": (context) => NewRoute()}, //**2**
    );
  }
}

//主页入口
class MyHomePage extends StatefulWidget {
  final String title;

  MyHomePage({Key key, this.title}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _HomePageState();
}

//主页状态渲染
class _HomePageState extends State<MyHomePage> {
  int _counter = 0;

  void increaseCount() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      //水平对齐
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('你点击了按钮:$_counter次'),
            FlatButton( 
              child: Text('点击前往新页面'),
              textColor: Colors.blue,
              onPressed: () {
                Navigator.pushNamed(context, "DemoNewPage");//**3**
              },
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
          onPressed: increaseCount, child: Icon(Icons.add)),
    );
  }
}

打开新的页面有两种方式

  • 自定义打开,优势可以传递参数
  Navigator.push( context,
           new MaterialPageRoute(builder: (context) {
                  return new NewRoute();
             }));
  • 路由方式,优势简单易维护,参考项目中的写法,代码中
    1处 引入 路由页面
    2处 声明路由
    3处 执行路由跳转

阶段三:文件访问&异步操作、网络访问

文件访问&异步操作

Dart的IO库包含了文件读写的相关类,它属于Dart语法标准的一部分,所以通过Dart IO库,无论是Dart VM下的脚本还是Flutter,都是通过Dart IO库来操作文件的,不过和Dart VM相比,Flutter有一个重要差异是文件系统路径不同,这是因为Dart VM是运行在PC或服务器操作系统下,而Flutter是运行在移动操作系统中,他们的文件系统会有一些差异。
Android和iOS的应用存储目录不同,第三方库PathProvider 提供了一种平台透明的方式来访问设备文件系统上的常用位置。该类当前支持访问两个文件系统位置:

  • 临时目录: 可以使用 getTemporaryDirectory() 来获取临时目录; 系统可随时清除的临时目录(缓存)。在iOS上,这对应于NSTemporaryDirectory() 返回的值。在Android上,这是getCacheDir()返回的值。
  • 文档目录: 可以使用getApplicationDocumentsDirectory()来获取应用程序的文档目录,该目录用于存储只有自己可以访问的文件。只有当应用程序被卸载时,系统才会清除该目录。在iOS上,这对应于NSDocumentDirectory。在Android上,这是AppData目录。

代码

我们在主页里的_HomePageState组件里添加下述的代码,使其能记住上次按钮点击数量。

 Future<File> _getLocalFile() async {
    String dir = (await getApplicationDocumentsDirectory()).path;
    return File('$dir/count.txt');
  }

  Future<int> _readCount() async {
    File file = await _getLocalFile();
    String count = await file.readAsString();

    if (count.isEmpty) return 0;

    return int.parse(count);
  }

  Future<Null> _saveCount(int count) async {
    File file = await _getLocalFile();
    file.writeAsString(count.toString());
  }

  int _counter = 0;

  void increaseCount() {
    setState(() {
      _counter++;
    });
    _saveCount(_counter);
  }

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

  void refreshCount() async {
    int count = await _readCount();
    setState(() => _counter = count);
  }

我们注意到,文件读写是耗时的异步操作,但是我们代码写的和同步操作一样,避免了很多回调嵌套。

打个比方,读取文件数据并显示在UI上,我们异步的写法是

    _readCount().then((int count) {
      setState(() {
        _counter = count;
      });
    });

在Dart里,我们可以这样写

  void refreshCount() async {
    int count = await _readCount();
    setState(() => _counter = count);
  }

Dart类库有非常多的返回Future或者Stream对象的函数。 这些函数被称为异步函数:它们只会在设置好一些耗时操作之后返回,比如像 IO操作。而不是等到这个操作完成。
async和await关键词支持了异步编程,你可以用同步风格的写法,创作异步代码。

我们介绍下Dart异步编程中常用的关键字

Future

Future与JavaScript中的Promise非常相似,表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future只会对应一个结果,要么成功,要么失败。

由于本身功能较多,这里我们只介绍其常用的API及特性。还有,请记住,Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用。

Future.then

为了方便示例,在本例中我们使用Future.delayed 创建了一个延时任务(实际场景会是一个真正的耗时任务,比如一次网络请求),即2秒后返回结果字符串"hi world!",然后我们在then中接收异步结果并打印结果,代码如下:

Future.delayed(new Duration(seconds: 2),(){
   return "hi world!";
}).then((data){
   print(data);
});
Async/await

回调地狱(Callback hell)

如果代码中有大量异步逻辑,并且出现大量异步任务依赖其它异步任务的结果时,必然会出现Future.then回调中套回调情况。举个例子,比如现在有个需求场景是用户先登录,登录成功后会获得用户Id,然后通过用户Id,再去请求用户个人信息,获取到用户个人信息后,为了使用方便,我们需要将其缓存在本地文件系统,代码如下:

//先分别定义各个异步任务
Future<String> login(String userName, String pwd){
    ...
    //用户登录
};
Future<String> getUserInfo(String id){
    ...
    //获取用户信息 
};
Future saveUserInfo(String userInfo){
    ...
    // 保存用户信息 
};

基于有三个彼此有关联的异步任务,因此,回调会嵌套三层

login("1000110002","xl123456").then((id){
 //登录成功后通过,id获取用户信息    
 getUserInfo(id).then((userInfo){
    //获取用户信息后保存 
    saveUserInfo(userInfo).then((){
       //保存用户信息,接下来执行其它操作
        ...
      });
   });
})

可以感受一下,如果业务逻辑中有大量异步依赖的情况,将会出现上面这种在回调里面套回调的情况,过多的嵌套会导致的代码可读性下降以及出错率提高,并且非常难维护,这个问题被形象的称为回调地狱(Callback hell)。
回调地狱问题在之前JavaScript中非常突出,也是JavaScript被吐槽最多的点,但随着ECMAScript6和ECMAScript7标准发布后,这个问题得到了非常好的解决,而解决回调地狱的两大神器正是ECMAScript6引入了Promise,以及ECMAScript7中引入的async/await。
而在Dart中几乎是完全平移了JavaScript中的这两者:Future相当于Promise,而async/await连名字都没改。接下来我们看看通过Future和async/await如何消除上面示例中的嵌套问题。

使用Future消除callback hell

login("1000110002","xl123456").then((id){
      return getUserInfo(id);
}).then((userInfo){
    return saveUserInfo(userInfo);
}).then((e){
   //执行接下来的操作 
}).catchError((e){
  //错误处理  
  print(e);
});

正如上文所述, “Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用” ,如果在then中返回的是一个Future的话,该future会执行,执行结束后会触发后面的then回调,这样依次向下,就避免了层层嵌套。

使用async/await消除callback hell

通过Future回调中再返回Future的方式虽然能避免层层嵌套,但是还是有一层回调,有没有一种方式能够让我们可以像写同步代码那样来执行异步任务而不使用回调的方式?答案是肯定的,这就要使用async/await了,下面我们先直接看代码,然后再解释,代码如下:

task() async {
   try{
    String id = await login("alice","******");
    String userInfo = await getUserInfo(id);
    await saveUserInfo(userInfo);
    //执行接下来的操作   
   } catch(e){
    //错误处理   
    print(e);   
   }  
}

async用来表示函数是异步的,定义的函数会返回一个Future对象,可以使用then方法添加回调函数。
await 后面是一个Future,表示等待该异步任务完成,异步完成后才会往下走;await必须出现在 async 函数内部。
可以看到,我们通过async/await将一个异步流用同步的代码表示出来了。

其实,无论是在JavaScript还是Dart中,async/await都只是一个语法糖,编译器或解释器最终都会将其转化为一个Promise(Future)的调用链。

网络访问

Dart的IO库中提供了Http请求的一些类,我们可以直接使用HttpClient来发起请求。使用HttpClient发起请求分为五步:

  1. 创建一个HttpClient
    HttpClient httpClient = new HttpClient();
  2. 打开Http连接,设置请求头
    HttpClientRequest request = await httpClient.getUrl(uri);

这一步可以使用任意Http method,如httpClient.post(...)、httpClient.delete(...)等。如果包含Query参数,可以在构建uri时添加,如:

Uri uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
    "xx":"xx",
    "yy":"dd"
  });

通过HttpClientRequest可以设置请求header,如:
request.headers.add("user-agent", "test");
如果是post或put等可以携带请求体方法,可以通过HttpClientRequest对象发送request body,如:

String payload="...";
request.add(utf8.encode(payload)); 
//request.addStream(_inputStream); //可以直接添加输入流
  1. 等待连接服务器
    HttpClientResponse response = await request.close();
    这一步完成后,请求信息就已经发送给服务器了,返回一个HttpClientResponse对象,它包含响应头(header)和响应流(响应体的Stream),接下来就可以通过读取响应流来获取响应内容。

  2. 读取响应内容
    String responseBody = await response.transform(utf8.decoder).join();
    我们通过读取响应流来获取服务器返回的数据,在读取时我们可以设置编码格式,这里是utf8。

  3. 请求结束,关闭HttpClient
    httpClient.close();
    关闭client后,通过该client发起的所有请求都会中止。

网络请求例子

我们演示一个网络请求例子,访问云教学登录页,把http返回值呈现在UI上。


访问网页,获取接口返回值

代码

import 'dart:_http';
import 'dart:convert';
import 'package:flutter/material.dart';

//网络请求例子
class WebViewShow extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("网络访问展示"),
      ),
      body: WebViewQueryWidget(),
    );
  }
}

class WebViewQueryWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _WebViewShowState();
}

class _WebViewShowState extends State<WebViewQueryWidget> {
  String _text = '载入中...';
  static const String DEFAULT_URL = "https://cas./cas/login";

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

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Text(_text),
      physics:BouncingScrollPhysics(),
      scrollDirection: Axis.vertical,
    );
  }

  void queryText() async {
    HttpClient httpClient = new HttpClient();
    HttpClientRequest request = await httpClient.getUrl(Uri.parse(DEFAULT_URL));
    request.headers.add("user-agent", "flutter test");
    request.headers.add("userId", "1030610065");

    //等待连接服务器(会将请求信息发送给服务器)
    HttpClientResponse response = await request.close();
    _text = await response.transform(utf8.decoder).join();
    httpClient.close();

    setState(() {});
  }
}

阶段四:列表渲染、操作

收藏页

我们实现一个列表,可以点击收藏单词。从右上角按钮跳去结果页查看


单词列表
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/CollectResultShow.dart';

//收藏列表
class ListViewShow extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _ListViewShowState();
}

class _ListViewShowState extends State<ListViewShow> {
  static const int MAX_COUNT = 100;
  final TextStyle _biggerFont = TextStyle(fontSize: 18.0);
  final Divider _divider = Divider();

  List<WordPair> _wordList = List<WordPair>();
  Set<WordPair> _likeWordList = Set<WordPair>();

  @override
  void initState() {
    _wordList = generateWordPairs().take(MAX_COUNT).toList();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('单词记忆表'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.list),
            onPressed: goDetail,
          )
        ],
      ),
      body: _buildList(),
    );
  }

  void goDetail() {
    Navigator.push(
        context,
        new MaterialPageRoute(
            builder: (context) => new CollectResultShow(word: _likeWordList)));
  }

  //创建单词列表
  Widget _buildList() {
    //创建一个带分隔符的列表控件
    return ListView.separated(
      itemCount: MAX_COUNT,
      padding: EdgeInsets.all(8),
      itemBuilder: (context, i) {
        return _buildItem(_wordList[i], i);
      },
      separatorBuilder: (context, i) => _divider,
    );
  }

  //创建列表元素
  Widget _buildItem(WordPair word, int pos) {
    final userLike = _likeWordList.contains(word);
    int showPos = pos + 1;

    return ListTile(
      title: Text(
        word.asCamelCase,
        style: _biggerFont,
      ),
      leading: Text('$showPos:'),
      trailing: Icon(
        userLike ? Icons.favorite : Icons.favorite_border,
        color: userLike ? Colors.red : null,
      ),
      onTap: () => setState(() {
        //更新收藏数据,通知列表更新
            if (userLike) {
              _likeWordList.remove(word);
            } else {
              _likeWordList.add(word);
            }
          }),
    );
  }
}

收藏结果页
单词收藏结果页
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';

class CollectResultShow extends StatelessWidget {
  final Set<WordPair> word;

  CollectResultShow({Key key, @required this.word}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('我收藏的单词'),
        ),
        body: Padding(
          padding: EdgeInsets.all(10),
          child: Text(
            translateTxt(),
            style: TextStyle(color: Colors.black87,fontSize: 18, height: 1.5),
          ),
        ));
  }

  String translateTxt() {
    if (word == null || word.length == 0) {
      return '暂无收藏';
    }

    StringBuffer stringBuffer = StringBuffer();

天然支持lambada表达式
    word.forEach((word) {
      stringBuffer.writeln(word);
    });
    return stringBuffer.toString();
  }
}

总结

通过上述学习,我们已经掌握了Flutter开发一些基本操作。Flutter目前没有布局文件,UI代码都是手写在dart文件中,因此结构比较复杂的布局嵌套会比较深,初学者需要适应下。未来期待会有UI设计器来提升开发效率,并且分离UI和后台代码,易维护。
dart语法用熟悉了,其实还是挺顺手的。

外环境:业界阿里巴巴、美团、马蜂窝等产品都已陆续引入Flutter改造非核心项目。说明业界对于Flutter的跨平台思路比较认可,愿意尝试落地;另一方面来说Flutter从发布到现在1.2.1版本,还处在发展阶段,对于Flutter逐步接入大型项目的解决方案,支持的不是很完美,阿里巴巴的解决方案成本较大,个人开发者可以保持学习观望。

本文内容基于《Flutter实战》,并结合自己学习的感悟。感谢这些作者:
《Flutter实战》Flutter社区Dart语法了解Flutter第三方插件RTC Dev Meetup闲鱼Flutter技术博客

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多