Dart基础

Hello World

Dart的入口也是main函数:

1
2
3
main(List<String> args) {
print("Hello World");
}

变量声明

显式变量声明的方式如下:

1
2
3
//变量类型 变量名称 = 赋值;
String name = "dart fans";
int age = 20;

另外,也可以省略变量类型,由Dart进行类型推断(类似swift)。

1
var/dynamic/const/final 变量名称 = 赋值;

Dart本身是一个强类型语言,任何变量都是有确定类型的,声明变量后,只可以修改变量值,不能修改变量类型。

var

使用示例:

1
2
3
4
var language = "Dart";

//runtimeType用于获取变量当前的类型
print(language.runtimeType); //String

dynamic和Object

dynamic与var一样都是关键词,声明的变量可以赋值任意对象,功能类似于Objective-C中的id类型和Swift中的Any

1
2
3
4
5
dynamic level = 28;
print(level.runtimeType); //int

level = "top";
print(level.runtimeType); //String

Object 是Dart所有对象的根基类,也就是说所有类型都是Object的子类(包括Function和Null),所以任何类型的数据都可以赋值给Object声明的对象。

dynamic与var一样都是关键词,声明的变量可以赋值任意对象。 而dynamic与Object相同之处在于,他们声明的变量可以在后期改变赋值类型。

dynamic与Object不同的是,dynamic声明的对象编译器会提供所有可能的组合, 而Object声明的对象只能使用Object的属性与方法, 否则编译器会报错。

final和const

final和const用于定义常量,定义之后的值都不可修改。使用final或者const修饰的变量,变量类型可以省略。

两者区别在于:

  • const为编译时常量,在编译期间就必须确认赋值;
  • final支持运行时赋值,可以在第一次使用时进行初始化;

数据类型

数字类型

Dart中的int和double可表示的范围并不是固定的,它取决于运行Dart的平台。

字符串和数字之间的转化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1.字符串转数字
var one = int.parse('111');
var two = double.parse('12.22');
print('${one} ${one.runtimeType}'); // 111 int
print('${two} ${two.runtimeType}'); // 12.22 double

// 2.数字转字符串
var num1 = 123;
var num2 = 123.456;
var num1Str = num1.toString();
var num2Str = num2.toString();
var num2StrD = num2.toStringAsFixed(2); // 保留两位小数
print('${num1Str} ${num1Str.runtimeType}'); // 123 String
print('${num2Str} ${num2Str.runtimeType}'); // 123.456 String
print('${num2StrD} ${num2StrD.runtimeType}'); // 123.46 String

布尔类型

Dart提供了一个bool的类型, 取值为true和false。要注意的是,Dart中不能判断非0即真, 或者非空即真。

1
2
3
4
5
var message = 'Hello Dart';
// 错误的写法
if (message) {
print(message)
}

字符串类型

Dart字符串是UTF-16编码单元的序列。可以使用单引号或双引号创建一个字符串, 可以使用三个单引号或者双引号表示多行字符串:

1
2
3
4
5
6
7
var s1 = 'Hello World';
var s2 = "Hello Dart";
var s3 = '''
哈哈哈
呵呵呵
嘿嘿嘿
''';

集合类型

对于集合类型,Dart则内置了最常用的三种:List / Set / Map。

List使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1.使用类型推导定义
var letters = ['a', 'b', 'c', 'd'];
print('$letters ${letters.runtimeType}'); //[a, b, c, d] List<String>
print(letters.length); //4

// 2.明确指定类型
List<int> numbers = [1, 2, 3, 4];
print('$numbers ${numbers.runtimeType}'); //[1, 2, 3, 4] List<int>
print(numbers.length); //4

// 3.常用操作
numbers.add(5); //[1, 2, 3, 4, 5]
numbers.remove(1); //[2, 3, 4, 5]
numbers.removeAt(1); //2, 4, 5]
numbers.contains(2); //true

Set使用:

1
2
3
4
5
6
7
8
// 1.使用类型推导定义
var lettersSet = {'a', 'b', 'c', 'd'};
print('$lettersSet ${lettersSet.runtimeType}'); //{a, b, c, d} _CompactLinkedHashSet<String>


// 2.明确指定类型
Set<int> numbersSet = {1, 2, 3, 4};
print('$numbersSet ${numbersSet.runtimeType}'); //{1, 2, 3, 4} _CompactLinkedHashSet<int>

