Dart介绍

Dart是由谷歌开发的计算机编程语言,它可以被用于web、服务器、移动应用和物联网等领域的开发。

Dart诞生于2011年,号称要取代JavaScript。但是过去的几年中一直不温不火。直到Flutter的出现现在被人们重新重视

要学Flutter的话我们必须首先得会Dart.

官网

Dart开发环境配置

安装Dart

在使用Dart开发之前,首先要配置Dart开发环境,我们在官网可以找到在windows和mac上安装Dart SDK的方法,也可以在dart for windows 中使用 .exe 文件进行安装

下载后,双击执行,选择好安装位置后,一路next,安装程序会默认把dart添加进环境变量的,输入dart --version 出现下图表示安装成功

vscode中配置开发Dart

  1. 在 vscode 中安装dart插件

  2. 在 vscode 中安装 code runner 来运行我们的代码

测试运行

新建一个 myfirst.dart 文件,输入以下代码,运行

1
2
3
main() {
print("yjr1100 hahahha ");
}

Dart 学习

基本语法

dartC 语言有点类似,每句话结束需要使用 ‘;’,注释也与 C 类似,使用 // /**/ 来进行注释

Dart 的入口方法

1
2
3
4
5
6
7
8
// dart的入口方法
main(){
print("yjr1100");
}
// 表示main方法没有返回值
void main(){
print("yjr1100");
}

Dart 中的变量

dart 是一个脚本类语言,可以不预先定义变量类型,自动会进行类型推断,这一点和 javascript 有点相似

1
2
var str = "this is var";
var num = 123;

也可以通过类型来申明变量

1
2
3
String str = 'this is var';

int num = 123;

注意:

  1. 变量名称必须由数字、字母、下划线、美元符($)组成
  2. 标识符开头不能是数字
  3. 标识符不能是保留字和关键字
  4. 变量的名字是区分大小写的
  5. 标识符一般要见名思意

