给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、其他类型-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);

给Java开发者的Flutter开发基础---Dart语言

这里补充一个不相干的知识点。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");
给Java开发者的Flutter开发基础---Dart语言

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

var int2 = 7 ~/ 3;
print("int2:$int2");
给Java开发者的Flutter开发基础---Dart语言
  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");
给Java开发者的Flutter开发基础---Dart语言
  1. ??运算符。如果前者不为空,返回前者;否则返回后面的
int int10;
int int11 = int10 ?? 11;
print("int11 = ${int11}");
给Java开发者的Flutter开发基础---Dart语言
  1. ?. 。类似于Kotlin的非空判断
int a10;
print(a10?.toString());
a10 = 10;
print(a10?.toString());
给Java开发者的Flutter开发基础---Dart语言
  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);

将该类移至到其他包下


给Java开发者的Flutter开发基础---Dart语言
//  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开发者的Flutter开发基础---Dart语言

可选参数
这个概念在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";
给Java开发者的Flutter开发基础---Dart语言

在初始化固定长度后的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");
给Java开发者的Flutter开发基础---Dart语言

利用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) {}

  @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";
  }
}

泛型跟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());
}
给Java开发者的Flutter开发基础---Dart语言

使用了await之后,虽然delayedFunc1延迟3s执行,delayedFunc2延迟2s执行,但是依然是FIFO的顺序

有没有想过如果不放在异步函数里面会有什么效果?我们将其改成这样

void sequence() {
  print(DateTime.now());
  delayedFunc1();
  print(DateTime.now());
  delayedFunc2();
  print(DateTime.now());
}

来看看结果。sequence函数很快就执行完了。似乎我们理解了其中的含义:函数本身并不是一个异步操作,若不加await则不会等待当前函数执行完成后再执行下一个

给Java开发者的Flutter开发基础---Dart语言


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

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

给Java开发者的Flutter开发基础---Dart语言

我们来分析一下

  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. 至此当前所有事件执行完成

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

给Java开发者的Flutter开发基础---Dart语言
在执行Event之前,Main与Microtask依次执行完毕
给Java开发者的Flutter开发基础---Dart语言
Future2执行完之后,执行Microtask。Microtask插队了
给Java开发者的Flutter开发基础---Dart语言
Future3执行
给Java开发者的Flutter开发基础---Dart语言
Future4执行
给Java开发者的Flutter开发基础---Dart语言
最终剩余事件依次执行

至此,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");
  });
});

看看结果,此时后订阅的数据就丢失了。

给Java开发者的Flutter开发基础---Dart语言

你也可以选择自定义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.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,所有导入的文件(图片等)还有版本库都在里面管理

给Java开发者的Flutter开发基础---Dart语言

这里有我们项目的一些信息,Dart SDK的版本,第三方的依赖包版本

给Java开发者的Flutter开发基础---Dart语言

第三方库一般在dartlang里寻找
比如我要找dio这个网络请求框架,我可以搜索它

给Java开发者的Flutter开发基础---Dart语言

可以在里面看到它目前的版本号是1.0.12,要使用它的话我们可以在dependencies里添加描述

dependencies:
  dio: '1.0.12'

有时候为了让Dart自己寻找最适合我们项目的版本,你可以写上any。这个在包版本冲突上算是比较好的解决方案

dependencies:
  dio: any

最后在右上角点击Get dependencies导入

给Java开发者的Flutter开发基础---Dart语言

你可以在pubspec.lock文件里查看到项目使用的某个库的版本

给Java开发者的Flutter开发基础---Dart语言

同样我们可以对包版本范围进行限制:指定一个最小和最大的版本号
这表示在2.x.x版本都是支持的,但是必须要大于2.1.0

'>=2.1.0 <3.0.0'

还有一种是指定最小版本,比其大的都支持

english_words : ^3.0.0

包下载成功会有如下显示

给Java开发者的Flutter开发基础---Dart语言

使用很简单,导入即可

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 探究