1.3 Dart语言简介
在跨平台开领域被 JS 一统天下的今天,Dart 语言的出现无疑是一股清流。作为后来者,Dart语言有着不少 Java、Kotlin 和 JS 的影子,所以对于 Android 原生开发者、前端开发者而言无疑是非常友好。
官方也提供了包括 iOS 、React Native 等开发者迁移到 Flutter 上的文档,所以请不要担心,Dart 语言不会是你掌握 Flutter 的门槛,甚至作为开发者,就算你不懂 Dart 也可以看着代码摸索。
注意:由于本书并非专门介绍Dart语言的书籍,所以本章主要会介绍一下在Flutter开发中常用的语法特性,如果想更多了解Dart,读者可以去Dart官网学习,现在互联网上Dart相关资料已经很多了。另外Dart 2.0已经正式发布,所以本书所有示例均采用Dart 2.0语法。
1.3.1 语言特性
Dart所有的东西都是对象, 即使是数字numbers、函数function、null也都是对象,所有的对象都继承自Object类。
Dart动态类型语言, 尽量给变量定义一个类型,会更安全,没有显示定义类型的变量在 debug 模式下会类型会是 dynamic(动态的)。
Dart 在 running 之前解析你的所有代码,指定数据类型和编译时的常量,可以提高运行速度。
Dart中的类和接口是统一的,类即接口,你可以继承一个类,也可以实现一个类(接口),自然也包含了良好的面向对象和并发编程的支持。
Dart 提供了顶级函数(如:main())。
Dart 没有 public、private、protected 这些关键字,变量名以"_"开头意味着对它的 lib 是私有的。
没有初始化的变量都会被赋予默认值 null。
final的值只能被设定一次。const 是一个编译时的常量,可以通过 const 来创建常量值,var c=const[];,这里 c 还是一个变量,只是被赋值了一个常量值,它还是可以赋其它值。实例变量可以是 final,但不能是 const。
编程语言并不是孤立存在的,Dart也是这样,他由语言规范、虚拟机、类库和工具等组成:
SDK:SDK 包含 Dart VM、dart2js、Pub、库和工具。
Dartium:内嵌 Dart VM 的 Chromium ,可以在浏览器中直接执行 dart 代码。
Dart2js:将 Dart 代码编译为 JavaScript 的工具。
Dart Editor:基于 Eclipse 的全功能 IDE,并包含以上所有工具。支持代码补全、代码导航、快速修正、重构、调试等功能。
1.3.2 数据类型
- num
num 是数字类型的父类,有两个子类 int 和 double。
int 根据平台的不同,整数值不大于64位。在Dart VM上,值可以从-263到263 - 1,编译成JavaScript的Dart使用JavaScript代码,允许值从-253到253 - 1。
double 64位(双精度)浮点数,如IEEE 754标准所规定
int a = 1;
print(a);
double b = 1.12;
print(b);
// String -> int
int one = int.parse('1');
// 输出3
print(one + 2);
// String -> double
var onePointOne = double.parse('1.1');
// 输出3.1
print(onePointOne + 2);
// int -> String
String oneAsString = 1.toString();
// The argument type 'int' can't be assigned to the parameter type 'String'
//print(oneAsString + 2);
// 输出 1 + 2
print('$oneAsString + 2');
// double -> String 注意括号中要有小数点位数,否则报错
String piAsString = 3.14159.toStringAsFixed(2);
// 截取两位小数, 输出3.14
- String
Dart里面的String是一系列 UTF-16 代码单元。
您可以使用单引号或双引号来创建一个字符串。
单引号或者双引号里面嵌套使用引号。
用 或{} 来计算字符串中变量的值,需要注意的是如果是表达式需要${表达式}
- bool
Dart 是强 bool 类型检查,只有bool 类型的值是true 才被认为是true。
只有两个对象具有bool类型:true和false,它们都是编译时常量。
Dart的类型安全意味着您不能使用 if(nonbooleanValue) 或 assert(nonbooleanValue) 等代码, 相反Dart使用的是显式的检查值。
assert 是语言内置的断言函数,仅在检查模式下有效
在开发过程中, 除非条件为真,否则会引发异常。(断言失败则程序立刻终止)。
-
List集合
在Dart中,数组是List对象,因此大多数人只是将它们称为List。
Dart list文字看起来像JavaScript数组文字
//创建一个int类型的list
List list = [10, 7, 23];
// 输出[10, 7, 23]
print(list);
// 使用List的构造函数,也可以添加int参数,表示List固定长度,不能进行添加 删除操作
var fruits = new List();
- Map集合
- 一般来说,map是将键和值相关联的对象。键和值都可以是任何类型的对象。
每个键只出现一次,但您可以多次使用相同的值。Dart支持map由map文字和map类型提供。
- 初始化Map方式一: 直接声明,用{}表示,里面写key和value,每组键值对中间用逗号隔开。
1.3.3 变量声明
- var
var 可以定义变量,如 var tag = "666" ,这和 JS 、 Kotlin 等语言类似,同时 Dart 也算半个动态类型语言,同时支持闭包。var可以接收任何类型的变量,但最大的不同是Dart中var变量一旦赋值,类型便会确定,则不能再改变其类型,如:
var t;
t = "hi world";
// 下面代码在dart中会报错,因为变量t的类型已经确定为String,
// 类型一旦确定后则不能再更改其类型。
t = 1000;
`Dart` 属于是**强类型语言** ,但可以用 `var` 来声明变量,`Dart` 会**自推导出数据类型**,所以 `var` 实际上是编译期的“语法糖”。
-
dynamic和Object
Object
是Dart所有对象的根基类,也就是说所有类型都是Object
的子类(包括Function和Null),所以任何类型的数据都可以赋值给Object
声明的对象.dynamic
与var
一样都是关键词,声明的变量可以赋值任意对象。而
dynamic
与Object
相同之处在于,他们声明的变量可以在后期改变赋值类型。
dynamic t;
Object x;
t = "hi world";
x = 'Hello Object';
//下面代码没有问题
t = 1000;
x = 1000;
dynamic
与Object
不同的是,dynamic
声明的对象编译器会提供所有可能的组合,
而Object
声明的对象只能使用Object的属性与方法, 否则编译器会报错。如:
dynamic a;
Object b;
main() {
a = "";
b = "";
printLengths();
}
printLengths() {
// no warning
print(a.length);
// warning:
// The getter 'length' is not defined for the class 'Object'
print(b.length);
}
变量a不会报错, 变量b编译器会报错
**`dynamic` 表示动态类型**, 被编译后,实际是一个 `object` 类型,在编译期间不进行任何的类型检查,而是在运行期进行类型检查。
`dynamic`的这个特点使得我们在使用它时需要格外注意,这很容易引入一个运行时错误.
- final和const
如果您从未打算更改一个变量,那么使用 final
或 const
,不是var
,也不是一个类型。 一个 final
变量只能被设置一次,两者区别在于:const
变量是一个编译时常量,final
变量在第一次使用时被初始化。被final
或者const
修饰的变量,变量类型可以省略,如:
//可以省略String这个类型声明
final str = "hi world";
//final String str = "hi world";
const str1 = "hi world";
//const String str1 = "hi world";
注意 Dart 下的数值,在作为字符串使用时,是需要显式指定的。比如:
int i = 0; print("aaaa" + i);
这样并不支持,需要print("aaaa" + i.toString());
这样使用,这和 Java 与 JS 存在差异,所以在使用动态类型时,需要注意不要把 number 类型当做 String 使用。
注意 Dart 中数组等于列表,所以
var list = [];
和List list = new List()
可以简单看做一样。
1.3.4 函数
Dart是一种真正的面向对象的语言,所以即使是函数也是对象,并且有一个类型Function。这意味着函数可以赋值给变量或作为参数传递给其他函数,这是函数式编程的典型特征。
每个应用程序都必须有一个顶层main()函数,它可以作为应用程序的入口点。该main()函数返回void并具有List
- 函数声明
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"))
- 可选参数
可选的命名参数
定义函数时,使用{param1, param2, …},放在参数列表的最后面,用于指定命名参数。例如:
//设置[bold]和[hidden]标志
void enableFlags({bool bold, bool hidden}) {
// ...
}
调用函数时,可以使用指定命名参数。例如:paramName: value
enableFlags(bold: true, hidden: false);
可选命名参数在Flutter中使用非常多。
可选的位置参数
包装一组函数参数,用[]标记为可选的位置参数,并放在参数列表的最后面:
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
注意,不能同时使用可选的位置参数和可选的命名参数
- 默认参数
函数可以使用=为命名参数和位置参数定义默认值。默认值必须是编译时常量。如果没有提供默认值,则默认值为null。
下面是为命名参数设置默认值的示例:
// 设置 bold 和 hidden 标记的默认值都为false
void enableFlags2({bool bold = false, bool hidden = false}) {
// ...
}
// 调用的时候:bold will be true; hidden will be false.
enableFlags2(bold: true);
* 下一个示例显示如何为位置参数设置默认值:
String say(String from, String msg,
[String device = 'carrier pigeon', String mood]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
if (mood != null) {
result = '$result (in a $mood mood)';
}
return result;
}
//调用方式:
say('Bob', 'Howdy'); //结果为:Bob says Howdy with a carrier pigeon;
- 匿名函数
大多数函数都能被命名为匿名函数,如 main() 或 printElement()。您还可以创建一个名为匿名函数的无名函数,有时也可以创建lambda或闭包。您可以为变量分配一个匿名函数,例如,您可以从集合中添加或删除它。
一个匿名函数看起来类似于一个命名函数 - 0或更多的参数,在括号之间用逗号和可选类型标注分隔。
下面的代码块包含函数的主体:
([[Type] param1[, …]]) {
codeBlock;
};
/// 下面的示例定义了一个具有无类型参数的匿名函数item,该函数被list中的每个item调用,输出一个字符串,该字符串包含指定索引处的值。
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});
1.3.5 异步支持
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 login(String userName, String pwd){
...
//用户登录
};
Future 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
代码很简单,就不赘述了。