给Java开发者的Flutter开发基础---Dart语言
接近半年没有在简书冒泡了。这段时间一是忙于使用云信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");
- ??运算符。如果前者不为空,返回前者;否则返回后面的
int int10;
int int11 = int10 ?? 11;
print("int11 = ${int11}");
?.
。类似于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) {}
}
不过把factory
包装一下让人产生“错觉”可能会更好一点
class EventBus {EventBus._singleInstance();static EventBus _instance = new EventBus._singleInstance();factory EventBus() {return _instance;}
}EventBus eventBus = new EventBus();
类中的函数的使用跟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) {}@overrideString get name => super.name;@overrideString getContinentName() {return "Asia";}@overridevoid desp() {print("Hello");super.desp();}
}
每个类都有一个隐式定义的接口,包含所有类和实例成员。Java里面接口就是接口,与Dart不同
class Europe implements Continent {@overrideString name;@overridevoid desp() {print("This is Continent ${getContinentName()}");}@overrideString getContinentName() {return "Europe";}
}
泛型跟Java也基本上差不多
class Utils<T, R> {T valueA;R valueB;
}
扩展类(mixins)
mixins
的中文意思是混入,就是在类中混入其他功能。它是一种在多个类层次结构中重用类代码的方法。mixins
要重用的代码,不是方法或者是接口,而是类!
mixins要用到的关键字with
,with
关键字后面跟着一个或多个扩展类名
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();}
}
这里,应该这样描述Person
类:类Club
想使用类Sponsor
的sFunction()
方法,那么这时候就需要用到mixins
,而类Sponsor
就是mixins
类(混入类),类Club
就是要被mixins
的类。最后Person
继承这个Club
与Sponsor
mixins
后的类(Club with Sponsor
)
一个类可以mixins
多个mixins
类
class Company1 {void name() {print("Company1");}
}
class Company2 {void name() {print("Company2");}
}
这里我们将Company1
与Company2
一起混入
Company1
与Company2
同时有相同名称的函数类型,那么在这种情况下应该选择哪一个呢
class SeniorExecutive with Company1, Company2 {void showValue() {name();}
}
打印出来的是Company2
,相当于(SeniorExecutive with Company1) with Company2
再修改一下代码,把继承也加进去
如果继承的NaturalPerson
与mixins
的Company1
同时有相同名称的函数类型,那么在这种情况下应该选择哪一个呢
class NaturalPerson {void name() {print("NaturalPerson");}
}
class LegalPerson extends NaturalPerson with Company1 {void showValue() {name();}
}
打印出来的是Company1
,相当于LegalPerson extends (NaturalPerson with Company1)
最后一种情况是,如果被mixin
的类NaturalPerson2与mixins的Company1、Company2同时有相同名称的函数类型,那么在这种情况下应该选择哪一个呢
class NaturalPerson2 with Company1, Company2 {void name() {print("NaturalPerson");}void showValue() {name();}
}
打印出来的是NaturalPerson,相当于LegalPerson extends (NaturalPerson with Company1)
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到现在才被添加进来执行
- 至此当前所有事件执行完成
如果文字部分解释还不满足你,我们来看图
至此,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.dartpart of 'PartLibrary.dart';void part2LibraryFunction() {print("Part2LibraryFunction");
}
// PartLibrary.dartpart '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 语法要点汇总
Flutter mixins 探究
给Java开发者的Flutter开发基础---Dart语言相关推荐
- Flutter开发准备工作dart语言
1.装flutter sdk,下载对应版本安装即可,配对应环境变量: 2.flutter如果不带dart sdk的话,需要下载dart sdk,配对应环境变量: 3.安装Android studio, ...
- flutter基础 dart语言学习笔记
1.JIT(Just-In-Time 动态编译) 即时编译为什么能大幅度提升性能 JIT,即Just-in-time,动态(即时)编译,边运行边编译: https://book.flutterchin ...
- 作为一名Java开发者应该掌握的基础知识汇总!
Java语言作为热门编程语言之一,受到了更多的欢迎.今天小千就为大家介绍一下作为一名Java开发者应该掌握的基础知识. 一.修饰符 java语言中提供了一些修饰符,这些修饰符可以修饰类,变量和方法. ...
- Java开发者需要掌握的基础知识
Java语言作为热门编程语言之一,受到了更多的欢迎.今天小千就为大家介绍一下作为一名Java开发者应该掌握的基础知识. 一.修饰符 java语言中提供了一些修饰符,这些修饰符可以修饰类,变量和方法.以 ...
- Flutter获取随机数 Dart语言核心基础
也许你迷茫,但是我想说,在你迷茫的同时,保持本心,过好今天就好. 学习Dart语言,首先我们需要使用到一个语言调试工具 DartPad 在 Dart 中,dart:math 类库提供了 数学常数和函数 ...
- Flutter学习之Dart语言注释
文章目录 1.单行注释 2.多行注释 3.文档注释 高效Dart注释 注释 注释句子化 避免使用块注释 文档注释 使用 /// 注释成员和类型 优先为public的接口编写注释 考虑写一个库级别的文档 ...
- 算数运算符与关系运算符_【Flutter 110】Flutter手把手教程Dart语言——运算符
运算符 运算符是一种告诉编译器执行特定的数学或逻辑操作的符号.Dart语言内置了丰富的运算符,并提供了以下类型的运算符:「算术运算符.关系运算符.类型判断运算符.赋值运算符.逻辑运算符.按位和移位运算 ...
- Flutter开发之Dart语言基础
Flutter 发展历史 Flutter是Google开发的一款用于帮助开发者在iOS和Android两个平台构建高质量原生应用的全新移动UI框架.说到Flutter,很多同学可能会将它和下面的几个词 ...
- Java开发者文档(开发软件+规范)
java-developer-document 文档地址:https://gitee.com/zhengqingya/java-developer-document 一.开发环境 Java开发环境系列 ...
最新文章
- [转]使用jQuery.ajax傳送物件陣列給ASP.NET MVC
- 计算机操作系统:处理机的调度
- 【发布】温度监测报警器v1.3a稳定版!
- .NET 6新特性试用 | TryGetNonEnumeratedCount
- 云原生的新思考,为什么容器已经无处不在了
- mybatis if test 判断参数_什么?你还在if判断参数?Spring Boot 注解进行参数校验真香...
- openresty 操作memcached例子
- 从源码解析LinkedList集合
- 从键盘输入二叉树怎么输入_手机输入法派别之争!九宫格和全键盘谁才是正统...
- 旧式电话机的高压振铃电路图
- Java的第一个你好世界
- 权限管理实现——权限过滤器
- Ubuntu16.04中修复Pycharm问号图标问题
- JavaScript格式化字符串为指定长度
- 熊猫压缩怎么使用_记录随时间变化的PagerDuty事件(使用熊猫)
- 声音在计算机内表现形式为,声音与视频信息在计算机内的表现形式是什么
- tilemap 导入unity_Unity2019基础教程:TileMap搭建像素画场景关卡
- 微信vue路由跳转兼容_Vue微信公众号开发踩坑记录
- 解决联想拯救者Y9000X触控板失灵问题
- 1.2帮助软件Rstudio的下载与安装
热门文章
- Win7 的70个使用技巧
- python数据模型和算法_万字案例 | 用Python建立客户流失预测模型(含源数据+代码)...
- servlet+javabean+jdbc+mysql基于MVC模式的课件管理系统,有三个表的增删改查和课件搜索、课件上传、课件下载功能, 具体功能请看界面上的导航条
- afudos备份bios不动_救命宝典:BIOS刷坏后的恢复方法
- 论文摘要部分如何撰写
- linux实用的磁盘大文件及大文件夹查找命令
- 全栈必备的技术栈设想
- 清华计算机专业考研经验分享
- [项目管理-19]:在项目管理中, 如何用Jira对项目管理中的所有活动进行结构化、数字化和量化?
- 新浪sina.cn邮箱注册python版