接近半年没有在简书冒泡了。这段时间一是忙于使用云信IM开发相应项目,二是整理和收集相关Flutter的相关资料进行学习。国内关于Flutter的资料还是太过于稀少,以至于我只能去YouTube和Udemy上看英文版视频去学习,其间的感受可想而知。。。不过不可否认的是,有些视频讲解的内容还是很好的,不是那种简简单单的零基础学习。
言归正传,我们还是开始今天的学习。
相关代码已经上传到github,欢迎大家star、follow
Dart语言是什么
Dart是谷歌开发的计算机编程语言,亮相于2011年10月10至12日在丹麦奥尔胡斯举行的GOTO大会上。2015年5月Dart开发者峰会上,亮相了基于Dart语言的移动应用程序开发框架Sky,后更名为Flutter。
为什么Flutter会选择Dart?我觉得主要基于以下三点:
a) 热重载。我想每个Android开发者都对漫长的编译时长感到很无奈,往往选择上厕所或者倒杯水舒缓一下心情。但是Dart不一样,直接给你一个热重载,瞬间就能看到效果。虽然之前阿里也出过Android开发热重载的相关插件Freeline,但是效果不仅一般,而且插件本身开发难度较高,导致项目停摆1年多,已经失去使用意义。所以总的来说,Dart的热重载绝对是一个跨时代的功能
b) Dart可以在没有锁的情况下进行对象分配和垃圾回收。就像JavaScript一样,Dart避免了抢占式调度和共享内存(因而也不需要锁),所以Dart可以更轻松地创建以60fps运行的流畅动画和转场。这个在你使用Dart开发出App之后必定会深有感触
c) Dart语言特别容易学习。Dart语言与Java语言非常类似,这也是本篇为什么以“给Java开发者”来开篇的原因。
话不多说,开始学习吧。
开始
1. Dart的一个简单例子
这是使用Dart语言来实现最基础的计算器加减乘除功能的范例。我们先宏观的了解一下Dart语言大体风格。
void main() {
print("加法结果为:${operation(10, 5, add)}");
print("减法结果为:${operation(10, 5, sub)}");
print("乘法结果为:${operation(10, 5, multi)}");
print("除法结果为:${operation(10, 5, div)}");
}
int add(int a, int b) {
return a+b;
}
int sub(int a, int b) {
return a-b;
}
int multi(int a, int b) {
return a*b;
}
int div(int a, int b) {
return a~/b;
}
int operation(int a, int b, int method(int a, int b)) {
return method(a, b);
}
如果你之前学习过Java或者Kotlin语言(当然,部分Dart语法可能与其他语言类似,不过我没学过,不在此深究),那么Dart给你的第一感觉便是:哇,这个看上去似曾相识
Dart语言的基本类型也是诸如int、String等寻常关键字。同Kotlin一样,也可以使用${表达式}将表达式放入字符串中。Dart的访问控制符没有显式的展现出来,这个我们会在稍后进行详细介绍
同Kotlin一样,在Dart中我们可把一个函数当做一个变量传入到另外一个函数中
只要你有程序开发基础,我想上面的范例对你来说应该没啥问题。下面我们从细节上开始来详细了解Dart语言吧,这里我有一个声明:过于基础的知识点我只会一笔带过,时间应该花费在重要的知识点上
2. 变量、类型
Dart的几种内置的数据类型如下:数值型- num
、布尔型-bool
、字符串-String
、列表-List
、键值对-Map
、其他类型-Runes
、Symbols
。而数值型仅有int
与double
,不像其他语言分的很细
num num1 = 1;
num num2 = 1.1;
int int1 = 1;
double double1 = 1.1;
如果变量使用num
进行声明,则可以随意在使用中转换为int
或double
类型;但如果使用int
或者double
进行明确的声明,那么就不能随意转换了
num1 = 1.1;
num2 = 1;
// int1 = 1.1; 错误
除了以上所说的数据类型,Dart还有var
、dynamic
与const
三种数据类型
var
可以用来声明任意类型,Dart会根据其被赋予的数值的数据类型进行自动推导
var var1 = "String";
var var2 = 1;
var var3 = 1.1;
var var4 = true;
如果你仅使用var
声明一个变量但是并未对其进行赋值,那么你可以在使用过程中将其更改为任意数据类型的值
var var5;
var5 = 1;
var5 = 1.1;
但如果你使用var
声明变量的时候已经对其赋予指定数据类型的值,那么其数据类型就不可以更改了,因为此时已经决定了它是什么类型。var
修饰的变量一旦被编译,则会自动匹配var变量的实际类型,并用实际类型来替换该变量的申明
var var6 = 1;
// var6 = 1.1; 错误
dynamic
被编译后,实际是一个Object
类型,只不过编译器会对dynamic
类型进行特殊处理,让它在编译期间不进行任何的类型检查,而是将类型检查放到了运行期
dynamic dynamic1 = 1;
dynamic1 = 1.1;
dynamic1 = "";
dynamic1 = true;
正因为类型检查放到了运行期,所以在使用dynamic
的时候需要倍加小心
// dynamic1++; 错误,编译期可以通过,但是运行时报错
再来看看const
。Dart中有两种数据常量数据类型,const
和final
。与final
不同的是,const
是编译时常量。什么是编译时常量,来看这么个例子
在String
类里面有一个判断其是不是为空的函数isEmpty
。在代码编译过程中是没法知道字符串""是不是为空的,只有当代码运行到此行之后,才能通过isEmpty
函数的调用知道。所以const
就不满足这一类型的常量声明,只能使用final
final int int3 = "".isEmpty ? 10 : 6;
// const int int4 = "".isEmpty ? 10 : 6; 错误
同理,下面这个例子就可以。因为代码在编译期就知道8是肯定大于6的,所以这个值就能被确定下来
const int int4 = 8 > 6 ? 8 : 6;
可以使用const
进行初始化的对象也可以使用final
进行赋值,反之则不行
final int5 = 8 > 6 ? 8 : 6;
任何数据类型,包括int
或者double
这种看似基本类型的,如果没有赋默认值,Dart都会用null
来作为其默认值。这是不是比Java还要面向对象?
int int7;
print(int7);
这里补充一个不相干的知识点。
print
可以打印任意Object
类型的对象,String
、double
、int
、bool
的父类都是Object
。
print("Hello World");
print(3);
print(1.1);
print(true);
print(null);
Dart中操作符大体上与其他语言差不多,这里只介绍一些不常见的操作符
-
~/
。Java里面如果int/int
的话,得到的也是int
,这就是通常所说的取整。如果要得到浮点型数值的结果,则需要将其中一个数值变成浮点型数值才行,但是Dart不需要这样
var double2 = 7 / 3;
print("double2:$double2");
有什么办法可以得到int
呢?那就需要用到~/
了
var int2 = 7 ~/ 3;
print("int2:$int2");
-
as
强转操作符。这个跟Kotlin是一样的。这边要是转换格式不匹配,则会报错
// int int8 = num1 as int; 运行时报错
double double3 = num1 as double;
所以在使用之前最好判断一下
if (num1 is int) {}
-
??=
。空赋值操作符
int int9;
int9 ??= 11;
print("int9:$int9");
-
?.
。类似于Kotlin的非空判断
int a10;
print(a10?.toString());
a10 = 10;
print(a10?.toString());
- 级联符号
..
允许您在同一个对象上进行一系列操作。 除了函数调用之外,还可以访问同一对象上的字段。其实相当于Java的链式调用
CircleShape shape = new CircleShape()
..radius = 3
..color = 1;
Dart里没有private
/protected
/public
等权限修饰符,这就意味着默认情况下函数或者常量、变量都是可访问的。但是Dart还是有私有权限设置的办法的,只需要将需要修饰的函数或者常量、变量加上_
前缀即可。但是这里比较坑的一点是,_
并不是从class级别去限制,而是从package级别去限制
在同一个包下面创建一个私有属性
class PrivateTest {
String _private = "private";
}
使用正常
print(PrivateTest()._private);
将该类移至到其他包下
// print(PrivateTest2()._private2); 错误,无法访问private
3. 控制流和异常
这个很简单,不多啰嗦了
int age = 30;
if (age < 30) {
print("young");
} else if (age > 33) {
print("old");
} else {
print("Just so so");
}
for (int i = 0; i < 7; i++) {
print(i);
}
var list2 = <String>["1", "2", "3", "4"];
for (String value in list2) {
print(value);
}
int a3 = 0;
while (a3 < 10) {
print("End?");
a3++;
}
do {
print("End2?");
a3--;
} while (a3 != 0);
switch
的用法有一个地方需要单独说一下。Dart提供了从一个case
转入其他case
的功能,只需要使用continue
关键字加上自定义的标签来完成
int score = 70;
switch (score ~/ 10) {
case 9:
print("Wonderful");
break;
case 8:
print("Great");
break;
case 7:
print("Good");
continue KeepTrying;
break;
case 6:
print("Just so so");
continue KeepTrying;
break;
KeepTrying:
default:
print("Keep trying");
break;
}
异常捕获。写法与Java基本类似,但是还是有点小区别
try {
String a;
print(a.length);
} on NoSuchMethodError catch (e) {
print(e.toString());
} catch (e) {
print(e.toString());
}
on
和catch
的区别在于是否关心异常的实例
4. 字符串
在任何一门语言中,字符串都是被单独拿出来的小重点,因为他启到一个承上启下的作用,后面我们将开始接触到类与函数的概念。刚才我们知道Dart中String
跟int
同属对象,虽然情况与Java等有所不同,但是我们依然重点单独讲一下它
Dart中可以使用单引号或双引号声明字符串。在Java和Kotlin中都不可以这样,单引号只能声明为一个char
String string2 = "Hello World";
String string3 = 'Hello World';
字符串拼接的方式很多:
使用空格来拼接
String string4 = "Hello" "Hello" "Hello";
使用+
来拼接
String string5 = "Hello" + "Hello" + "Hello";
使用换行来拼接
String string6 = "Hello"
"Hello"
"Hello";
使用${表达式}
来拼接。这个跟Kotlin是一样的
String string7 = "$string2";
剩下的,如何使用String
中的函数,倒是没什么好说的,大家都能看得懂
String string1 = "Hello World";
string1.contains("Hello");
string1.endsWith("World");
string1.indexOf("e");
string1.isEmpty;
string1.length;
string1.lastIndexOf("l");
string1.replaceRange(0, 5, "Hi");
string1.substring(0, 5);
string1.split(" ").length;
string1.trim();
string1.toLowerCase();
string1.toUpperCase();
string1.toString();
5. 函数
开始进入重点部分了
Dart的函数基本上跟Java是一样的,除了没有权限修饰符
int func1(int a, int b) {
return a + b;
}
void func4() {
print("fun4");
}
函数调用
func1(1, 2);
有语法糖可以让单行函数体变得更优雅
int func2(int a, int b) => a + b;
可以省略函数返回类型,默认返回null
func3(int a, int b) => a + b;
print("func3():${func3(1, 2)}");
可选参数
这个概念在Java跟Kotlin都是没有的。既然是可选,那就是参数可以不传
可选参数分为2种,可选位置参数和可选命名参数
可选位置参数严格根据函数的位置传入参数,它有个很明显的标志[]
。来看下可选位置参数的写法,你可以选择只传c或者同时传c、d,但是不可以只传d不传c
int func5(int a, int b, [int c, int d]) {
if (c != null && d != null) {
return a + b + c + d;
} else if (c != null) {
return a + b + c;
} else if (d != null) {
return a + b + d;
}
return a + b;
}
print(func5(1, 2));
print(func5(1, 2, 3));
print(func5(1, 2, 3, 4));
可选命名参数相对灵活一点,你可以选择传递任何一个你想传的参数,它有个很明显的标志{}
。在传入的时候只需要指定下对应的参数名,没有顺序限制也没有可选位置参数那样传参前置条件
int func6(int a, int b, {int c, int d}) {
if (c != null && d != null) {
return a + b + c + d;
} else if (c != null) {
return a + b + c;
} else if (d != null) {
return a + b + d;
}
return a + b;
}
print(func6(1, 2));
// print(func6(1, 2, 3, 4)); 错误,可选参数必须要指定对应的参数名
print(func6(1, 2, c: 3));
print(func6(1, 2, d: 4));
print(func6(1, 2, c: 3, d: 4));
默认参数值
在函数的参数上面使用=
号给一个常量值。如果没有传入该值,代码在运行时就使用刚才给的值
void func7(int a, int b, [int c = 10, int d]) {}
void func8(int a, int b, {int c = 10, int d}) {}
函数对象
Dart中函数也是一个对象,可以通过var或者Function来声明
void func9() {
print("func9");
}
Function function = func9;
function();
函数对象可以作为一个入参,也可以作为一个返回值对象返回
void func10(Function function) {
function();
}
func10(func9);
当函数作为一个返回值对象返回时,我们也称其为闭包。闭包定义在其它函数内部,能够访问外部函数的局部变量,并持有其状态
Function func11(int value1) {
return (int value2) {
return value1 + value2;
};
}
Function function11 = func11(1);
print(function11(2));
6. 类型List(列表)、Set(集合)和Map(映射)
这节其实是对函数概念的理解的一个升华。List
/Set
/Map
使用起来其实很简单的,函数名与Java几乎雷同
先来看看List
创建List
对象的方式很多,常见的是使用[]
创建列表。这个可不是Java里的数组
List list1 = ["Ronaldo", 33, "Messi", 30];
使用泛型来限制列表可添加数据类型
List list3 = <int>[33, 30];
此种写法又是在运行时进行数据类型检查,所以要小写添加的数据的类型是否匹配
// list3.add(true); 错误,编译通过,运行出错
创建固定长度的列表
List<String> list5 = new List(5);
创建可改变长度的列表
List<String> list7 = new List();
list7.add("1");
list7.add("2");
list7.add("3");
list7.add("4");
list7.add("5");
list7.add("6");
list7.length = 10;
list7[9] = "9";
// list7[99] = "9"; 错误,长度只有10
list7.length = 13;
list7[12] = "12";
list7.length = 15;
list7[14] = "14";
在初始化固定长度后的List
中添加数据
List<String> list6 = new List()..length = 10;
list6.add("1");
list6.add("2");
list6.add("3");
list6.add("4");
list6.add("5");
list6.add("6");
list6.add("7");
list6.add("8");
利用List
类中的工厂构造函数来创建
List<String> list8 = List<String>.from(["Ronaldo", "Messi"]);
为所有List
中的元素统一赋初值
List<String> list9 = List<String>.filled(3, "");
用生成器给所有元素赋初始值
List<String> list10 = List<String>.generate(3, (int index) {
return "";
});
遍历列表的方式
虽然之前指定了添加的数据类型,但是泛型在前与在后效果是不一样的
List list3 = <int>[33, 30];
list3.forEach((dynamic value) {
print("$value");
});
泛型在前的话就可以直接使用指定类型进行遍历了
List<String> list2 = ["Ronaldo", "Messi"];
list2.forEach((String value) {
print("$value");
});
其他List
类中的函数,跟之前一样,应该都能看得懂
list2.add("Dybala");
list2.length;
list2.contains("Dybala");
list2.clear();
list2.elementAt(0);
// 对现有元素进行扩展
List<String> temp = ["Piatek", "Mandzukic"];
temp.expand((String element) {
return [element, element];
});
list2.insert(2, "Messi");
List<String> iterable = <String>["Pogba", "Griezmann"];
// List iterable = <String>["Pogba", "Griezmann"]; 错误,类型不一致不能添加
list2.addAll(iterable);
list2.addAll(<String>["Harry Kane", "Modric"]);
// 判断列表中的任一元素是否满足指定条件
bool any = list2.any((String element) {
return element == "Dybala";
});
// 判断列表中的全部元素是否满足指定条件
bool every = list2.every((String element) {
return element == "Dybala";
});
list2.first;
// 获取列表中第一个满足指定条件的元素
try {
var firstWhere = list2.firstWhere((String element) {
return element == "Dybala1";
});
} catch (e) {
}
var range = list2.getRange(0, 2);
print(list2.indexOf("Griezmann"));
// 从第几个元素开始查找
print(list2.indexOf("Griezmann", 1));
list2.isEmpty;
// 查找满足指定条件的元素索引
int index = list2.indexWhere((String element) {
return element == "Messi";
});
// 转成字符串
print(list2.join());
list2.map((String value) {
return "person $value";
}).forEach((String value) {
print(value);
});
list2.removeLast();
list2.remove("Messi");
list2.removeAt(1);
// 列表按照指定逻辑进行拼接
var reduce = list2.reduce((String value, String element) {
return "${value} + ${element}";
});
print(reduce);
// 获取列表中start、end索引间的集合
var tempList = list2.sublist(2);
// 查找列表中唯一一条满足指定条件的元素,如果元素数量大于1则报错
String singleWhere = list2.singleWhere((String string) {
return "Harry Kane" == string;
});
print(singleWhere);
// 查找列表中所有满足指定条件的元素
list2.where((String string) {
return string.substring(0, 1).toLowerCase() == 'm';
}).forEach((String string) {
print(string);
});
// 查找列表中所有满足指定条件的元素。与where不同的是,retainWhere直接将不满足的元素从原始List中去除,而where则不会破坏原数据
list2.retainWhere((String string) {
return string.substring(1, 2).toLowerCase() == 'o';
});
// 排序
list2.sort((String a, String b) {
return a.codeUnitAt(0) > b.codeUnitAt(0) ? 1 : 0;
});
使用{}
创建Map。Dart中Map
使用方式基本与Java类似
Map<String, int> map = {
"Juventus": 1,
"Napoli": 2,
"Inter": 3,
"A.C. Milan": 4
};
Map
也有相应的工厂函数来实现构建
Map<String, int> map2 = new Map.fromIterables(["a", "b"], [1, 2]);
Map<String, String> map3 = new Map.fromIterable(["a", "b"], key: (element) {
return element;
}, value: (element) {
return element;
});
其他Map
类中的函数,跟之前一样,应该都能看得懂
map["Lazio"] = 5;
print(map["Juventus"]);
Map<String, int> tempMap = {"Roma": 8};
map.addAll(tempMap);
map.containsKey("Real Madrid");
map.isNotEmpty;
map.isEmpty;
map.keys;
map.values;
map.length;
map.map((String key, int value) {
return MapEntry("team: $key", value);
}).forEach((String key, int value) {
print("key: $key value: $value");
});
map.remove("Inter");
map.removeWhere((String key, int value) {
return key == "Napoli";
});
map.update("Roma", (int value) {
return 2;
});
map.clear();
Set是没有顺序且不能重复的集合,所以不能通过索引去获取值
Set<String> set = new Set.from(["Ronaldo", "Messi"]);
Set<String> set2 = new Set();
set2.add("Italy");
set2.add("Italy");
set2.addAll(["England", "France"]);
set2.forEach((String value) {
print("value: ${value}");
});
set.first;
set.last;
set.contains("Ronaldo");
set.containsAll(["Ronaldo", "Messi"]);
set.difference(set2).forEach((dynamic value) {
print("$value");
});
// set.clear();
set.elementAt(0);
set.length;
set.take(2).toList();
set.union(set2).forEach((dynamic value) {
print("$value");
});
List
、Set
和Map
有一些通用的函数。其中的一些通用函数都是来自于类Iterable
。List
和Set
是Iterable
类的实现。虽然Map
没有实现Iterable
, 但是Map
的属性keys
和values
都是Iterable
对象
7. 类和泛型
其实类和泛型的概念很简单,因为几乎与Java一样
先看看类的声明
class ClassTest {
ClassTest()
}
如果没有声明构造函数,那么初始化的时候使用的是默认构造函数。
Object
类是所有类的父类
在Dart语言中,子类构造函数必须继承父类的构造函数
若调用的是默认构造函数,则无需显式声明继承关系
所以完整的描述ClassTest
类应该是
class ClassTest {
ClassTest() : super() {}
}
子类可以自由选择继承父类的哪个构造函数,只需要在自身构造函数后加:
号,在:
后面指定父类的构造函数
class ClassParentTest {
ClassParentTest(String value) {}
ClassParentTest.FromString(String value) {}
}
class ClassChildTest extends ClassParentTest {
ClassChildTest(String value) : super.FromString(value) {}
}
当然也重定向到同类的另一个构造函数上,但是不可以有额外的函数体
class ClassChildTest extends ClassParentTest {
ClassChildTest(String value) : super.FromString(value) {}
ClassChildTest.FromString(String value) : this(value);
}
类的使用跟Java一样
ClassTest classTest = new ClassTest();
类中未初始化的实例变量的默认值都为null
,我们可以像在Java中那样通过构造函数去初始化
class Caculator {
int x;
int y;
Caculator(int x, int y) {
this.x = x;
this.y = y;
}
}
来一个Dart特有的构造函数语法糖,这样初始化是不是看的更简洁一点
class Caculator {
int x;
int y;
Caculator(this.x, this.y) {}
}
还可以在构造函数体运行之前初始化实例变量
class Caculator {
int x;
int y;
Caculator(int x, int y)
: this.x = x,
this.y = y {}
}
因为Dart没法重载构造函数,所以提供了命名构造函数来解决这个问题
class Caculator {
int x;
int y;
Caculator.FromAnother(this.x, this.y) {}
}
命名构造函数的使用
Caculator caculator = Caculator.FromAnother(1, 2);
工厂构造函数,它是实现单例的一个好选择
class Shape {
String desp;
static final Map<String, Shape> _cache = new Map();
factory Shape.Type(String type) {
if (_cache.containsKey(type)) {
return _cache[type];
}
if (type == "circle") {
Shape shape = new Shape.Circle("this is a circle");
_cache[type] = shape;
return shape;
} else if (type == "square") {
Shape shape = new Shape.Square("this is a square");
_cache[type] = shape;
return shape;
} else {
Shape shape = new Shape.Unknown("this is an unknown shape");
_cache[type] = shape;
return shape;
}
}
Shape.Circle(this.desp) {}
Shape.Square(this.desp) {}
Shape.Unknown(this.desp) {}
}
类中的函数的使用跟Java也是几乎一样
class Caculator {
int x;
int y;
Caculator(this.x, this.y) {}
int add() {
return x + y;
}
int subract() {
return x - y;
}
}
getter
和setter
是特殊的函数,可以读写访问对象的属性,每个实例变量都有一个隐式的getter
,适当的加上一个setter
,可以通过实现getter
和setter
创建附加属性
class Caculator {
int x;
void set setX(int x) {
this.x = x;
}
int get getX {
return x;
}
}
怎么初始化类中final类型的变量呢,可以这样
class CircleShape {
int radius = 0;
int color = 0;
final int size;
final int price;
CircleShape() : size = 3, price = 1; // 通过此种方式对final值进行初始化
}
级联,刚才我们已经介绍过了
CircleShape shape = new CircleShape()
..radius = 3
..color = 1;
静态类
class StaticClass {
// 静态变量
static String staticValue = "";
// 静态函数
static void staticFunction() {}
}
想让类生成的对象永远不会改变,可以让这些对象变成编译时常量,定义一个const构造函数并确保所有实例变量是final的。这是实现单例的一个好办法
class StaticClass {
const StaticClass();
static final StaticClass value = new StaticClass();
}
抽象类
abstract class Continent {
String name = "Continent";
Continent(String name) {
this.name = name;
}
// 抽象函数
String getContinentName();
void desp() {
print("This is Continent ${getContinentName()}");
}
}
继承抽象类
class Asia extends Continent {
Asia(String name) : super(name) {}
@override
String get name => super.name;
@override
String getContinentName() {
return "Asia";
}
@override
void desp() {
print("Hello");
super.desp();
}
}
每个类都有一个隐式定义的接口,包含所有类和实例成员。Java里面接口就是接口,与Dart不同
class Europe implements Continent {
@override
String name;
@override
void desp() {
print("This is Continent ${getContinentName()}");
}
@override
String getContinentName() {
return "Europe";
}
}
使用with
关键字后面跟着一个或多个扩展类名,这相当于同时继承了2个类。但是,如果相同变量或者函数名类型不一致,则不可以mixin
class Club {
String clubName;
int _year;
int color;
void set year(int year) {
this._year = year;
}
int get year {
return this._year;
}
void cFunction() {}
}
class Sponsor {
String sponsorName;
int _year;
void set year(int year) {
this._year = year;
}
int get year {
return this._year;
}
void sFunction() {}
}
class Person extends Club with Sponsor {
void a() {
color;
clubName;
cFunction();
sFunction();
}
}
泛型跟Java也基本上差不多
class Utils<T, R> {
T valueA;
R valueB;
}
8. 异步
这可是Dart中的重点和难点
Dart是一个单线程的语言,遇到有延迟的运算(比如IO操作、延时执行)时,按顺序执行运算会发生阻塞,用户就会感觉到卡顿,于是Dart采用异步处理来解决这个问题。当遇到有需要延迟的运算时,将其放入到延迟运算的队列中去,把不需要延迟运算的部分先执行掉,最后再来处理延迟运算的部分。
async和await
async
关键字声明该函数内部有代码需要延迟执行
await
关键字声明运算为延迟执行,然后return
运算结果,返回值为一个Future
对象
要使用await
,必须在有async
标记的函数中运行,否则这个await
会报错
因此,整个流程只需要记住两点
-
await
关键字必须在async
函数内部使用 - 调用
async
函数必须使用await
关键字
下面是一个网络请求的例子
Future<String> httpRequestTest() async {
var httpClient = HttpClient();
HttpClientRequest request = await httpClient
.getUrl(Uri.parse("http://polls.apiblueprint.org/questions"));
HttpClientResponse response = await request.close();
if (response.statusCode == 200) {
String value = await response.transform(utf8.decoder).join();
return value;
}
return null;
}
通过then()
来设置异步回调
被添加到then()
中的函数会在Future
执行得到结果后立马执行(then()
函数没有被加入任何队列,只是被回调而已)
httpRequestTest().then((String value) {}).catchError(onError);
通过then()
可以实现Future的链式调用
如下例,addAddress
函数返回的是另外一个异步函数
Future<Function> addAddress(int value) async {
return (int x) async => value + x;
}
通过两次then()
处理异步结果,可以在一行代码里得到最终返回值
addAddress(10).then((Function function) {
return function(20);
}).then((dynamic value) {
return value;
});
如果不使用then()
函数是如何处理,我们来看看
Future<int> normalUse() async {
Function function = await addAddress(10);
int value = await function(20);
return value;
}
normalUse().then((int onValue) {});
由此可见,对应普通调用方式,链式调用简单多了
在Event队列中,事件以先进先出顺序执行。来看如下的实验,delayed1等待3s,delayed2等待2s
Future<String> delayedFunc1() {
return new Future.delayed(Duration(seconds: 3), () {
print("Finish delayed1");
return "Finish delayed1";
});
}
Future<String> delayedFunc2() {
return new Future.delayed(Duration(seconds: 2)).then((_) {
print("Finish delayed2");
return "Finish delayed2";
});
}
}
我们放到异步函数里面来测试一下
void sequence() async {
print(DateTime.now());
await delayedFunc1();
print(DateTime.now());
await delayedFunc2();
print(DateTime.now());
}
使用了await之后,虽然delayedFunc1延迟3s执行,delayedFunc2延迟2s执行,但是依然是FIFO
的顺序
有没有想过如果不放在异步函数里面会有什么效果?我们将其改成这样
void sequence() {
print(DateTime.now());
delayedFunc1();
print(DateTime.now());
delayedFunc2();
print(DateTime.now());
}
来看看结果。sequence函数很快就执行完了。似乎我们理解了其中的含义:函数本身并不是一个异步操作,若不加await则不会等待当前函数执行完成后再执行下一个
Dart线程中有一个消息循环机制(event loop
)和两个队列(event queue
和microtask queue
)
event queue
包含所有外来的事件:I/O,mouse events,drawing events,timers,isolate之间的message等;microtask queue
只在当前任务队列中排队,优先级高于event queue
。Dart事件循环执行两个队列里的事件。当main
执行完毕退出后,event loop
就会以FIFO(先进先出)的顺序执行microtask
,当所有microtask
执行完后它会从event queue
中取事件并执行。如此反复,直到两个队列都为空
当事件循环正在处理microtask
的时候,event queue
会被堵塞。这时候app就无法进行UI绘制,响应鼠标事件和I/O等事件
让我们以一个实际例子来了解一下microtask
与event
的流程。初看这个很感觉复杂,如果你第一遍不能理解,请再理解一遍
void sequence2() {
print('main #1 of 2');
scheduleMicrotask(() => print('microtask #1 of 3'));
new Future.delayed(
new Duration(seconds: 1), () => print('future #1 (delayed)'));
new Future(() => print('future #2 of 4'))
.then((_) => print('future #2a'))
.then((_) {
print('future #2b');
scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
new Future(() => print('future #2d (a new future)'));
}).then((_) => print('future #2c'));
scheduleMicrotask(() => print('microtask #2 of 3'));
new Future(() => print('future #3 of 4'))
.then((_) => print('future #3a'))
.then((_) => new Future(() => print('future #3b (a new future)')))
.then((_) => new Future(() => print('future #3c (a new future)')))
.then((_) => print('future #3d'));
new Future(() => print('future #4 of 4'))
.then((_) => new Future(() => print('future #4a (a new future)')));
new Future(() => print('future #5 of 5'));
scheduleMicrotask(() => print('microtask #3 of 3'));
print('main #2 of 2');
}
如果你的结果与此图一致,恭喜你,你完全搞懂了这个流程
我们来分析一下
- 首先执行主线程,其次执行Microtask,最后才是Event
- 开始Event,添加Future2、3、4。先来到2,按then顺序打印。2这里新建了一个Microtask 0和Future 2d。因为return的依然是当前Future,所以2c依然跟着当前Future打印,新建的那个Future 2d被添加到Event最末尾
- 当前Event中的Future 2执行完毕,来了一个插队Microtask 0。执行新建的Microtask 0。
- Microtask执行完毕,继续回到Event。继续顺序打印3,3b被添加到Event最末尾,因为return的是新Future,所以跟之前的流程不一样,此时3c与3d暂不存在
- 继续顺序打印4,4a被添加到Event最末尾
- 继续顺序打印5,至此回过头来再检查Event中有没有剩余未执行的事件
- 按照添加的顺序2d开始执行,然后是3b,此时又添加了新的Future 3c到最后,3b执行完毕之后是4a
- 这波执行完毕之后,执行3c,异步回调得到3d
- 之前延迟的delay到现在才被添加进来执行
- 至此当前所有事件执行完成
如果文字部分解释还不满足你,我们来看图
在执行Event之前,Main与Microtask依次执行完毕 Future2执行完之后,执行Microtask。Microtask插队了 Future3执行 Future4执行 最终剩余事件依次执行至此,async和await
学习完毕,下面还有另外一个挑战Stream
Stream
是一个异步数据源,它是Dart中处理异步事件流的统一API
集合可以理解为“拉”模式,比如你有一个List
,你可以主动地通过迭代获得其中的每个元素,想要就能拿出来。而Stream
可以理解为“推”模式,这些异步产生的事件或数据会推送给你(并不是你想要就能立刻拿到)。这种模式下,你要做的是用一个listener
(即callback
)做好数据接收的准备,数据可用时就通知你。
Stream
有3个工厂构造函数:fromFuture
、fromIterable
和periodic
,分别可以通过一个Future
、Iterable
或定时触发动作作为Stream
的事件源构造Stream
。
下面的代码就是通过一个List
构造的Stream
List<int> datas = new List(10000000);
second(Stream.fromIterable(datas));
我们可以通过async* + yield
返回Stream
对象
Stream<String> getStreamData(Iterable<int> values) async* {
for (int value in values) {
await Future<String>.delayed(Duration(seconds: 1));
yield "$value";
}
}
通过listen()
函数订阅Stream
上发出的数据(即事件)
下面的代码会先打印出从Stream
收到的每个数字,最后打印一个‘Done’
当Stream
中的所有数据发送完时,就会触发onDone
的调用,但提前取消订阅不会触发onDone
streamData.listen((String onData) {
print("streamData $onData");
}, onDone: () {
print("onDone");
}, onError: (dynamic error) {
print("$error");
});
还可以通过listen
的返回者subscription
对象设置onData
和onDone
的处理
下面的代码与前面的示例代码作用相同
Stream<String> streamData2 = getStreamData(<int>[1, 3, 5, 7, 9]);
StreamSubscription<String> subscription = streamData2.listen(null);
subscription.onData((String onData) {
if (int.parse(onData) > 5) {
subscription.cancel();
}
print("streamData $onData");
});
subscription.onError((dynamic error) {
print("$error");
});
subscription.onDone(() {
print("onDone");
});
listen
中的参数为null
,也就是没有订阅者。通过listen
的返回者subscription
对象设置了onData
和onDone
的处理,这才有了订阅者
如果在发出事件的同时添加订阅者,那么要在订阅者在该事件发出后才会生效。如果订阅者取消了订阅,那么它会立即停止接收事件
上面一个例子最后会打印出1、3、5、7,9因为被cancel
了所以不会打印
Stream
有两种订阅模式:单订阅(single)和多订阅(broadcast)。单订阅就是只能有一个订阅者,而广播是可以有多个订阅者
Stream
默认处于单订阅模式,所以同一个Stream
上的listen
和其它大多数函数只能调用一次,调用第二次就会报错。但Stream
可以通过transform()
函数(返回另一个Stream
)进行连续调用。通过Stream.asBroadcastStream()
可以将一个单订阅模式的Stream
转换成一个多订阅模式的Stream
,isBroadcast
属性可以判断当前Stream
所处的模式
streamData2.isBroadcast
单订阅在订阅者出现之前会持有数据,在订阅者出现之后就才转交给它。而多订阅模式,可以同时有多个订阅者,当有数据时就会传递给所有的订阅者,而不管当前是否已有订阅者存在。但是多订阅模式如果没有及时添加订阅者则可能丢数据,不过具体取决于Stream的实现。
下面的一个例子就是一旦有了第一个订阅者,然后再延迟添加第二个订阅者就会漏数据
Stream<String> streamData21 = streamData2.asBroadcastStream();
new Timer(Duration(seconds: 1), () {
streamData21.listen((String onData) {
print("streamData21 $onData");
});
});
new Timer(Duration(seconds: 5), () {
streamData21.listen((String onData) {
print("streamData22 $onData");
});
});
看看结果,此时后订阅的数据就丢失了。
你也可以选择自定义Stream
StreamController<int> streamController = new StreamController();
streamController..add(1)..add(2)..add(3)..add(4)..add(5);
streamController.close();
Stream<int> stream = streamController.stream;
stream.listen((int onData) {
print(onData);
});
注意这里close
就意味着事件结束了,所以多订阅模式会收不到数据,而单订阅模式则可以
Stream
和一般的集合类似,都是一组数据,只不过一个是异步推送,一个是同步拉取,所以他们都很多共同的函数,比如any
函数
Stream<String> streamData3 = getStreamData(<int>[1, 3, 5, 7, 9]);
streamData3.any((e) => int.parse(e) > 2).then((bool value) {
print(value);
});
Stream
也有自己通用的数据转换函数transform()
把一个Stream
作为输入,然后经过计算或数据转换,输出为另一个Stream
。另一个Stream
中的数据类型可以不同于原类型,数据多少也可以不同
Stream<String> streamData4 = getStreamData(<int>[1, 3, 5, 7, 9]);
var transformer = new StreamTransformer.fromHandlers(
handleData: (String data, EventSink<String> sink) {
sink.add("data:$data");
sink.add("data2:$data");
});
streamData4.transform(transformer).listen(print);
最后梳理一下Stream与Future的异同
Stream
和Future
是Dart异步处理的核心API。Future
只能表示一次异步获得的数据,而Stream
表示多次异步获得的数据,比如界面上的按钮可能会被用户点击多次,所以按钮上的点击事件(onClick)就可有理解为一个Stream
Stream
是流式处理,比如IO处理的时候,一般情况是每次只会读取一部分数据(具体取决于实现)。这和一次性读取整个文件的内容相比,Stream
的好处是处理过程中内存占用较小
来对比分别使用Stream
与Future
实现读文件的两种写法
Future<String> readText() async {
File file = new File("1.txt");
return await file.readAsString();
}
void readText2() {
File file = new File("1.txt");
Stream<List<int>> stream = file.openRead();
stream.transform(utf8.decoder).transform(LineSplitter()).listen(
(String element) {
print(element);
}, onError: (dynamic error) {
print("onError");
}, onDone: () {
print("onDonw");
});
}
9. 库
Dart的库管理比Java和Kotlin都要强大很多
导入dart库里面的包
import 'dart:math';
导入项目为DartDemo
中lib
目录下的包
import 'package:DartDemo/PrivateLibrary.dart';
导入相对路径下的包
import '../src/SrcLibrary.dart';
解决变量名冲突的办法是将引入的库加上别名。这个跟Kotlin的处理方式是一样的
import '../lib/PrivateLibrary.dart' as Private2;
不完全导入。只导入showFunction
函数
import 'package:DartDemo/ShowLibrary.dart' as ShowLibrary show showFunction;
不完全导入。只导入除hideFunction
函数之外的所有函数
import 'package:DartDemo/HideLibrary.dart' as HideLibrary hide hideFunction;
库的拆分
Part2Library.dart
是PartLibrary.dart
的一部分
// Part2Library.dart
part of 'PartLibrary.dart';
void part2LibraryFunction() {
print("Part2LibraryFunction");
}
// PartLibrary.dart
part 'Part2Library.dart';
在part
中,import
进来的库是共享命名空间的,所以我们没有再导入Part2Library.dart
import 'package:DartDemo/PartLibrary.dart';
延迟加载
import 'package:DartDemo/DeferredLibrary.dart' deferred as deferredLibrary;
我暂时还没有体会到延迟加载与非延迟加载在使用上有何区别
void deferred() async {
await deferredLibrary.loadLibrary();
deferredLibrary.deferredFunction();
}
可以通过重新导出部分库或者全部库来组合或重新打包库,一个库管理提供多个库的导入支持。这个在库的管理上比较省心
import "dart:convert";
export "dart:convert";
void reExportingFunction() {
print("reExportingFunction");
}
import 'package:DartDemo/ReExportingLibrary.dart';
我们来具体说一下第三方库如何导入
首先找到这个pubspec.yaml
文件,这个如同我们Android的build.gradle
,所有导入的文件(图片等)还有版本库都在里面管理
这里有我们项目的一些信息,Dart SDK的版本,第三方的依赖包版本
第三方库一般在dartlang里寻找
比如我要找dio
这个网络请求框架,我可以搜索它
可以在里面看到它目前的版本号是1.0.12,要使用它的话我们可以在dependencies
里添加描述
dependencies:
dio: '1.0.12'
有时候为了让Dart自己寻找最适合我们项目的版本,你可以写上any
。这个在包版本冲突上算是比较好的解决方案
dependencies:
dio: any
最后在右上角点击Get dependencies
导入
你可以在pubspec.lock
文件里查看到项目使用的某个库的版本
同样我们可以对包版本范围进行限制:指定一个最小和最大的版本号
这表示在2.x.x版本都是支持的,但是必须要大于2.1.0
'>=2.1.0 <3.0.0'
还有一种是指定最小版本,比其大的都支持
english_words : ^3.0.0
包下载成功会有如下显示
使用很简单,导入即可
import 'package:dio/dio.dart';
Dio dio = new Dio();
dio.get("https://www.baidu.com/").then((Response<dynamic> response) {
print(response.data);
});
至此,所有Dart的基本概念简单介绍完了。若有不清楚的可以私信或者添加评论,有时间我会来跟你讨论的
参考文章
为什么 Flutter 会选择 Dart ?
flutter实战5:异步async、await和Future的使用技巧
Dart与消息循环机制[翻译]
理解Dart 异步事件流 Stream
Dart 语法要点汇总
网友评论