Map使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1.使用类型推导定义
var infoMap1 = {'name': 'why', 'age': 18};
print('$infoMap1 ${infoMap1.runtimeType}');

// 2.明确指定类型
Map<String, Object> infoMap2 = {'height': 1.88, 'address': '北京市'};
print('$infoMap2 ${infoMap2.runtimeType}');

// 3.Map的操作
// 根据key获取value
print(infoMap1['name']); // why
// 获取所有的keys
print('${infoMap1.keys} ${infoMap1.keys.runtimeType}'); // (name, age) _CompactIterable<String>
// 获取所有的values
print('${infoMap1.values} ${infoMap1.values.runtimeType}'); // (why, 18) _CompactIterable<Object>
// 判断是否包含某个key或者value
print('${infoMap1.containsKey('age')} ${infoMap1.containsValue(18)}'); // true true
// 根据key删除元素
infoMap1.remove('age');
print('${infoMap1}'); // {name: why}

函数

Dart是一种真正的面向对象的语言,所以即使是函数也是对象,并且有一个类型Function。这意味着函数可以赋值给变量或作为参数传递给其他函数,这是函数式编程的典型特征。

函数的定义:

1
2
3
4
返回值 函数的名称(参数列表) {
函数体
return 返回值
}

Dart函数声明如果没有显式声明返回值类型时会默认当做dynamic处理,并不会进行类型推断。

对于只包含一个表达式的函数,可以使用简写语法:

1
sum(num1, num2) => num1 + num2;

Dart中没有函数重载。

函数的可选参数

函数的可选参数分为可选的位置参数可选的命名参数。但是两者不能同时使用。

可选的位置参数

包装一组函数参数,用[]标记为可选的位置参数,并放在参数列表的最后面:

1
2
3
4
5
6
7
8
9
// 定义
printInfo2(String name, [int age, double height]) {
print('name=$name age=$age height=$height');
}

// 调用
printInfo2('why'); // name=why age=null height=null
printInfo2('why', 18); // name=why age=18 height=null
printInfo2('why', 18, 1.88); // name=why age=18 height=1.88

可选的命名参数

定义函数时,使用{param1, param2, …},放在参数列表的最后面,用于指定命名参数。可选命名参数在Flutter中使用非常多。例如:

1
2
3
4
5
6
7
8
9
10
// 定义
printInfo1(String name, {int age, double height}) {
print('name=$name age=$age height=$height');
}

// 调用
printInfo1('why'); // name=why age=null height=null
printInfo1('why', age: 18); // name=why age=18 height=null
printInfo1('why', age: 18, height: 1.88); // name=why age=18 height=1.88
printInfo1('why', height: 1.88); // name=why age=null height=1.88

可选参数的默认值

只有可选参数才可以设置默认值。

1
2
3
printInfo1(String name, {int age = 18, double height = 1.88}) {
print('name=$name age=$age height=$height');
}

Frist class citizen

函数在Dart中属于一等公民,这就是说可以将函数赋值给一个变量,也可以将函数作为另外一个函数的参数或者返回值来使用。

函数作为变量使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void say(String msg) {
print("hello ${msg}");
}

var func1 = say;
func1("dart1");

var func2 = (msg){
print("hello $msg");
};
func2("dart2");

var func3 = (msg)=>print("hello $msg");
func3("dart3");

函数作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int add(int num1, int num2) {
return num1 + num2;
}

int subtract(int num1, int num2) {
return num1 - num2;
}


typedef Calculate = int Function(int num1, int num2);

int calculate(int num1, int num2, Calculate cal) {
return cal(num1, num2);
}


var ret1 = calculate(20, 10, add);
print(ret1); //30

var ret2 = calculate(10, 20, subtract);
print(ret2); //-10

var ret3 = calculate(10, 20, (num1, num2) => num1 * num2);
print(ret3); //200

var ret4 = calculate(20, 10, (num1, num2){
return num1 ~/ num2;
});
print(ret4); //2

函数作为返回值

1
2
3
Function getFunc() {
return add;
}

运算符

除法与取模运算符

1
2
3
4
var num = 7;
print(num / 3); // 除法操作, 结果2.3333..
print(num ~/ 3); // 整除操作, 结果2;
print(num % 3); // 取模操作, 结果1;

赋值运算符??=

