接近半年没有在简书冒泡了。这段时间一是忙于使用云信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、其他类型-RunesSymbols。而数值型仅有intdouble,不像其他语言分的很细

num num1 = 1;
num num2 = 1.1;
int int1 = 1;
double double1 = 1.1;

如果变量使用num进行声明,则可以随意在使用中转换为intdouble类型;但如果使用int或者double进行明确的声明,那么就不能随意转换了

num1 = 1.1;
num2 = 1;
//  int1 = 1.1;  错误

除了以上所说的数据类型,Dart还有vardynamicconst三种数据类型
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中有两种数据常量数据类型,constfinal。与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类型的对象,Stringdoubleintbool的父类都是Object

print("Hello World");
print(3);
print(1.1);
print(true);
print(null);

Dart中操作符大体上与其他语言差不多,这里只介绍一些不常见的操作符

  1. ~/。Java里面如果int/int的话,得到的也是int,这就是通常所说的取整。如果要得到浮点型数值的结果,则需要将其中一个数值变成浮点型数值才行,但是Dart不需要这样
var double2 = 7 / 3;
print("double2:$double2");

有什么办法可以得到int呢?那就需要用到~/

var int2 = 7 ~/ 3;
print("int2:$int2");
  1. as强转操作符。这个跟Kotlin是一样的。这边要是转换格式不匹配,则会报错
//  int int8 = num1 as int;  运行时报错
double double3 = num1 as double;

所以在使用之前最好判断一下

if (num1 is int) {}
  1. ??=。空赋值操作符
int int9;
int9 ??= 11;
print("int9:$int9");
  1. ??运算符。如果前者不为空,返回前者;否则返回后面的
int int10;
int int11 = int10 ?? 11;
print("int11 = ${int11}");
  1. ?. 。类似于Kotlin的非空判断
int a10;
print(a10?.toString());
a10 = 10;
print(a10?.toString());
  1. 级联符号.. 允许您在同一个对象上进行一系列操作。 除了函数调用之外,还可以访问同一对象上的字段。其实相当于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());
}

oncatch的区别在于是否关心异常的实例


4. 字符串

在任何一门语言中,字符串都是被单独拿出来的小重点,因为他启到一个承上启下的作用,后面我们将开始接触到类与函数的概念。刚才我们知道Dart中Stringint同属对象,虽然情况与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");
});

ListSetMap有一些通用的函数。其中的一些通用函数都是来自于类IterableListSetIterable类的实现。虽然Map没有实现Iterable, 但是Map的属性keysvalues都是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;}
}

gettersetter是特殊的函数,可以读写访问对象的属性,每个实例变量都有一个隐式的getter,适当的加上一个setter,可以通过实现gettersetter创建附加属性

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要用到的关键字withwith关键字后面跟着一个或多个扩展类名

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想使用类SponsorsFunction()方法,那么这时候就需要用到mixins,而类Sponsor就是mixins类(混入类),类Club就是要被mixins的类。最后Person继承这个ClubSponsor mixins后的类(Club with Sponsor)

一个类可以mixins多个mixins

class Company1 {void name() {print("Company1");}
}
class Company2 {void name() {print("Company2");}
}

这里我们将Company1Company2一起混入
Company1Company2同时有相同名称的函数类型,那么在这种情况下应该选择哪一个呢

class SeniorExecutive with Company1, Company2 {void showValue() {name();}
}

打印出来的是Company2,相当于(SeniorExecutive with Company1) with Company2

再修改一下代码,把继承也加进去
如果继承的NaturalPersonmixinsCompany1同时有相同名称的函数类型,那么在这种情况下应该选择哪一个呢

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会报错
因此,整个流程只需要记住两点

  1. await关键字必须在async函数内部使用
  2. 调用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 queuemicrotask 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等事件

让我们以一个实际例子来了解一下microtaskevent的流程。初看这个很感觉复杂,如果你第一遍不能理解,请再理解一遍

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');
}

如果你的结果与此图一致,恭喜你,你完全搞懂了这个流程