Dart中的数据类型

  • 数值型:

    • int
    • double
  • 字符串:

    • String

      1
      2
      3
      4
      5
      6
      7
      String str1 = 'sdfsdf';
      String str2 = "sfsdfsf";
      // 可以用三个单引号来定义多行字符串
      String str3 = '''sdfsdfssdf
      sdfsdfsdf
      sdfsdf
      '''
    • 字符串的拼接

      1
      2
      3
      4
      String str1 = "yjr";
      String str2 = "1100";
      print("$str1$str2");
      print(str1 + str2);
  • 布尔:

    • bool
      1
      2
      bool flag = true;
      bool flag2 = false;
  • List:

    • 在Dart中,数组是列表对象,所以大多数人只称它们为列表
      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
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      // 定义List
      var list1 = ["sdf",123,true];
      print(list1);
      //获取集合的长度
      print(list1.length)
      // 通过索引可以获取数据
      print(list1[0]);
      print(list1[1]);
      print(list1[2]);

      // 定义指定类型的 List
      var list2 = <String>["sdf","kkk"];
      var list3 = <int>[12,34];

      // 定义空的 List,动态的向其中添加数据
      var list4 = [];
      list4.add("sdf");

      // 老版本的Dart还可以通过new的方式来创建list,不过新版本已经不用了
      var list5 = new List();

      // 创建一个固定长度的集合,不可增加数据,但是可以修改里面的数据,不可以修改长度
      var list6 = List.filled(3,"");
      var list6 = List<String>.filled(3,"");

      // 除了使用var,还可以用List 来声明数据类型
      List list7 = [12,34,56];

      // List 常用属性
      print(list7.length); // 返回list长度
      print(list7.reversed); // 翻转list,返回的不是list,用 toList()方法来转成list
      print(list7.isEmpty); // 判断list是否为空
      print(list7.isNotEmpty); // 判断list是否非空

      // List 常用方法
      list7.add(55); //增加一个数据
      list7.addAll([12,34,46,99]); //一次增加多个数据
      list7.indexOf(12); //查找索引,如果查不到返回-1,否则返回找到的第一个索引位置
      list7.remove(34); // 删除 34 这个值,如果有多个,删除前面的
      list7.removeAt(0); // 删除0号位置的元素
      list7.fillRange(1,2,888); // 修改位置1开始到位置2(不包括位置2)的元素值为888
      list7.insert(1,777); // 在1号位置之前插入777 这个值
      list7.insertAll(1,[333,4444]); // 一次插入多个值
      var str = list7.join(",");// 把list转为字符串,用逗号分隔
      str.split(","); //把字符串转为list,用逗号切割
  • Set:
    和list差不多,就是不能有重复的数据,下面主要看一下怎么定义一个set

    1
    2
    3
    4
    5
    6
    var set1 = {12,34,12};
    print(set1); // 重复的数据没有
    var set2 = new Set();
    set2.add(34);
    set2.addAll([2,3,12,34]);

  • Map:
    通常来说,Map是一个键值对相关的对象,键和值可以是任何类型的对象,每个键

    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
    // 定义一个Map
    var person ={
    "name":"yjr1100",
    "age":12
    };

    // 另一种方法定义一个Map
    var p = new Map();
    p["name"] = "yjr1100";
    p["age"] = 12;

    // Map的常用属性
    print(person.keys); // 获取所有的key
    print(person.values);// 获取所有的 value
    print(person.isEmpty); // 判空
    print(person.isNotEmpty); // 判非空

    // Map的常用方法
    person.addAll({
    "work":"打代码"
    "height":100
    }); // 添加键值对

    person.remove("work"); //删除属性

    person.containsValue("yjr1100"); // 看所有值中有没有 “yjr1100” 这个value
    person.containsKey("name"); // 看看所有的key中有没有 name这个key
  • List Map 和 Set 的遍历
    下面用list做演示,其它几种用法类似

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    List list1 = [12,34,87,24343,34];
    for(int i = 0;i<list1.length;i++){
    print(list[i]);
    }

    for(var item in list1){
    print(item);
    }

    list1.forEach((value){
    print(value);
    })

    var newlist = list1.map((value)=>value*2);// 可以用来修改每个值
    var newlist2 = list1.where((value) => value>20); // 把满足条件的元素组成新的list返回
    var f = list1.any((value) => value>30); // 只要集合里有一个满足条件,就返回true
    var f2 = list1.every((value) => value>30); // 只要集合里元素都条件,就返回true

类型的判断

1
2
3
4
5
6
7
8

var str = "123";

if(str is String){
print("是 String 类型");
}else if(str is int){
print("是 int 类型");
}

类型的转换

  • Number 和 String 类型之间的转换

    1
    2
    3
    4
    5
    6
    // Number转String toString();
    var myNum = 12;
    var str1 = myNum.toString();
    // String转Number parse;
    String str="123";
    var mynum = int.parse(str);
  • 其它类型和boolean类型的转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var str = "123";
    str.isEmpty

    var myNum = 0;
    myNum==0;

    var a;
    a == null;

    var b = 0/0;
    b.isNaN

final 和 const修饰符

const 值不变,开始就要赋值

final 可以开始不赋值,只能赋值一次

finalconst的主要区别:final不仅有const编译时常量的特性,最重要的是它是运行时常量,是惰性初始化,即在运行时第一次使用前才初始化

Dart 中的运算符

算术运算符

+-*/~/(取整)、%(取余)、++--

关系运算符

==!=><>=<=

逻辑运算符

!&&||

赋值运算符

  • 基础赋值运算符 =??=

    1
    2
    3
    4
    5
    6
    int b = 10;
    b??=23; //b是空的时候,把23赋值给b
    print(b);
    var c;
    c??=99; //如果c是空的时候,把99赋给c
    print(c);
  • 复合赋值运算符 +=-=*=/=%=~/=

Dart 中的流程控制