规则:

  • 当变量为null时,使用后面的内容进行赋值
  • 当变量有值时,使用自己原来的值
1
2
3
4
5
6
7
8
main(List<String> args) {
var name1 = 'coderwhy';
print(name1);
// var name2 = 'kobe';
var name2 = null;
name2 ??= 'james';
print(name2); // 当name2初始化为kobe时,结果为kobe,当初始化为null时,赋值了james
}

条件运算符??

expr1 ?? expr2,规则:

  • 如果expr1是null,则返回expr2的结果
  • 如果expr1不是null,直接使用expr1的结果
1
2
3
4
var temp = 'why';
var temp = null;
var name = temp ?? 'kobe';
print(name);

级联运算符

可以使用级联运算符..对一个对象进行连续操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Person {
String? name;

void run() {
print("${name} is running");
}

void eat() {
print("${name} is eating");
}

void swim() {
print("${name} is swimming");
}
}

main(List<String> args) {
final p1 = Person();
p1.name = 'why';
p1.run();
p1.eat();
p1.swim();

final p2 = Person()
..name = "why"
..run()
..eat()
..swim();
}

流程控制

if,for,swich语法都与其他语言一致。

类和对象

类的定义

使用class关键字定义一个类:

1
2
3
4
5
6
class 类名 {
类型 成员名;
返回值类型 方法名(参数列表) {
方法体
}
}

可以在成员变量命名前加上下划线_表明该成员变量为私有变量。

构造方法

普通构造方法

  • 当类中没有明确指定构造方法时,将默认拥有一个无参的构造方法
  • 自定义构造方法后,默认的构造方法将会失效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
String? name;
int? age;

Person(String name, int age) {
this.name = name;
this.age = age;
}

@override
String toString() {
return 'name=$name age=$age';
}
}

上面的构造方法也可以简写:

1
2
3
4
5
6
Person(String name, int age) {
this.name = name;
this.age = age;
}
// 等同于
Person(this.name, this.age);

命名构造方法

由于不支持方法(函数)的重载,所以没办法创建相同名称的构造方法, 此时可以使用命名构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person {
String?name;
int?age;

Person() {
name = '';
age = 0;
}
// 命名构造方法
Person.withArgments(String name, int age) {
this.name = name;
this.age = age;
}

@override
String toString() {
return 'name=$name age=$age';
}
}

// 创建对象
var p1 = new Person();
print(p1);
var p2 = new Person.withArgments('why', 18);
print(p2);

初始化列表

下面这种初始化变量的方法, 称为初始化列表(Initializer list):

1
2
3
4
5
6
7
8
9
10
11
12
13
class Point {
final num x;
final num y;
final num distance;

// 错误写法
// Point(this.x, this.y) {
// distance = sqrt(x * x + y * y);
// }

// 正确的写法
Point(this.x, this.y) : distance = sqrt(x * x + y * y);
}

重定向构造方法

重定向构造方法是指在一个构造方法中去调用另外一个构造方法:

1
2
3
4
5
6
7
class Person {
String? name;
int? age;

Person(this.name, this.age);
Person.fromName(String name) : this(name, 0);
}

常量构造方法

默认情况下,创建对象时,即使传入相同的参数,创建出来的也不是同一个对象。

在某些情况下,传入相同值时,我们希望返回同一个对象,这个时候,可以使用常量构造方法。拥有常量构造方法的类中,所有的成员变量必须是final修饰的。

1
2
3
4
5
6
7
8
9
10
11
main(List<String> args) {
var p1 = const Person('why');
var p2 = const Person('why');
print(identical(p1, p2)); // true
}

class Person {
final String name;

const Person(this.name);
}

工厂构造方法

Dart提供了factory关键字, 用于通过工厂去获取对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main(List<String> args) {
var p1 = Person('why');
var p2 = Person('why');
print(identical(p1, p2)); // true
}

class Person {
String? name;

static final Map<String, Person> _cache = <String, Person>{};

factory Person(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final p = Person._internal(name);
_cache[name] = p;
return p;
}
}

Person._internal(this.name);
}

getter和setter

getter和setter可以用来监控类的属性被访问的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
main(List<String> args) {
final d = Dog("黄色");
d.setColor = "黑色";
print(d.getColor);
}

class Dog {
String? color;

String get getColor {
return color;
}
set setColor(String color) {
this.color = color;
}

Dog(this.color);
}

