共计 12339 个字符,预计需要花费 31 分钟才能阅读完成。
本文只适合初次接触 Flutter 的开发者。原文链接: http://blog.myweb.kim/flutter/Flutter%E5%85%A5%E5%9D%91%E5%88%86%E4%BA%AB/
简介
Flutter 是 Google 推出并开源的移动端开发框架(基于「Dart」语言)。使用 Flutter 开发的 APP 可以同时运行在 IOS 与 Android 平台上。并且 Flutter 默认带有 Material 风格 与 Cupertino 风格的主题包(前者 Android,后者 IOS),可以快速开发一个 IOS 风格或者 Android 风格的…Demo…
跨平台 Flutter 不使用 WebView 也不使用操作系统的原生控件,而是自己有用一个 高性能 的渲染引擎,可以非常高效的进行组件绘制 UI 渲染。这样 Flutter 可以保证在 IOS 与 Android 上的 UI 表现一致性,开发者无需过多关注平台差异性上的问题。对于初创公司来说,前期节约开发成本就是最好的融资。。。
高性能
与 React Native (以下简称 RN)的跨平台不同的是,RN 是会将 JS 编写的对应组件转换为原生组件去渲染,而 Flutter 是基于最底层 Skia 的图形库去渲染(我觉得有点类似于 DOM 中的 canvas , 从平台上得到一个画布,自己在画布上去渲染),所有的渲染都有 Skia 来完成。
Skia 延伸 …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 的布局、渲染都是 Dart 直接控制,在一些交互中,比如滑动的时候它的高性能就会体现出来。而 RN 在这方面的渲染则是与原生平台进行通信,不断的进行信息同步,这部分的开销放到手机上还是很大的。
而且在渲染层,Flutter 底层也有一个类似虚拟 DOM 的组件,在 UI 进行变化后,会进行 diff 算法。
开发高效率 Flutter 在开发的时候有一个特点,热重载。就像在 webpack 与 浏览器,在编辑器中保存后,界面立马就能看到变化。Flutter 也是这样,当将 APP 在虚拟容器中或者真机设备中调试时,保存后,APP 会立刻响应。节省了大量时间。
Dart 初步了解
因为 Flutter 是基于 Dart 语言开发的,所以我们多多少少也要了解下 Dart 这玩意怎么写,他的语法与结构是个怎样的。虽然官网的 Demo 有提到说:「如果您熟悉面向对象和基本编程概念(如变量、循环和条件控制),则可以完成本教程,您无需要了解 Dart 或拥有移动开发的经验。」emmmm… 纯属扯淡 …
如果不了解 Dart,那也仅限于看 Demo 是怎么写的 …
Dart 出自 Google。是一种面向对象编程的强类型语言,语法有点像 Java 与 JavaScript 的集合体。
官方学习资料
以下是使用 Flutter 需要掌握的 Dart 基础语法:
(以下内容摘抄来至 官网文档 , 没必要细看,可快速的过一遍,只做了解。)
变量声明
var
类似于 JavaScript 中的 var,它可以接收任何类型的变量,但最大的不同是 Dart 中 var 变量一旦赋值,类型便会确定,则不能再改变其类型,如:
var t;
t=”hi world”;
// 下面代码在 dart 中会报错,应为变量 t 的类型已经确定为 String,
// 类型一旦确定后则不能再更改其类型。
t=1000;
上面的代码在 JavaScript 是没有问题的,前端开发者需要注意一下,之所以有此差异是因为 Dart 本身是一个强类型语言,任何变量都是有确定类型的,在 Dart 中,当用 var 声明一个变量后,Dart 在编译时会根据第一次赋值数据的类型来推断其类型,编译结束后其类型就已经被确定,而 JavaScript 是纯粹的弱类型脚本语言,var 只是变量的声明方式而已。
dynamic 和 Object
Dynamic 和 Object 与 var 功能相似,都会在赋值时自动进行类型推断,不同在于,赋值后可以改变其类型,如:
dynamic t;
t=”hi world”;
// 下面代码没有问题
t=1000;
Object 是 dart 所有对象的根基类,也就是说所有类型都是 Object 的子类,所以任何类型的数据都可以赋值给 Object 声明的对象,所以表现效果和 dynamic 相似。
final 和 const
如果您从未打算更改一个变量,那么使用 final 或 const,不是 var,也不是一个类型。一个 final 变量只能被设置一次,两者区别在于:const 变量是一个编译时常量,final 变量在第一次使用时被初始化。被 final 或者 const 修饰的变量,变量类型可以省略,如:
// 可以省略 String 这个类型声明
final str = “hi world”;
//final str = “hi world”;
const str1 = “hi world”;
//const String str1 = “hi world”;
函数
Dart 是一种真正的面向对象的语言,所以即使是函数也是对象,并且有一个类型 Function。这意味着函数可以赋值给变量或作为参数传递给其他函数,这是函数式编程的典型特征。
函数声明
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
dart 函数声明如果没有显示申明返回值类型时会默认当做 dynamic 处理,注意,函数返回值没有类型推断:
typedef bool CALLBACK();
// 不指定返回类型,此时默认为 dynamic,不是 bool
isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
void test(CALLBACK cb){
print(cb());
}
// 报错,isNoble 不是 bool 类型
test(isNoble);
对于只包含一个表达式的函数,可以使用简写语法
bool isNoble(int atomicNumber)=> _nobleGases [atomicNumber]!= null ;
函数作为变量
var say= (str){
print(str);
};
say(“hi world”);
函数作为参数传递
void execute(var callback){
callback();
}
execute(()=>print(“xxx”))
可选的位置参数
包装一组函数参数,用 [] 标记为可选的位置参数:
String say(String from, String msg, [String device]) {
var result = ‘$from says $msg’;
if (device != null) {
result = ‘$result with a $device’;
}
return result;
}
下面是一个不带可选参数调用这个函数的例子:
say(‘Bob’, ‘Howdy’); // 结果是:Bob says Howdy
下面是用第三个参数调用这个函数的例子:
say(‘Bob’, ‘Howdy’, ‘smoke signal’); // 结果是:Bob says Howdy with a smoke signal
可选的命名参数
定义函数时,使用{param1, param2, …},用于指定命名参数。例如:
// 设置 [bold] 和[hidden]标志
void enableFlags({bool bold, bool hidden}) {
// …
}
调用函数时,可以使用指定命名参数。例如:paramName: value
enableFlags(bold: true, hidden: false);
可选命名参数在 Flutter 中使用非常多。
异步支持
Dart 类库有非常多的返回 Future 或者 Stream 对象的函数。这些函数被称为异步函数:它们只会在设置好一些需要消耗一定时间的操作之后返回,比如像 IO 操作。而不是等到这个操作完成。
async 和 await 关键词支持了异步编程,运行您写出和同步代码很像的异步代码。
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);
});
Future.catchError
如果异步任务发生错误,我们可以在 catchError 中捕获错误,我们将上面示例改为:
Future.delayed(new Duration(seconds: 2),(){
//return “hi world!”;
throw AssertionError(“Error”);
}).then((data){
// 执行成功会走到这里
print(“success”);
}).catchError((e){
// 执行失败会走到这里
print(e);
});
在本示例中,我们在异步任务中抛出了一个异常,then 的回调函数将不会被执行,取而代之的是 catchError 回调函数将被调用;但是,并不是只有 catchError 回调才能捕获错误,then 方法还有一个可选参数 onError,我们也可以它来捕获异常:
Future.delayed(new Duration(seconds: 2), () {
//return “hi world!”;
throw AssertionError(“Error”);
}).then((data) {
print(“success”);
}, onError: (e) {
print(e);
});
Future.whenComplete
有些时候,我们会遇到无论异步任务执行成功或失败都需要做一些事的场景,比如在网络请求前弹出加载对话框,在请求结束后关闭对话框。这种场景,有两种方法,第一种是分别在 then 或 catch 中关闭一下对话框,第二种就是使用 Future 的 whenComplete 回调,我们将上面示例改一下:
Future.delayed(new Duration(seconds: 2),(){
//return “hi world!”;
throw AssertionError(“Error”);
}).then((data){
// 执行成功会走到这里
print(data);
}).catchError((e){
// 执行失败会走到这里
print(e);
}).whenComplete((){
// 无论成功或失败都会走到这里
});
Future.wait
有些时候,我们需要等待多个异步任务都执行结束后才进行一些操作,比如我们有一个界面,需要先分别从两个网络接口获取数据,获取成功后,我们需要将两个接口数据进行特定的处理后再显示到 UI 界面上,应该怎么做?答案是 Future.wait,它接受一个 Future 数组参数,只有数组中所有 Future 都执行成功后,才会触发 then 的成功回调,只要有一个 Future 执行失败,就会触发错误回调。下面,我们通过模拟 Future.delayed 来模拟两个数据获取的异步任务,等两个异步任务都执行成功时,将两个异步任务的结果拼接打印出来,代码如下:
Future.wait([
// 2 秒后返回结果
Future.delayed(new Duration(seconds: 2), () {
return “hello”;
}),
// 4 秒后返回结果
Future.delayed(new Duration(seconds: 4), () {
return ” world”;
})
]).then((results){
print(results[0]+results[1]);
}).catchError((e){
print(e);
});
执行上面代码,4 秒后你会在控制台中看到“hello world”。
Async/await
Dart 中的 async/await 和 JavaScript 中的 async/await 功能和用法是一模一样的,如果你已经了解 JavaScript 中的 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(“alice”,”******”).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(“alice”,”******”).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)的调用链。
Stream
Stream 也是用于接收异步事件数据,和 Future 不同的是,它可以接收多个异步操作的结果(成功或失败)。也就是说,在执行异步任务时,可以通过多次触发成功或失败事件而传递结果数据或错误异常。Stream 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。举个例子:
Stream.fromFutures([
// 1 秒后返回结果
Future.delayed(new Duration(seconds: 1), () {
return “hello 1”;
}),
// 抛出一个异常
Future.delayed(new Duration(seconds: 2),(){
throw AssertionError(“Error”);
}),
// 3 秒后返回结果
Future.delayed(new Duration(seconds: 3), () {
return “hello 3”;
})
]).listen((data){
print(data);
}, onError: (e){
print(e.message);
},onDone: (){
});
上面的代码依次会输出:
I/flutter (17666): hello 1
I/flutter (17666): Error
I/flutter (17666): hello 3
代码很简单,就不赘述了。
思考题:既然 Stream 可以接收多次事件,那能不能用 Stream 来实现一个订阅者模式的事件总线?
总结
通过上面介绍,相信你对 Dart 应该有了一个初步的印象,由于笔者平时也使用 Java 和 JavaScript,下面笔者根据自己的经验,结合 Java 和 JavaScript,谈一下自己的看法。
之所以将 Dart 与 Java 和 JavaScript 对比,是因为,这两者分别是强类型语言和弱类型语言的典型代表,并且 Dart 语法中很多地方也都借鉴了 Java 和 JavaScript。
Dart vs Java
客观的来讲,Dart 在语法层面确实比 Java 更有表现力;在 VM 层面,Dart VM 在内存回收和吞吐量都进行了反复的优化,但具体的性能对比,笔者没有找到相关测试数据,但在笔者看来,只要 Dart 语言能流行,VM 的性能就不用担心,毕竟 Google 在 go(没用 vm 但有 GC)、javascript(v8)、dalvik(android 上的 java vm)上已经有了很多技术积淀。值得注意的是 Dart 在 Flutter 中已经可以将 GC 做到 10ms 以内,所以 Dart 和 Java 相比,决胜因素并不会是在性能方面。而在语法层面,Dart 要比 java 更有表现力,最重要的是 Dart 对函数式编程支持要远强于 Java(目前只停留在 lamda 表达式),而 Dart 目前真正的不足是生态,但笔者相信,随着 Futter 的逐渐火热,会回过头来反推 Dart 生态加速发展,对于 Dart 来说,现在需要的是时间。
Dart vs JavaScript
JavaScript 的弱类型一直被抓短,所以 TypeScript、Coffeescript 甚至是 Facebook 的 flow(虽然并不能算 JavaScript 的一个超集,但也通过标注和打包工具提供了静态类型检查)才有市场。就笔者使用过的脚本语言中(笔者曾使用过 Python、PHP),JavaScript 无疑是动态化支持最好的脚本语言,比如在 JavaScript 中,可以给任何对象在任何时候动态扩展属性,对于精通 JavaScript 的高手来说,这无疑是一把利剑。但是,任何事物都有两面性,JavaScript 的强大的动态化特性也是把双刃剑,你可经常听到另一个声音,认为 JavaScript 的这种动态性糟糕透了,太过灵活反而导致代码很难预期,无法限制不被期望的修改。毕竟有些人总是对自己或别人写的代码不放心,他们希望能够让代码变得可控,并期望有一套静态类型检查系统来帮助自己减少错误。正因如此,在 Flutter 中,Dart 几乎放弃了脚本语言动态化的特性,如不支持反射、也不支持动态创建函数等。并且 Dart 在 2.0 强制开启了类型检查(Strong Mode),原先的检查模式(checked mode)和可选类型(optional type)将淡出,所以在类型安全这个层面来说,Dart 和 TypeScript、Coffeescript 是差不多的,所以单从这一点来看,Dart 并不具备什么明显优势,但综合起来看,dart 既能进行服务端脚本、APP 开发、web 开发,这就有优势了!
官方 PPT 宣传截图
Flutter 底层架构的一个大概示意图:
Material 和 Cupertino 是 Flutter 官方提供的两个不同的 UI 风格组件库(前者 Android,后者 IOS)。
在 Flutter 中,一切皆是 Widget。一个按钮是 Widget,一段文字也是 Widget,一个图片也是 Widget,一个路由导航 也是 Widget。所以前期接触 Flutter 可以先学习这两个 UI 库如何使用即可。(个人见解)
基础组件库
Material 组件库
Cupertino 组件库
搭建开发环境
windows 上的搭建
macOS 上的搭建
linux 上的搭建
搭建过程很简单,下载 SDK 包,然后配置下环境变量就 ok 了。
编辑器推荐
VScode,轻巧、简洁。
配置好 Flutter 环境,只需要在安装一个 Flutter 插件就好了。
官方配置教程
第一个 Demo
在 VScode 中安装好插件后,按下 shift + command + p 输入 flutter,选择 New Project。
第一次创建时可能需要选择 Flutter SDK 的位置。
下面的 Demo 是官网上的给出的代码,整理出来的一个完整的。
先在 pubspec.yaml 中添加一个依赖: english_words 它是 Dart 语言编写的一个随机生成英文单词的工具包。
pubspec.yaml 是 Flutter 配置文件,可以理解为 npm 中的 package.json
找到文件的第 21 行:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
# 在这里添加 版本号遵循 语义化(Semantic Versioning)
english_words: ^3.1.5
dev_dependencies:
flutter_test:
sdk: flutter
Flutter 有一个官方的包管理平台,pub.dartlang.org 类似于 npm
添加完成后,在控制台输入 flutter packages get 或者在编辑器中右键点击 pubspes.yaml 选择 Get Packages
也就是安装新的依赖。
替换 Demo 代码这个 Demo 是一个随机生成英文名字的程序,有一个可以无限滚动的列表,可以让用户对喜欢的名字进行红心标记搜藏,然后点击右上角,可以查看已收藏的名字(路由跳转来实现的)。
将 lib/main.dart 中的所有代码删除,替换成下面的代码:
下面的代码是将官网 Demo 中的代码整理好的,可以先不去管它什么样的结果或者具体每句代码什么意思,先将 Demo 在模拟器中跑起来再说。
import ‘package:flutter/material.dart’;
import ‘package:english_words/english_words.dart’;
// 程序入口
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: ‘Startup Name Generator’,
home: new RandomWords(),
theme: new ThemeData(
primaryColor: Colors.white,
),
);
}
}
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0);
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text(‘Startup Name Generator’),
actions: <Widget>[
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
return new Scaffold(
appBar: new AppBar(
title: new Text(‘Saved Suggestions’),
),
body: new ListView(children: divided),
);
},
)
);
}
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
);
}
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// 对于每个建议的单词对都会调用一次 itemBuilder,然后将单词对添加到 ListTile 行中
// 在偶数行,该函数会为单词对添加一个 ListTile row.
// 在奇数行,该行书湖添加一个分割线 widget,来分隔相邻的词对。
// 注意,在小屏幕上,分割线看起来可能比较吃力。
itemBuilder: (context, i) {
// 在每一列之前,添加一个 1 像素高的分隔线 widget
if (i.isOdd) return new Divider();
// 语法 “i ~/ 2” 表示 i 除以 2,但返回值是整形(向下取整),比如 i 为:1, 2, 3, 4, 5
// 时,结果为 0, 1, 1, 2, 2,这可以计算出 ListView 中减去分隔线后的实际单词对数量
final index = i ~/ 2;
// 如果是建议列表中最后一个单词对
if (index >= _suggestions.length) {
// … 接着再生成 10 个单词对,然后添加到建议列表
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
}
);
}
}
选择调试 -> 启动调试 然后选择 ios emulator , 等待启动即可。(这个是 macOS 上的操作,windows 只能选择 Android 的模拟器,当前所有的前提是你的 Flutter 环境确保搭建成功了。)运行成功后如下图所示:
官方学习资料链接
Flutter 中文网
Flutter 实战
以上, 致那颗骚动的心……