条件表达式

  • if-else
  • switch-case
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var sex = "男";
    switch(sex){
    case "男":
    print("是男的");
    break;
    case "女":
    print("是女的");
    break;
    default:
    print("都不是");
    break;
    }
  • 三目运算符
  • ??运算符
    1
    2
    3
    var a;
    var b = a ?? 10;
    print(b) //如果a是空的时候,把 10 赋值给b

for循环

c++ 相同

1
2
3
for(int i = 0;i<50;i++){
print(i);
}

while循环

1
2
3
4
5
6
7
8
9
10
int i = 1;
while(i<=10){
print(i);
i++;
}

do{
print(i);
i++;
}while(i<=10);

break 和 continue

break语句功能:

  • 在switch语句中使流程跳出switch结构。
  • 在循环语句中使流程跳出当前循环,遇到break循环终止,后面代码也不会执行

强调:

  1. 如果在循环中已经执行了break语句,就不会执行循环体中位于break后的语句。
  2. 在多层循环中,一个break语句只能向外跳出一层

break可以用在switch case中也可以用在for循环和while循环中

continue语句的功能:

  • 只能在循环语句中使用,使本次循环结束,即跳过循环体重下面尚未执行的语句,接着进行下次循环,continue可以用在for循环以及while循环中,但是不建议用在whi1e循环中,不小心容易死循环

Dart 中的函数

自定义方法

这里和C++ 语言类似,代码如下。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
// 基本格式
int addab(int a,int b){
return a+b
}

// 定义带有可选参数的方法
void printUserInfo(String name,[var age,var sex]){
if(age !=null){
print( "姓名$name----年龄$age");
}else{
print( "姓名$name-----年龄保密");
}
}

// 定义带有默认参数的方法
void printUserInfo2(String name,[var sex='女',var age = 10]){
if(age !=null){
print( "姓名$name----年龄$age");
}else{
print( "姓名$name-----年龄保密");
}
}

// 定义一个命名参数的方法,就是使用的时候,传参要指定形参名称
void printUserInfo3(String name,{var sex='女',var age}){
if(age !=null){
print( "姓名$name----年龄$age");
}else{
print( "姓名$name-----年龄保密");
}
}
printUserInfo3("yjr1100",age:18);

// 定义一个传入方法做参数的方法
void fn1(){
print("fn1");
}
void fn2(var fn){
fn();
}
fn2(fn1);

箭头函数

箭头函数里的代码就只有一行

1
2
3
4
5
6
7
8
9
10
List list1=[1,2,3];
list1.forEach((value){
print(value);
});

list1.forEach((value)=>print(value));

list1.forEach((value)=>{
print(value)
});

匿名函数

就是没有名字的函数,一般来说就是临时用的时候写一下,在下面的代码中,括号就是用来传参的参数列表,后面是函数体,我们将这个没有名字的函数赋值给了printNum,就可以通过printNum来调用它;

1
2
3
4
5
6
var printNum = (int n){
print(n);
}

printNum(12);

对于匿名方法,我们有自动执行的方式,就是不需要显示的调用,直接让它自己自动执行,并且也可以传入参数

1
2
3
4
((n){
print("自己执行了");
print(n);
})(12);

函数的闭包

  1. 全局变量:全局变量常驻内存、全局变量污染全局
  2. 局部变量:不常驻内存,会被垃圾回收机制回收,不会污染全局

引入闭包:

  • 常驻内存
  • 不污染全局

闭包粗浅的理解就是函数内部嵌套函数,内部函数会调用外部函数的变量或参数,变量或参数不会被系统回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn(){
int a = 12;
return (){
a++;
print(a);
};
}

var b=fn();
b();
b();
b();

var c = fn();
c();
c();
c();

Dart 中的类

面向对象编程(O0P)的三个基本特征是:封装、继承、多态

封装:封装是对象和类概念的主要特性。封装,把客观事物封装成抽象的类,并且把自己的部分属性和方法提供给其它函数使用。