类的继承

Dart中的继承使用extends关键字,子类中使用super来访问父类。

父类中的所有成员变量和方法都会被继承,,但是构造方法除外。子类可以拥有自己的成员变量, 并且可以对父类的方法进行重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
main(List<String> args) {
var p = Person();
p.age = 18;
p.run();
print(p.age);
}

class Animal {
int? age;

run() {
print('在奔跑ing');
}
}


class Person extends Animal {
String? name;

@override
run() {
print('$name在奔跑ing');
}
}

子类中可以调用父类的构造方法,对某些属性进行初始化:

  • 子类的构造方法在执行前,将隐含调用父类的无参默认构造方法(没有参数且与类同名的构造方法)。
  • 如果父类没有无参默认构造方法,则子类的构造方法必须在初始化列表中通过super显式调用父类的某个构造方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Animal {
int age;

Animal(this.age);

run() {
print('在奔跑ing');
}
}

class Person extends Animal {
String name;

Person(String name, int age) : name=name, super(age);

@override
run() {
print('$name在奔跑ing');
}

@override
String toString() {
return 'name=$name, age=$age';
}
}

抽象类

使用abstract声明一个抽象类。抽象类不能实例化,抽象类中的抽象方法必须被子类实现, 抽象类中的已经被实现方法, 可以不被子类重写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
abstract class Shape {
getArea();
}

class Circle extends Shape {
double r;

Circle(this.r);

@override
getArea() {
return r * r * 3.14;
}
}

class Reactangle extends Shape {
double w;
double h;

Reactangle(this.w, this.h);

@override
getArea() {
return w * h;
}
}

隐式接口

Dart中没有一个专门的关键字来声明接口。Dart不支持多继承,可以使用抽象类实现接口的功能。当将一个类当做接口使用时, 那么实现(implements)这个接口的类, 必须实现这个接口中所有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class Runner {
run();
}

abstract class Flyer {
fly();
}

class SuperMan implements Runner, Flyer {
@override
run() {
print('超人在奔跑');
}

@override
fly() {
print('超人在飞');
}
}

Mixin混入

在通过implements实现某个类时,类中所有的方法都必须被重新实现(无论这个类原来是否已经实现过该方法)。

要想服用之前类的实现,可以使用Mixin混入方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
main(List<String> args) {
var superMan = SuperMain();
superMan.run();
superMan.fly();
}

mixin Runner {
run() {
print('在奔跑');
}
}

mixin Flyer {
fly() {
print('在飞翔');
}
}

// implements的方式要求必须对其中的方法进行重新实现
// class SuperMan implements Runner, Flyer {}

class SuperMain with Runner, Flyer {

}

类成员和方法

使用static关键字来定义类成员和方法。

枚举类型

使用enum关键字来进行定义,枚举类型中有两个比较常见的属性:

  • index: 用于表示每个枚举常量的索引, 从0开始
  • values: 包含每个枚举值的List
1
2
3
4
5
6
7
8
9
10
11
main(List<String> args) {
print(Colors.red.index); //0
print(Colors.values); //[Colors.red, Colors.green, Colors.blue]

}

enum Colors {
red,
green,
blue
}

泛型

List和Map

List和Map中泛型的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建List的方式
var names1 = ['why', 'kobe', 'james', 111];
print(names1.runtimeType); // List<Object>

// 限制类型
var names2 = <String>['why', 'kobe', 'james', 111]; // 最后一个报错
List<String> names3 = ['why', 'kobe', 'james', 111]; // 最后一个报错


// 创建Map的方式
var infos1 = {1: 'one', 'name': 'why', 'age': 18};
print(infos1.runtimeType); // _InternalLinkedHashMap<Object, Object>

// 对类型进行显示
Map<String, String> infos2 = {'name': 'why', 'age': 18}; // 18不能放在value中
var infos3 = <String, String>{'name': 'why', 'age': 18}; // 18不能放在value中

类的泛型

定义一个泛型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
main(List<String> args) {
Location l2 = Location<int>(10, 20);
print(l2.x.runtimeType); // int

Location l3 = Location<String>('aaa', 'bbb');
print(l3.x.runtimeType); // String
}
}

class Location<T> {
T x;
T y;

Location(this.x, this.y);
}