我们来分析一下

  1. 首先执行主线程,其次执行Microtask,最后才是Event
  2. 开始Event,添加Future2、3、4。先来到2,按then顺序打印。2这里新建了一个Microtask 0和Future 2d。因为return的依然是当前Future,所以2c依然跟着当前Future打印,新建的那个Future 2d被添加到Event最末尾
  3. 当前Event中的Future 2执行完毕,来了一个插队Microtask 0。执行新建的Microtask 0。
  4. Microtask执行完毕,继续回到Event。继续顺序打印3,3b被添加到Event最末尾,因为return的是新Future,所以跟之前的流程不一样,此时3c与3d暂不存在
  5. 继续顺序打印4,4a被添加到Event最末尾
  6. 继续顺序打印5,至此回过头来再检查Event中有没有剩余未执行的事件
  7. 按照添加的顺序2d开始执行,然后是3b,此时又添加了新的Future 3c到最后,3b执行完毕之后是4a
  8. 这波执行完毕之后,执行3c,异步回调得到3d
  9. 之前延迟的delay到现在才被添加进来执行
  10. 至此当前所有事件执行完成

如果文字部分解释还不满足你,我们来看图

在执行Event之前,Main与Microtask依次执行完毕

Future2执行完之后,执行Microtask。Microtask插队了

Future3执行

Future4执行

最终剩余事件依次执行

至此,async和await学习完毕,下面还有另外一个挑战Stream


Stream是一个异步数据源,它是Dart中处理异步事件流的统一API
集合可以理解为“拉”模式,比如你有一个List,你可以主动地通过迭代获得其中的每个元素,想要就能拿出来。而Stream可以理解为“推”模式,这些异步产生的事件或数据会推送给你(并不是你想要就能立刻拿到)。这种模式下,你要做的是用一个listener(即callback)做好数据接收的准备,数据可用时就通知你。
Stream有3个工厂构造函数:fromFuturefromIterableperiodic,分别可以通过一个FutureIterable或定时触发动作作为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对象设置onDataonDone的处理
下面的代码与前面的示例代码作用相同

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对象设置了onDataonDone的处理,这才有了订阅者
如果在发出事件的同时添加订阅者,那么要在订阅者在该事件发出后才会生效。如果订阅者取消了订阅,那么它会立即停止接收事件
上面一个例子最后会打印出1、3、5、7,9因为被cancel了所以不会打印

Stream有两种订阅模式:单订阅(single)和多订阅(broadcast)。单订阅就是只能有一个订阅者,而广播是可以有多个订阅者
Stream默认处于单订阅模式,所以同一个Stream上的listen和其它大多数函数只能调用一次,调用第二次就会报错。但Stream可以通过transform()函数(返回另一个Stream)进行连续调用。通过Stream.asBroadcastStream()可以将一个单订阅模式的Stream转换成一个多订阅模式的StreamisBroadcast属性可以判断当前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的异同
StreamFuture是Dart异步处理的核心API。Future只能表示一次异步获得的数据,而Stream表示多次异步获得的数据,比如界面上的按钮可能会被用户点击多次,所以按钮上的点击事件(onClick)就可有理解为一个Stream
Stream是流式处理,比如IO处理的时候,一般情况是每次只会读取一部分数据(具体取决于实现)。这和一次性读取整个文件的内容相比,Stream的好处是处理过程中内存占用较小
来对比分别使用StreamFuture实现读文件的两种写法

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';