继承:面向对象编程(O0P)语言的一个主要功能就是“继承”。继承是指这样一种能力:它可以使用现有类的功能,并有它父类的功能。

多态:允许将子类类型的指针赋值给父类类型的指针,同一个函数调用会有不同的执行效果。

Dart所有的东西都是对象,所有的对象都继承自Object类。

Dart是一门使用单继承的面向对象语言,所有的对象都是类的实例,并且所有的类都是Object的子类

一个类通常由属性方法组成。

定义一个类

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

// 定义一个类
class Person{
// 定义属性
String name;
int age;
// 默认构造函数
Person(){
print("构造函数");
}
// 命名构造函数
Person.now(){
print("命名构造函数");
}
// 定义方法
void getInfo(){
print("${this.name}");
}
}

// 实例化

var p1 = new Person();

print(p1.name);
p1.getInfo();

// 一般我们单独把类写在一个文件中,使用import关键字来引入

dart没有像 C++ 那样使用 public,private,protected 等修饰符来限制类属性的访问权限,如果我们想要把一个方法或者属性定义为私有,需要在定义时名字前面 加上 _ ,而且私有方法和私有属性必须单独放在一个文件中。

dart中的getter,setter

在dart中通过 get 可以让一个方法像类属性那样直接使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Rect{
int height;
int width;
Rect(this.height,this.width);
get area{
return this.height*this.width;
}

set areaHeight(value){
this.height=value;
}
}

Rect r = new Rect(10,2);
print("面积${r.area}");
r.areaHeight=6;
print("面积${r.area}");

Dart中的静态成员

可以通过 static 关键字来实现类级别的变量和函数,即不需要实例化对象,就可以使用

静态方法不能访问非静态成员,静态方法可以访问静态成员,非静态方法可以访问静态成员以及非静态成员。在类里,我们一般使用this访问非静态属性,而直接用属性名访问静态属性。

1
2
3
4
5
6
class Person{
static String name;
static void show(){
print(name);
}
}

Dart中的对象操作符

? 条件运算符
as 类型转换
is 类型判断
.. 级联操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Person p;
// p.printInfo()// 报错,p没有初始化,是空的
p?.printInfo(); // 什么都不会打印,只有p不是空的时候,才会打印

print(p is Person); // 判断p是不是Person类

var p1;
p1 = "";
p1 = new Person();
(p1 as Person).printInfo();


p1.name = "sdlf";
p1.age = 12;
p1.printInfo();

// 使用级联方式
p1..name="lala"
..age = 12
..printInfo();

Dar类的继承

子类使用 extends 关键字来继承父类
子类会继承父类里面可见的属性和方 但不会继承构造函数
子类能复写父类的方法 getter 和 setter

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
30
31
32
33
34
class Person{
String myname;
int age;
Person(String myname,int age){
this.myname=myname;
this.age=age;
}
void printInfo(){
print("${this.myname}---${this.age}");
}
void work(){
print("${this.myname}在工作")
}
}

class Son extends Person{
String sex;
Son(String myname,int age,String sex):super(myname,age){
this.sex = sex;
}
run(){
super.work(); // 在自类调用父类的方法
print("${this.myname}---${this.age}-----${this.sex}");
}

// 复写父类方法
@override
void printInfo(){

}
}

Son s1 = new Son("sdf",324,"女");
s1.printInfo();

抽象类

Dart中抽象类:Dart抽象类主要用于定义标准,子类可以继承抽象类,也可以实现抽象类接口。

  1. 抽象类通过abstract关键字来定义
  2. Dart中的抽象方法不能用abstract声明,Dart中没有方法体的方法我们称为抽象方法。
  3. 如果子类继承抽象类必须得实现里面的抽象方法
  4. 抽象类不能被实例化,只有继承它的子类可以
  5. 如果把抽象类当做接口实现的话必须得实现抽象类里面定义的所有属性和方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class Animal{
eat();//抽象方法
printInfo(){
print("我是抽象类的普通方法");
}
}
class Dog extends Animal{
@override
eat(){ //实现抽象方法
print("狗吃骨头");
}
}
class Cat extends Animal{
@override
eat(){ //实现抽象方法
print("猫吃鱼");
}
}