还可以限制T只能是指定类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
main(List<String> args) {
Location l2 = Location<int>(10, 20);
print(l2.x.runtimeType);

// 错误的写法, 类型必须继承自num
Location l3 = Location<String>('aaa', 'bbb');
print(l3.x.runtimeType);
}

class Location<T extends num> {
T x;
T y;

Location(this.x, this.y);
}

方法的泛型

1
2
3
4
5
6
7
8
9
main(List<String> args) {
var names = ['why', 'kobe'];
var first = getFirst(names);
print('$first ${first.runtimeType}'); // why String
}

T getFirst<T>(List<T> ts) {
return ts[0];
}

库的使用

Dart中任何一个dart文件都是一个库,即使你没有用关键字library声明。

库的导入

使用import导入一个库:

1
import '库所在的uri';

导入系统库:

1
import 'dart:io';

导入项目中其他库:

1
2
//当然,你也可以用相对路径或绝对路径的dart文件来引用
import 'lib/student/student.dart';

导入Pub包管理工具管理的一些库,包括自己的配置以及一些第三方的库,通常使用前缀package:

1
import 'package:flutter/material.dart';

库命名冲突

当各个库有命名冲突的时候,可以使用as关键字来使用命名空间:

1
2
3
import 'lib/student/student.dart' as Stu;

Stu.Student s = Stu.Student();

库文件的显示和隐藏

如果希望只导入库中某些内容,或者刻意隐藏库里面某些内容,可以使用showhide关键字:

1
2
3
// 屏蔽其他
import 'lib/student/student.dart' show Student, Person;
import 'lib/student/student.dart' hide Person;

库的定义

使用关键字library定义一个库。当一个库包含多个文件时,可以使用关键字export进行管理:

1
2
3
4
5
6
7
// 定义:
library utils;
export "mathUtils.dart";
export "dateUtils.dart";

// 使用:
import "lib/utils.dart";

异步支持

Dart类库有非常多的返回Future或者Stream对象的函数。 这些函数被称为异步函数:它们只会在设置好一些耗时操作之后返回,比如像 IO操作。而不是等到这个操作完成。

asyncawait关键词支持了异步编程,允许写出和同步代码很像的异步代码。

Future

Future表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future只会对应一个结果,要么成功,要么失败。

Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用。

Future.then

使用Future.delayed创建了一个延时任务,即2秒后返回结果字符串”hi world!”,然后我们在then中接收异步结果:

1
2
3
4
5
Future.delayed(new Duration(seconds: 2),(){
return "hi world!";
}).then((data){
print(data);
});

Future.catchError

如果异步任务发生错误,可以在catchError中捕获错误,将上面示例改为:

1
2
3
4
5
6
7
8
9
10
Future.delayed(new Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print("success");
}).catchError((e){
//执行失败会走到这里
print(e);
});

then方法还有一个可选参数onError,我们也可以它来捕获异常:

1
2
3
4
5
6
7
8
Future.delayed(new Duration(seconds: 2), () {
//return "hi world!";
throw AssertionError("Error");
}).then((data) {
print("success");
}, onError: (e) {
print(e);
});

Future.whenComplete

whenComplete中的代码无论成功或失败都会被执行:

1
2
3
4
5
6
7
8
9
10
11
12
Future.delayed(new Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print(data);
}).catchError((e){
//执行失败会走到这里
print(e);
}).whenComplete((){
//无论成功或失败都会走到这里
});

Future.wait

等待多个异步任务都执行结束后才进行一些操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Future.wait([
// 2秒后返回结果
Future.delayed(new Duration(seconds: 2), () {
return "hello";
}),
// 4秒后返回结果
Future.delayed(new Duration(seconds: 4), () {
return " world";
})
]).then((results){
print(results[0]+results[1]);
}).catchError((e){
print(e);
});

Async/await

如果代码中有大量异步逻辑,并且出现大量异步任务依赖其它异步任务的结果时,必然会出现Future.then回调中套回调情况。

async用来表示函数是异步的,定义的函数会返回一个Future对象,可以使用then方法添加回调函数。

await后面是一个Future,表示等待该异步任务完成,异步完成后才会往下走;await必须出现在async函数内部。

1
2
3
4
5
6
7
Future<String> getNetworkData() async {
var result = await Future.delayed(Duration(seconds: 3), () {
return "network data";
});

return "请求到的数据:" + result;
}