导入项目为DartDemolib目录下的包

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.dartPartLibrary.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语言相关推荐

  1. Flutter开发准备工作dart语言

    1.装flutter sdk,下载对应版本安装即可,配对应环境变量: 2.flutter如果不带dart sdk的话,需要下载dart sdk,配对应环境变量: 3.安装Android studio, ...

  2. flutter基础 dart语言学习笔记

    1.JIT(Just-In-Time 动态编译) 即时编译为什么能大幅度提升性能 JIT,即Just-in-time,动态(即时)编译,边运行边编译: https://book.flutterchin ...

  3. 作为一名Java开发者应该掌握的基础知识汇总!

    Java语言作为热门编程语言之一,受到了更多的欢迎.今天小千就为大家介绍一下作为一名Java开发者应该掌握的基础知识. 一.修饰符 java语言中提供了一些修饰符,这些修饰符可以修饰类,变量和方法. ...

  4. Java开发者需要掌握的基础知识

    Java语言作为热门编程语言之一,受到了更多的欢迎.今天小千就为大家介绍一下作为一名Java开发者应该掌握的基础知识. 一.修饰符 java语言中提供了一些修饰符,这些修饰符可以修饰类,变量和方法.以 ...

  5. Flutter获取随机数 Dart语言核心基础

    也许你迷茫,但是我想说,在你迷茫的同时,保持本心,过好今天就好. 学习Dart语言,首先我们需要使用到一个语言调试工具 DartPad 在 Dart 中,dart:math 类库提供了 数学常数和函数 ...

  6. Flutter学习之Dart语言注释

    文章目录 1.单行注释 2.多行注释 3.文档注释 高效Dart注释 注释 注释句子化 避免使用块注释 文档注释 使用 /// 注释成员和类型 优先为public的接口编写注释 考虑写一个库级别的文档 ...

  7. 算数运算符与关系运算符_【Flutter 110】Flutter手把手教程Dart语言——运算符

    运算符 运算符是一种告诉编译器执行特定的数学或逻辑操作的符号.Dart语言内置了丰富的运算符,并提供了以下类型的运算符:「算术运算符.关系运算符.类型判断运算符.赋值运算符.逻辑运算符.按位和移位运算 ...

  8. Flutter开发之Dart语言基础

    Flutter 发展历史 Flutter是Google开发的一款用于帮助开发者在iOS和Android两个平台构建高质量原生应用的全新移动UI框架.说到Flutter,很多同学可能会将它和下面的几个词 ...

  9. Java开发者文档(开发软件+规范)

    java-developer-document 文档地址:https://gitee.com/zhengqingya/java-developer-document 一.开发环境 Java开发环境系列 ...

最新文章

  1. [转]使用jQuery.ajax傳送物件陣列給ASP.NET MVC
  2. 计算机操作系统:处理机的调度
  3. 【发布】温度监测报警器v1.3a稳定版!
  4. .NET 6新特性试用 | TryGetNonEnumeratedCount
  5. 云原生的新思考,为什么容器已经无处不在了
  6. mybatis if test 判断参数_什么?你还在if判断参数?Spring Boot 注解进行参数校验真香...
  7. openresty 操作memcached例子
  8. 从源码解析LinkedList集合
  9. 从键盘输入二叉树怎么输入_手机输入法派别之争!九宫格和全键盘谁才是正统...
  10. 旧式电话机的高压振铃电路图
  11. Java的第一个你好世界
  12. 权限管理实现——权限过滤器
  13. Ubuntu16.04中修复Pycharm问号图标问题
  14. JavaScript格式化字符串为指定长度
  15. 熊猫压缩怎么使用_记录随时间变化的PagerDuty事件(使用熊猫)
  16. 声音在计算机内表现形式为,声音与视频信息在计算机内的表现形式是什么
  17. tilemap 导入unity_Unity2019基础教程:TileMap搭建像素画场景关卡
  18. 微信vue路由跳转兼容_Vue微信公众号开发踩坑记录
  19. 解决联想拯救者Y9000X触控板失灵问题
  20. 1.2帮助软件Rstudio的下载与安装

热门文章

  1. Win7 的70个使用技巧
  2. python数据模型和算法_万字案例 | 用Python建立客户流失预测模型(含源数据+代码)...
  3. servlet+javabean+jdbc+mysql基于MVC模式的课件管理系统,有三个表的增删改查和课件搜索、课件上传、课件下载功能, 具体功能请看界面上的导航条
  4. afudos备份bios不动_救命宝典:BIOS刷坏后的恢复方法
  5. 论文摘要部分如何撰写
  6. linux实用的磁盘大文件及大文件夹查找命令
  7. 全栈必备的技术栈设想
  8. 清华计算机专业考研经验分享
  9. [项目管理-19]:在项目管理中, 如何用Jira对项目管理中的所有活动进行结构化、数字化和量化?
  10. 新浪sina.cn邮箱注册python版