Dart中的接口

和Java一样,dart也有接口,但是和Java还是有区别的。

首先,dart的接口没有interface关键产定义接口,而是普通类抽象类都可以作为接口被实现。建议使用抽象类定义接口,实现接口,必须重写抽象类中的所有属性和方法。

使用implements:关键字进行接口实现。

extends抽象类和implements的区别:

  1. 如果要复用抽象类里面的其它普通方法,并且要用抽象方法约束子类的话我们就用extends继承抽象类
  2. 如果只是把抽象类当做标准的话我们就用implements:实现抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class Db{//当做接口接口:就是约定、规范
String uri;
//数据库的链接地址
add();
save();
}
class Mysql implements Db{ //实现接口,必须重写抽象类中的所有属性和方法
@override
String uri
@override
add(){
return null;
}
save(){
return null;
}
}

Dart中可以一个类实现多个接口,我们需要实现所有接口中的所有属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class A{
String name;
printA();
}
abstract class B{
printB();
}
class C implements A,B{
@override
String name;
@override
printA(){
print("A");
}
@override
printB(){
print("B");
}
}

Dart中的mixins

mixins的中文意思是混入,就是在类中混入其他功能。在Dart中可以使用mixins实现类似多继承的功能

因为mixins使用的条件,随着Dart版本一直在变,这里讲的是Dart2.x中使用mixins的条件:

  1. 作为mixins的类只能继承自object,不能继承其他类
  2. 作为mixins的类不能有构造函数
  3. 一个类可以mixins多个mixins类
  4. mixins绝不是继承,也不是接口,而是一种全新的特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A{
void printA(){
print("A");
}
}
class B{
void printB(){
print("B");
}
}
class C with A,B{
}

var c = new C();
c.printA();
c.printB();

Dart中的多态

允许将子类类型的指针赋值给父类类型的指针,同一个函数调用会有不同的执行效果

子类的实例赋值给父类的引用。

多态就是父类定义一个方法不去实现,让继承他的子类去实现,每个子类有不同的表现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class Animal{
eat();//抽象方法
printInfo(){
print("我是抽象类的普通方法");
}
}
class Dog extends Animal{
@override
eat(){ //实现抽象方法
print("狗吃骨头");
}
run(){
print("run");
}
}

Animal d = new Dog();
d.eat();
// d.run();// 此时,没有run方法,因为这个把子类的指针赋值给了父类。

Dart中的泛型

泛型就是解决类、接口方法的复用性、以及对不特定数据类型的支持(类型校验)

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
T getData<T>(T value){
return value;
}

// 解决了代码复用问题的同时,进行了数据类型校验
gerData<String>("sdf");
gerData<int>(123);

// 泛型类
class MyList<T>{
List list=<T>[];
void add(T value){
this.list.add(value);
List getList(){
return list;
}
}

MyList l1 = new MyList();
l1.add(12);
l1.add("sdf");
l1.add(true);

MyList l2 = new MyList<String>();
l2.add("sdfsd"); // 这个时候就只能传入String类型的数据

// 和泛型类相同,我们也可以定义泛型接口

Dart 中的库

Dart中的库主要有三种:

  1. 我们自定义的库

    1
    import 'lib/xxx.dart';
  2. 系统内置库

    1
    2
    3
    import 'dart:math';
    import 'dart:io';
    import 'dart:convert';
  3. Pub包管理系统中的库
    在下面的网站中,我们就可以找到一些第三方库进行下载
    https://pub.dev/packages
    https://pub.flutter-io.cn/packages
    https://pub.dartlang.org/flutter/

使用时:

  1. 需要在自己项目根目录新建一个pubspec.yam1
  2. pubspec.yaml文件然后配置名称、描述、依赖等信息
    1
    2
    3
    4
    name:XXX
    description:A new flutter module project.
    dependencies:
    http:^8.12.0+2
  3. 然后在项目目录中运行pub get获取包下载到本地
  4. 项目中引入库import'package:http/http.dart'as http;看文档使用

如果有库冲突,我们可以使用 as 来重命名库,通过 show 关键字,可以进行库的部分引入,即只引入我们需要的部分。通过hide 关键字,可以隐藏库中的某些部分,而引入其它部分。

Dart 2.13 以后的一些新特性

Null safety

空安全,可以帮助开发者避免一些日常开发中很难被发现的错误,并且额外的好处是可以改善性能

Flutter2.2.0(2021.5.19发布)以后的版本都要求使用 Null Safety

  • ? 使用问号,表示可空类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // int a = 12;
    // a = null; 不可以赋值为null,报错
    int? a = 12;
    a = null; //可以 ,int? 表示 a 可以是空类型

    String? getDate(url){
    if(url==null){
    return null;
    }
    }
  • ! 类型断言
    1
    2
    3
    4
    String? str="sdfsdfsdf";
    str = null;
    //print(str.length);// 报错,因为str是可空类型,我们需要进行类型断言
    print(str!.length) //类型断言,如果不等于null,打印长度,如果是null,抛出异常

    required 关键词

最开始 @required 是注释,现在它已经作为内置修饰符,主要用于标记命名参数,使得他们不为空。

1
2
3
4
5
6
7
8
9
10
11

String printuserInfo(String username, {int age = 10, String sex = ""})
{
return "姓名:$username--性别:$sex--年龄:$age";
}
// 没有赋初值的命名参数,需要用required来修饰,表示使用时必须传入该参数,不然会报错
String printInfo(String username, {required int age, required String sex})
{
//
return "姓名:$username--性别:$sex-年龄:$age";
}

const 和final补充

core库中 identical 函数的用法

通过 identical 函数,可以检查两个引用是否指向同一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var o1 = new Object();
var o2 = Object(); // 可以省略new
print(identical(o1,o2)); //false
print(identical(o1,o1)); //true

// 使用const表示初始化常量构造函数,想要使用 const来实例化常量构造函数,首先这个构造函数定义的时候,就必须是常量构造函数
var o3 = const Object();
var o4 = const Object();
// o3,o4 共享了存储空间
print(identical(o3,o4)); //true
print(identical(o3,o3)); //true
// 说明const关键词再多个地方创建相同对象的时候,内存中只保留了一个对象

const a=[2];
const b=[2];
print(identical(a,b));//true
const c=[2];
const d=[3]; // 必须是相同对象,才会指向同一个空间
print(identical(c,d));//false

常量构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//常量构造函数
class Container{
final int width;
final int height;
const Container({required this.width,required this.height});
}

var c1=Container(width:100,height:100);
var c2=Container(width:100,height:100);
print(identical(c1,c2));//false

// 使用const后才说明是要用常量构造函数了。
var c3=const Container(width:100,height:100);
var c4=const Container(width:100,height:100);
print(identical(c3,c4));//true

// 常量构造函数传入的值相同才会只保留一个,否则还是分开的。
var c5=const Container(width:100,height:100);
var c6=const Container(width:120,height:100);
print(identical(c5,c6));//false

常量构造函数总结如下几点:

  1. 常量构造函数需以const关键字修怖
  2. const构造函数必须用于成员变量都是final的类
  3. 如果实例化时不加cost修饰符,即使调用的是常量构造函数,实例化的对象也不是常量实例
  4. 实例化常量构造函数的时候,多个地方创建这个对象,如果传入的值相同,只会保留一个对象
  5. Flutterconst修饰不仅仅是节省组件构建时的内存开销,Flutter在需要重新构建组的时候,不构建const声明的组件。