Flutter学习笔记:01. Dart 基础

260 阅读13分钟

image.png

1. 概述

1.1. 简介

  • Dart 是谷歌开发的,类型安全的,面向对象的编程语言,被应用与Web服务器移动应用和物联网等领域。
  • Dart 诞生于2011年10月10日。
  • Dart 简单易学(类型TypeScript,是强类型的语言)
  • 运行方式
    • 原生虚拟机(Dart 代码可以运行在 Windows、Mac、Linux 上)
    • JavaScript 引擎 (Dart 代码可以转为 JS 代码,然后运行在浏览器上)

1.2. Dart 与 JavaScript

image.png

1.3. 环境搭建

  • 从 Flutter 1.21 版本开始,Flutter SDK 会同时包含完整的 Dart SDK。
    • 如果你已经安装了 Flutter,就无需再下载 Dart SDK 了。
    • 官网介绍:dart.cn/get-dart

image.png

  • 绑定环境变量
    • 先确定 Flutter SDK 的安装路径
      • 例如:我的 Flutter SDK 的安装路径是 D:\flutter
    • D:\flutter\bin
      • Dark 和 Flutter 的相关执行命令
    • D:\flutter\bin\cache\dart-sdk\bin
      • Dart SDK 的相关命令的执行路径
    • D:\flutter.pub-cache\bin
      • Pub下载的公共模块的执行路径

1.4. 资源网站

2. 语法基础

  • 注释语法与 JS 一致
  • 声明函数不需要关键字(JS 中通过 function 关键字来声明函数)
  • 函数和参数前面都有类型声明,void 表示没有返回值,int 是整型数字
  • 打印使用 print (JS 使用 console.log())
  • 每行代码结束时,必须写结束分号(;)
  • 字符串使用引号包起来,支持模板字符串
  • main 是入口函数,Dart 应用程序总是从 main 函数开始执行
  • var 声明的变量,其数据类型是动态的

2.1. 运行代码

  • 在命令行中运行
    • dart hello.dart
  • Dart 执行文件中的 main 函数
void main(){
    print('Hello, World');
}
  • 输出:Hello, World

2.2. 注释

  • 单行注释
    • //我是单行注释
  • 多行注释
    • /*我是多行注释*/
  • 文档注释
    • ///我是文档注释
    • 可以通过 dartdoc 将注释转成文档(文档注释支持 markdown 语法)

2.3. 变量

  • 变量是一个引用,Dart 万物皆对象,变量存储的是对象的引用
  • 声明变量:
    • 明确指定类型:int age = 18;
    • 不明确类型:var age = 18; 或者 dynamic age = 18;
    • 变量名大小写敏感(age 和 Age 是两个不同的变量)
    • 变量默认值是 null(JS 中变量的默认值是 undefined
    • Dart 变量的值不会进行隐式转换(null 不会自动转成 false
void main() {
  // 声明变量
  var age = 18;
  print(age); // 18

  // 指定数据类型的方式
  String name = '张三';
  // String name = 123; 报错,不能将数字类型123赋值给 String类型的 name
  print(name); // 张三

  // dynamic myName = 18;
  dynamic myName = '李四';
  print(myName); // 李四

  // 变量的默认值
  var defaultName;
  print(defaultName); // null
  
  // 变量名大小写敏感
  var Age = 30;
  print(Age); // 30
}

2.4. 常量

  • 常量就是值不可变的变量(一旦声明,其值不可更改)
  • 声明常量
    • const age = 18;
    • final age = 18;
  • constfinal 的区别:
    • const time = DateTime.now(); // 报错,无法将运行时的值赋值给 const 变量
    • final time = DateTime.now(); // 成功,可以将运行时的值赋值给 final 变量
void main() {
  const n1 = 1;
  // n1 = 2; // 报错,常量一旦声明无法修改
  print(n1); // 1
  
  final n2 = 2;
  print(n2); // 2
}

3. 数据类型

3.1. Number

void main() {
  // 声明整数
  int count = 3;
  print(count); // 3

  // 声明浮点数
  // double price = 4; // 4.0
  double price = 3.7;
  print(price); // 3.7

  // 声明数值类型,不确定具体的数字类型时使用
  // num n1 = 3.8;
  num n1 = 10;
  print(n1); // 10

  // 类型转换
  print(n1.toString());
  print(3.18.toInt()); // 向下取整
  
  // 四舍五入
  print(3.1415926.round()); // 3
  print(3.1415926.toStringAsFixed(2)); // 3.14, 保留2位小数
  
  // 返回余数
  print(10.remainder(4)); // 2
  
  // 数字比较: 0:相等,1:大于,-1:小于
  print(10.compareTo(12)); // -1
  
  // 返回最大公约数
  print(12.gcd(18)); // 6
  
  // 科学计数法
  print(1000.toStringAsExponential(2)); // 1.00e+3
}

3.2. String

  • 声明字符串(String)

    • 单引号、双引号都可以
    • 三个引号可以声明包含换行符的字符串
  • 常见API

  • 正则表达式

    • RegExp(r'正则表达式');
    • RegExp(r'/d+');
void main() {
  // 声明字符串
  //   var str1 = 'Hello World'; // 单引号
  //   print(str);
  //   var str2 = "你好,世界"; // 双引号
  //   print(str2);
  
  String str1 = 'Hello World'; // 单引号
  print(str1);
  String str2 = "你好,世界"; // 双引号
  print(str2);
  // 三引号,可以是三个单引号,也可以是三个双引号,但必须成对
  String str3 = '''Hello
  World
  ''';
  print(str3);
  
  // 常见操作   
  // 字符串拼接
  print(str1 + str2);
  // 模板字符串写法
  print("$str1 $str2");
  
  // 字符串的分割
  print(str1.split('')); // [H, e, l, l, o,  , W, o, r, l, d]
  
  // 字符串去除空格
  print('  abc  '.trim()); // abc
  
  // 判断字符串是否为空
  print(''.isEmpty); // true
  print('abc'.isEmpty); // false
  print(''.isNotEmpty); // false
  print('abc'.length); // 3
  
  // 字符串的替换
  print(str1.replaceAll('World', 'Dart')); // Hello Dart
  // 支持正则替换
  print('h1n2b3v4k5j6l7'.replaceAll(RegExp(r'\d+'), '-')); // h-n-b-v-k-j-l-
  
  // 通过正则匹配手机号
  var isPhone = RegExp(r'^1\d{10}$');
  print(isPhone.hasMatch('13333333333')); // true
  print(isPhone.hasMatch('1333333333')); // false
  
  // 查找字符串
  print(str1.contains('e')); // true
  // 定位字符串
  print(str1.indexOf('e')); // 1
}

3.3. Boolean

  • Dart 通过 bool 关键字来表示布尔类型
  • 布尔类型只有2个值:truefalse
  • 对变量进行判断时,要显式地检查布尔值
    • if(varName){...} ×
    • if(varName == 0){...}
    • if(varName == null){...}
void main() {
  // 声明布尔类型
  bool flag1 = true;
  print(flag1);

  bool flag2 = false;
  print(flag2);

  // 显式地进行判断
  // 因为 Dart 是强类型语言,声明变量时类型就确定了,所以在 Dart 中没有 ===,只有 == 用于判断是否相等
  var flag3;
  if (flag3 == null) {
    print('真');
  } else {
    print('假');
  }
  
  // 特殊的判断
  var n1 = 0 / 0;
  print(n1); // NaN
  // 判断一个变量是否是 NaN
  print(n1.isNaN); // true
}

3.4. List

  • Dart 中的数组,由 List 对象表示。List 有2种声明方式
    • 字面量方式
      • List list = []; // 不限定元素的数据类型
      • List list = <int>[]; // 限定元素的数据类型是 int
    • 构造函数方式
      • List list = new List.empty(growable:true); // 不限制长度的空列表
      • List list = new List.filled(3, 0); // 声明指定长度的填充列表
  • 扩展操作符(...)
    • var list = [1, 2, 3];
    • var list2 = [0, ...list]; // [0, 1, 2, 3]
  • 常用API
void main() {
  // 声明List - 字面量
  List l1 = ['a', 'b', 'c', 1, 2, 3];
  print(l1); // [a, b, c, 1, 2, 3]

  // 限定元素类型的声明方式
  List l2 = <int>[1, 2, 3];
  print(l2);

  // 通过构造函数的声明方式,growable: true 表示数组可扩充
  var l3 = new List.empty(growable: true);
  l3.add(1);
  print(l3);

  var l4 = new List.filled(3, 6);
  print(l4); // [6, 6, 6]
  
  // 扩展运算符
  var l5 = [0, ...l4];
  print(l5); // [0, 6, 6, 6]
  
  // 扩展运算符的一个变种, l6的默认值是null,不能使用扩展运算符,会报错,可使用 ...?
  var l6;
  var l7 = [7, ...?l6];
  print(l7); // [7]
  
  // 返回列表的长度
  print(l1.length); // 6
  
  // 列表的翻转
  print(l1.reversed); // (3, 2, 1, c, b, a),翻转后的结果不再是数组了,但任然是可迭代的
  print(l1.reversed.toList()); // [3, 2, 1, c, b, a],通过 toList() 方法转换为数组
  
  // 添加元素
  l3.addAll([4, 5, 6]);
  print(l3); // [1, 4, 5, 6]
  
  // 删除元素
  l3.remove(6);
  print(l3); // [1, 4, 5]
  // 根据索引删除元素
  l3.removeAt(1);
  print(l3); // [1, 5]
  
  // 在指定的位置添加元素
  l3.insert(1, 9);
  print(l3); // [1, 9, 5]
  
  // 清空
  l3.clear();
  print(l3.length);
  print(l3.isEmpty); // true
  
  // 合并元素
  List words = ['Hello', 'World'];
  print(words.join('-')); // Hello-World
}
  • 遍历 List
    • forEach()
      • 遍历列表
    • map()
      • 遍历并处理元素,然后生成新的列表
    • where()
      • 返回满足条件的数据
    • any()
      • 只要有一项满足条件,即返回 true
    • every()
      • 判断每一项是否都满足条件,都满足条件才返回 true
void main() {
  var nums = [1, 2, 3];

  // for 循环进行遍历
  for (var i = 0; i < nums.length; i++) {
    print(nums[i]);
  }

  // for...in
  for (var item in nums) {
    print(item);
  }

  // forEach
  nums.forEach((item) {
    print(item);
  });

  // map
  var newNums = nums.map((e) {
    return e * e;
  });
  print(newNums); // (1, 4, 9)
  print(newNums.toList()); // [1, 4, 9]

  // where 返回符合条件的元素
  bool isOdd(n) => n % 2 == 1;
  var oddNum = nums.where((item) => isOdd(item));
  print(oddNum.toList()); // [1, 3]

  // 使用 any() 来检测是否有奇数(至少一个)
  print(nums.any(isOdd)); // true

  // 使用 every() 来判断是否都是奇数
  print(nums.every(isOdd)); // false

  // 拉平数组
  var pairs = [
    [1, 2],
    [3, 4]
  ];
  var flattened = pairs.expand((item) => item).toList();
  print(flattened); // [1, 2, 3, 4]

  // 折叠 - 对列表中的每一个元素,做一个累计操作
  int result = nums.fold(2, (pre, cur) => pre * cur); // 2 * (1 * 2 * 3)
  print(result); // 12
}

3.5. Set

  • Set 是一个无序的,元素唯一的集合
  • Set 有字面量和构造函数两种声明方式(字面量中用大括号)
  • 无法通过下标取值
  • 具有集合特有的操作
    • 例如:求交集、并集、差集等
  • 常用API
void main() {
  // 字面量
  var nums = <int>{1, 2, 3};
  print(nums); // {1, 2, 3}
  
  // 构造函数
  var fruits = new Set();
  fruits.add('苹果');
  fruits.add('香蕉');
  fruits.add('橘子');
  print(fruits); // {苹果, 香蕉, 橘子}
  print(fruits.toList()); // [苹果, 香蕉, 橘子]
  
  List myNums = [1, 2, 2, 3, 4];
  print(myNums.toSet()); // {1, 2, 3, 4},可以将重复的元素过滤掉
  
  // 集合特有的操作
  var caocao = new Set();
  caocao.addAll(['张辽', '司马懿', '关羽']);
  
  var liubei = new Set();
  liubei.addAll(['关羽', '张飞', '诸葛亮']);
  
  // 求交集
  print(caocao.intersection(liubei)); // {关羽}
  
  // 求并集
  print(caocao.union(liubei)); // {张辽, 司马懿, 关羽, 张飞, 诸葛亮}
  
  // 求差集
  print(caocao.difference(liubei)); // {张辽, 司马懿}
  
  // 返回第一个元素
  print(caocao.first); // 张辽
  
  // 返回最后一个元素
  print(caocao.last); // 关羽
  
  // 集合不能通过下标取值
  // print(caocao[1]); // 报错
}

3.6. Map

  • Map 是一个无序的键值对(key - value)映射,通常被称为哈希或字典。
  • 声明方式
    • var map = { key1: value1, key2: value2 };
    • var map = new Map();
    • map['key'] = value;
  • 常用API
void main() {
  // 字面量
  var person = {
    'name': 'zs',
    'age': 20,
  };
  print(person); // {name: zs, age: 20}
  
  // 构造函数
  // var p = new Map();
  var p = Map(); // new 关键字可以省略
  p['name'] = 'ls';
  p['age'] = 22;
  print(p);
  
  // 访问属性
  print(p['name']); // ls
  
  // 判断 Map 中的 key 是否存在
  print(p.containsKey('name')); // true
  print(p.containsKey('aaa')); // false
  
  // 赋值
  // 如果 key 不存在,我们才赋值(如果key已经存在,则不赋值)
  p.putIfAbsent('gender', () => '男');
  print(p); // {name: ls, age: 22, gender: 男}
  p.putIfAbsent('gender', () => '女');
  print(p); // {name: ls, age: 22, gender: 男}
  
  // 获取 Map 中所有的 key
  print(p.keys); // (name, age, gender)
  
  // 获取 Map 中所有的 value
  print(p.values); // (ls, 22, 男)
  
  // 根据条件进行删除
  p.removeWhere((key, value) => key == 'gender');
  print(p); // {name: ls, age: 22}
}

3.7. 其他

  • Runes(符文)
    • Runes 对象是一个 32 位字符对象。它可以把文字转换成符号表情或特定的文字
    • print('\u{1f44d}'); // 👍
    • copychar.cc/
  • Symbol
    • 在 Dart 中符号用 # 开头来表示的标识符
  • dynamic
    • 动态数据类型
void main() {
  var str = '😍';
  print(str); // 😍
  print(str.length); // 2 UTF-16
  print(str.runes.length); // 1 UTF-32
  
  // Runes 可以将 UTF-32 字符集表示的内容转成符号
  Runes input = new Runes('\u{1f680}');
  print(new String.fromCharCodes(input)); // 🚀
  
  // Symbol
  var a = #abc;
  print(a); // Symbol("abc")
  var b = new Symbol('abc');
  print(b); // Symbol("abc")
  
  print(#abc == new Symbol('abc')); // true
  
  // 声明动态类型的变量
  dynamic foo = 'bar';
  foo = 123;
  print(foo); // 123
}

4. 运算符

  • 地板除:求余数再向下取整(~/
  • 类型判断运算符(is | is!
  • 避空运算符(?? | ??=
  • 条件属性访问(?.
  • 级联运算符(..
    • myObject.myMethod(); // 返回 myMethod 的返回值
    • myObject..myMethod(); // 返回 myObject 对象的引用
void main() {
  // 地板除
  print(7 ~/ 4); // 1

  // 类型判断运算符
  List list = [];
  if (list is List) {
    print('list is List');
  } else {
    print('list is not List');
  } // list is List

  if (list is! List) {
    print('不是列表');
  } else {
    print('是列表');
  } // 是列表

  // 避空运算符
  print(1 ?? 3); // 1
  print(null ?? 12); // 12

  var foo;
  // foo = 6;
  print(foo ?? 0); // 如果 foo 是 null,则返回 0

  var a;
  // if (a == null) {
  //   a = 3;
  // }
  a ??= 3;
  print(a); // 3
  a ??= 6; // 如果 a 不是 null,则赋值失败
  print(a); // 3

  // 条件属性运算符(保护可能为空的属性)
  var m = new Map();
  print(m.length); // 0
  var obj;
  // print(obj.length); // TypeError: Cannot read properties of null (reading 'get$length')
  print(obj?.length); // null

  // 级联运算符
  // Set s = new Set();
  // s.add(1);
  // s.add(2);
  // s.add(3);
  // s.remove(2);
  // print(s); // {1, 3}

  Set s = new Set();
  s
    ..add(1)
    ..add(2)
    ..add(3)
    ..remove(2);
  print(s); // {1, 3}
}

5. 函数

5.1. 声明函数

  • 直接声明
    • Dart 中声明函数不需要 function 关键字
  • 箭头函数
    • Dart 中的箭头函数,函数体只能写一行且不能带有结束的分号
    • Dart 中的箭头函数,只是函数的一种简写形式,没有其他副作用(这点与 JS 不同)
  • 匿名函数
  • 立即执行函数
// Dart 中声明函数,不需要 function 关键字
void printInfo() {
  print('Hello World');
}

// 返回值,与函数声明的类型要一致
int getNum() {
  // return 'Hello'; // 不能返回字符串类型
  return 1;
}

void main() {
  // 调用函数
  printInfo(); // Hello World

  print(getNum()); // 1

  // 匿名函数
  var myPrint = (value) {
    print(value);
  };
  List fruits = ['苹果', '香蕉', '橘子'];
  fruits.forEach(myPrint);

  // 箭头函数
  fruits.forEach((item) => {
        print(item) // 箭头函数中,不能写结束的分号(;)
      });

  fruits.forEach((item) => print(item));

  // 立即执行函数
  ((int n) {
    print(n); // 17
  })(17);
}

5.2. 函数参数

  • 必填参数
    • 参数类型参数名称
  • 可选参数
    • 放在必选参数后面
    • 通过中括号包裹起来
    • 带默认值的可选参数
  • 命名参数
    • 用大括号包裹起来
    • 调用函数时,命名参数的名称与声明函数中的名称保持一致
  • 函数参数
void main() {
  // 必填参数
  String userInfo(String name) {
    return '你好:$name';
  }
  
  String res = userInfo('张三');
  print(res); // 你好:张三
  
  // 可选参数
  String userInfo2(String name, [int age = 0]){
    return '你好:$name,年龄:$age';
  }
  
  String res2 = userInfo2('李四');
  print(res2); // 你好:李四,年龄:0
  
  // 命名参数
  String userInfo3(String name, {int age = 0}){
    return '你好:$name,年龄:$age';
  }
  // 命名参数调用时,需要与声明的形参一致
  String res3 = userInfo3('李四', age: 20);
  print(res3); // 你好:李四,年龄:20
  
  // 函数参数
  var myPrint = (value) {
    print(value);
  };
  List fruits = ['苹果', '香蕉', '橘子'];
  // 将匿名函数 myPrint 传递给函数 forEach
  fruits.forEach(myPrint);
}

5.3. 作用域与闭包

  • 作用域和闭包与JS的一样

image.png

  • Dart 中闭包的实现方式与JavaScript 中完全一致
  • 使用时机:即能重用变量,又保护变量不被污染
  • 实现原理:外层函数被调用后,外层函数的作用域对象 (AO)被内层函数引用着,导致外层函数的作用域对象无法释放,从而形成闭包
var globalNum = 10;

void main() {
  printInfo() {
    // 局部变量
    var localNum = 100;
    print(localNum); // 100
    print(globalNum); // 10 我们可以在函数作用域中,访问全局变量
  }

  printInfo();
  // print(localNum); // 不能再全局作用域中,访问局部变量

  // 闭包
  parent() {
    var money = 1000;
    return () {
      money -= 100;
      print(money);
    };
  }
  
  var p = parent();
  p(); // 900
  p(); // 800
  p(); // 700
}

5.4. 异步函数

  • JavaScript 中,异步调用通过 Promise 来实现

    • async 函数返回一个 Promiseawait 用于等待 Promise
  • Dart 中,异步调用通过 Future 来实现

    • async 函数返回一个 Futureawait 用于等待 Future
  • Future 详情

  • 第一种用法:

import 'dart:convert' as convert;
import 'package:http/http.dart' as http;

// https://httpbin.org/ip 返回 IP 地址
Future getIpAddress() {
  var url = Uri.https('httpbin.org', 'ip');

  return http.get(url).then((response) {
    print(response.body);
    String ip = convert.jsonDecode(response.body)['origin'];
    return ip;
  });
}

void main() {
  // 调用 getIpAddress
  getIpAddress()
      .then((ip) => print(ip))
      .catchError((error) => print(error)); // 107.189.14.105
}
  • 第二种用法:
import 'dart:convert' as convert;

import 'package:http/http.dart' as http;

// https://httpbin.org/ip 返回 IP 地址
Future getIpAddress() async {
  var url = Uri.https('httpbin.org', 'ip');
  final response = await http.get(url);
  String ip = convert.jsonDecode(response.body)['origin'];
  return ip;
}

void main() async {
  // 调用 getIpAddress
  try {
    final ip = await getIpAddress();
    print(ip); // 107.189.14.105
  } catch (error) {
    print(error);
  } finally {
    print('finally'); // finally
  }
}

6. 类与对象

6.1. 类

6.1.1. 类的简介

  • 类是通过 class 声明的代码段,包含属性和方法
    • 属性: 用来描述类的变量
    • 方法:类中的函数称为类的方法
  • 对象是类的实例化结果 (var obj= new Myclass())
  • 编程方式
    • 面向对象编程(OOP)
    • 面向过程编程(POP)

image.png

// 声明类
class Person {
  // 类的属性
  String name = '张三';

  // 类的方法
  void getInfo() {
    print('我是:$name');
  }
}

void main() {
  // 实例化类,然后得到一个对象
  Person p = new Person();
  // 访问类中的属性
  print(p.name); // 张三
  // 访问类中的方法
  p.getInfo(); // 我是:张三
  
  // Dart 中所有的内容都是对象
  Map m = new Map();
  print(m.length); // 0
  m.addAll({'name': '李四', 'age': 20});
  print(m.length); // 2
}

6.1.2. 构造器(构造函数)

  • 默认构造函数
    • 与类同名的函数,在实例化时,自动被调用
// 声明类
class Point {
  var x, y;

  // 声明普通构造函数
  Point() {
    print('我是默认构造函数,会第一个被调用');
    
    // this 可以省略
    // x = 0;
    // y = 0;
    
    // 当命名指向有歧义时,this 不能省略
    this.x = 0;
    this.y = 0;
  }
}

void main() {
  var p = new Point();
  print(p.x); // 0
}
// 声明类
class Point {
  var x, y;

  // 声明带参数的普通构造函数
  Point(num x, num y) {
    // 当命名指向有歧义时,this 不能省略
    this.x = x;
    this.y = y;
  }
}

void main() {
  var p = new Point(3, 4);
  print(p.y); // 4
}
// 声明类
class Point {
  var x, y;

  // 声明带参数的普通构造函数的简写形式
  Point(this.x, this.y);
}

void main() {
  var p = new Point(3, 4);
  print(p.y); // 4
}
  • 命名构造函数
    • 在类中使用命名构造器(类名.函数名)实现多个构造器,可以提供额外的清晰度
// 声明类
class Point {
  var x, y;

  Point(this.x, this.y);

  // 命名构造函数
  Point.origin() {
    x = 0;
    y = 0;
  }

  // 命名构造函数
  Point.fromJson({x = 0, y = 0}) {
    this.x = x;
    this.y = y;
  }
}

void main() {
  // 默认坐标
  Point p1 = new Point.origin();
  print(p1.y); // 0

  // 手动设置坐标
  Point p2 = new Point.fromJson(x: 3, y: 6);
  print(p2.x); // 3
}
  • 常量构造函数
    • 如果类生成的对象不会改变,您可以通过常量构造函数使这些对象成为编译时常量
    • 使用不可变对象可以提高 Flutter 的渲染效率
class Point {
  var x, y;

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

class ImmutablePoint {
  // 属性必须通过 final 声明
  final num x;
  final num y;

  // 常量构造函数,必须通过 const 声明,同时不能有函数体
  const ImmutablePoint(this.x, this.y);
}

void main() {
  Point p1 = new Point(1, 2);
  Point p2 = new Point(1, 2);
  print(p1 == p2); // false

  // 常量构造函数,可以当做普通构造函数使用
  var p3 = new ImmutablePoint(1, 2);
  var p4 = new ImmutablePoint(1, 2);
  print(p3 == p4); // false

  // 声明不可变对象,必须通过 const 关键字
  var p5 = const ImmutablePoint(1, 2);
  var p6 = const ImmutablePoint(1, 2);
  print(p5 == p6); // true
  
  // 实例化时,new 关键字,可以省略
  var p7 = ImmutablePoint(1, 2);
  var p8 = ImmutablePoint(1, 2);
  print(p7 == p8); // false
}
  • 工厂构造函数
    • 通过 factory 关键字声明,工厂函数不会自动生成实例,而是通过代码来决定返回的实例
class Person {
  String name;

  static var instance;

  // 工厂构造函数
  factory Person([String name = '刘备']) {
    // 工厂构造函数中,不能使用 this 关键字
    // print(this.name);

    if (Person.instance == null) {
      // 第一次实例化
      Person.instance = Person.newSelf(name);
    }
    // 非第一次实例化
    return Person.instance;
  }
  
  // 命名构造函数
  Person.newSelf(this.name);
}

void main() {
  // 实例化操作
  Person p1 = new Person('关羽');
  print(p1.name); // 关羽

  Person p2 = new Person('张飞');
  print(p2.name); // 关羽

  print(p1 == p2); // true
}

6.1.3. 访问修饰

  • Dart 与 TypeScript 不同,没有访问修饰符 (publicprotectedprivate)
  • Dart 类中,默认的访问修饰符是公开的 (即 public)
  • 如果属性方法_(下划线)开头,则表示私有 (即 private)
  • 只有把类单独抽离出去,私有属性和方法才起作用
    • lib/Person.dart
    • import 'lib/Person.dart'
class Person {
  String name;

  // 声明私有属性
  num _money = 100;

  Person(this.name);
}

void main() {
  var p = new Person('张三');
  print(p.name); // 张三
  // 访问私有属性,因为 Person 类和 main 函数平级,所以 Person 类的私有属性不起作用,仍然可以访问到,要让私有属性起作用,需要将 Person 类抽离出去
  print(p._money); // 100
}

抽离后:

// lib/Person.dart

class Person {
  String name;

  // 声明私有属性
  num _money = 100;

  Person(this.name);

  num getMoney() {
    return this._money;
  }

  // 声明私有方法
  void _wife() {
    print('我是 $name 的老婆');
  }
}
import 'lib/Person.dart';

void main() {
  var p = new Person('张三');
  print(p.name); // 张三
  // 被抽离后就访问不到私有属性了
  // print(p._money);
  print(p.getMoney()); // 100
  // 访问私有方法
  // p._wife(); // 不能访问
}

6.1.4. Getter 和 Setter

  • Getter (获取器) 是通过 get 关键字修饰的方法
    • 函数没有小括号,访问时也没有小括号(像访问属性一样访问方法)
  • Setter (修改器)是通过 set 关键字修饰的方法
    • 访问时,像设置属性一样给函数传参
class Circle {
  final double PI = 3.1415;
  num r;

  Circle(this.r);

  // num area() {
  //   return this.PI * this.r * this.r;
  // }

  // 使用 get 声明的方法,不能有小括号
  num get area {
    return this.PI * this.r * this.r;
  }

  // Setter
  set setR(value) {
    this.r = value;
  }
}

void main() {
  var c = new Circle(100);
  // print(c.area()); // 31415.000000000004
  
  // 通过 Setter 修改属性
  c.setR = 20;
  
  print(c.area); // 1256.6000000000001
}

6.1.5. 初始化列表

  • 作用:在构造函数中设置属性的默认值
  • 时机:在构造函数体执行之前执行
  • 语法:使用逗号分隔初始化表达式
  • 场景:常用于设置 final 常量的值

image.png

class Rect {
  var height;
  var width;

  // Rect(this.height, this.width);

//   Rect([int height = 2, int width = 10]) {
//     this.height = height;
//     this.width = width;
//     print('${this.height} --- ${this.width}');
//   }

//   Rect({int height = 2, int width = 10}) {
//     this.height = height;
//     this.width = width;
//     print('${this.height} --- ${this.width}');
//   }

  // 初始化列表
  Rect()
      : height = 4,
        width = 20 {
    print('${this.height} --- ${this.width}');
  }
}

class Point {
  double x, y, z;

  Point(this.x, this.y, this.z);

  // 初始化列表的特殊用法(重定向构造函数)
  Point.twoD(double x, double y) : this(x, y, 0);
}

void main() {
  var r = new Rect();

  // 实例化点
  var p = new Point(1, 2, 3);
  print(p.z); // 3
  
  var p2 = new Point.twoD(4, 5);
  print(p2.z); // 0
}

6.1.6. static

  • static 关键字用来指定静态成员
    • 通过 static 修饰的属性是静态属性
    • 通过 static 修饰的方法是静态方法
  • 静态成员可以通过类名称直接访问 (不需要实例化)
    • 实例化是比较消耗资源的,声明静态成员,可以提高程序性能
  • 静态方法不能访问非静态成员,非静态方法可以访问静态成员
    • 静态方法中不能使用 this 关键字
    • 不能使用 this 关键字,访问静态属性
class Person {
  static String name = '张三';
  int age = 18;

  static void printInfo() {
    // print(this.name); // 不能通过 this 关键字访问静态属性
    print(name);

    // 静态方法中不能访问非静态属性
    // print(age);
  }

  printUserInfo() {
    // 非静态方法,可以访问静态属性
    print(name);
    print(age);
  }
}

void main() {
  // 静态成员,可以通过类名称直接访问
  print(Person.name); // 张三
  Person.printInfo(); // 张三
  
  // 不能通过类名称,直接访问非静态方法
  // print(Person.printUserInfo());
  
  var p = new Person();
  // print(p.name); // 不能通过实例化对象访问静态属性
  p.printUserInfo();
}

6.1.7. 元数据

  • 元数据以 @ 开头,可以给代码标记一些额外的信息
    • 元数据可以用来库,类,构造器,函数,字段,参数或变量声明的前面
  • @override (重写)
    • 某方法添加该注解后,表示重写了父类中的同名方法
  • @required (必填)
    • 可以通过 @required 来注解 Dart 中的命名参数,用来指示它是必填参数
  • @deprecated (弃用)
    • 若某类或某方法加上该注解之后,表示此方法或类不再建议使用
class Phone {
  // 这是旧版本中的开机方法,会在将来的版本中移除
  @deprecated
  activate(){
    turnOn();
  }
  
  turnOn(){
    print('开机');
  }
}

void main() {
  var p = new Phone();
  p.activate(); // 开机
}

6.2. 继承

  • 根据类的先后顺序,可以将类分成父类子类
  • 子类通过 extends 关键字 继承父类
    • 继承后,子类可以使用父类中,可见的内容 (属性或方法)
  • 子类中,可以通过 @override 元数据来标记“覆写”方法
    • “覆写”方法:子类中与父类中同名的方法
  • 子类中,可以通过 super 关键字来引用父类中,可见的内容
    • 属性
    • 方法(普通构造函数,命名构造函数)
class Father {
  String name = '刘备';
  num money = 10000;

  say() {
    print('我是:$name');
  }
}

class Son extends Father {
  @override
  say() {
    print('我是:刘禅');
  }
}

void main() {
  var f = new Father();
  print(f.name);

  var s = new Son();
  print(s.name);
  print(s.money);
  s.say();
}

接下来演示私有属性和方法

// Father.dart

class Father {
  String name = '刘备';
  num _money = 10000;
  String job;

  Father(this.job);

  // 命名构造函数
  Father.origin(this.job);

  say() {
    print('我是:$name');
  }

  get getMoney {
    return this._money;
  }
}
// Son.dart

import 'Father.dart';

class Son extends Father {
  String name = '刘禅';

  // 通过 super 继承父类的普通构造函数
  Son(String job) : super(job);

  // 继承命名构造函数
  // Son(String job) : super.origin(job);
  Son.origin(String job) : super.origin(job);


  @override
  say() {
    super.say(); // 调用父类中的方法
    print('我是:刘禅,我爹是${super.name},他的工作是${super.job}');
  }
}
// main.dart

import 'lib/Father.dart';
import 'lib/Son.dart';

void main() {
  var f = new Father('皇帝');
  print(f.name);

  // var s = new Son('皇帝');
  var s = new Son.origin('卖草鞋的');
  print(s.name);
  // print(s._money); // 子类不能访问父类中的私有内容
  print(s.getMoney); // 10000
  s.say();
}

6.3. 抽象类

  • 抽象类是用 abstract 关键字修饰的类
  • 抽象类的作用是充当普通类的模板,约定一些必要的属性和方法
  • 抽象方法是指没有方法体的方法
    • 抽象类中一般都有抽象方法,也可以没有抽象方法
    • 普通类中,不能有抽象方法
  • 抽象类不能被实例化 (不能被 new)
  • 抽象类可以被普通类继承 (extends)
    • 如果普通类继承抽象类,必须实现抽象类中所有的抽象方法
  • 抽象类还可以充当接口被实现 (implements)
    • 如果把抽象类当做接口实现的话,普通类必须得实现抽象类里面定义的所有属性和方法
// 1. 抽象类,必须通过 abstract 关键字声明
// 2. 抽象类中,可以有抽象方法,也可以没有抽象方法,一般来说,抽象类都有抽象方法
abstract class Phone {
  // 声明抽象方法
  void processor(); // 手机的处理器

  void camera(); // 手机的摄像头

  void info() {
    print('我是抽象类中的一个普通方法');
  }
}

class Xiaomi extends Phone {
  // 普通类继承了抽象类,就必须实现抽象类中所有的抽象方法
  @override
  void processor() {
    print('晓龙888');
  }

  @override
  void camera() {
    print('1200万');
  }

  // 普通类中不能有抽象方法
  // void aaa();
}

class Huawei extends Phone {
  @override
  void processor() {
    print('麒麟990');
  }

  @override
  void camera() {
    print('徕卡摄像头');
  }
}

void main() {
  // 抽象类,不能被实例化
  // var p = new Phone();

  Xiaomi m = new Xiaomi();
  m.processor();
  m.camera();
  m.info();

  Huawei h = new Huawei();
  h.processor();
  h.camera();
  h.info();
}

6.4. 接口

  • 接口在 Dart 中就是一个类 (只是用法不同)
    • 与Java 不同,Java 中的接口需要用 interface 关键字声明;Dart 中不需要
    • 接口可以是任意类,但一般使用抽象类做接口
  • 一个类可以实现 (implements) 多个接口,多个接口用逗号分隔
    • class MyClass implements Interface1, Interface2 {...}
    • 接口可以看成一个个小零件。类实现接口就相当于组装零件
  • 普通类实现接口后,必须重写接口中所有的属性和方法
// 手机的处理器
abstract class Processor {
  var cores; // 内核:2核,4核
  arch(String name); // 芯片制程:7nm,5nm
}

// 手机的摄像头
abstract class Camera {
  var resolution; // 分辨率:1000万,3000万
  brand(String name); // 品牌:三星,徕卡,蔡司
}

// 通过普通类实现接口
class Phone implements Processor, Camera {
  @override
  var cores;

  @override
  var resolution;

  Phone(this.cores, this.resolution);

  @override
  arch(String name) {
    print('芯片制程:' + name);
  }

  @override
  brand(String name) {
    print('相机品牌:' + name);
  }
}

void main() {
  Phone p = new Phone('4核', '3000万');
  p.arch('5nm');
  p.brand('徕卡');
}

6.5. 混入

  • 混入 (Mixin)是一段公共代码。混入有两种声明方式:
    1. 将类当作混入 class MixinA { ...}
      • 作为 Mixin 的类只能继承自 object,不能继承其他类
      • 作为 Mixin 的类不能有构造函数
    2. 使用 mixin 关键字声明 mixin MixinB {..}
  • 混入(Mixin)可以提高代码复用的效率,普通类可以通过 with 来使用混入
    • class MyClass with MixinA, MixinB {...}
  • 使用多个混入时,后引入的混入会覆盖之前混入中的重复的内容
    • MixinA 和 MixinB 中都有 hello() 方法,MyClass 会使用 MixinB 中的
mixin MixinA {
  String name = 'MixinA';

  void printA() {
    print('A');
  }

  void run() {
    print('A is runing');
  }
}

mixin MixinB {
  String name = 'MixinB';

  void printB() {
    print('B');
  }

  void run() {
    print('B is runing');
  }
}

class MyClass with MixinA, MixinB {}

void main() {
  var c = new MyClass();
  c.printA();
  c.printB();

  // 后引入的混入,会覆盖之前引入的混入中的重复的内容
  print(c.name); // MixinB
  c.run(); // B is runing
}

6.6. 泛型

  • 泛型是在函数接口中指定宽泛数据类型的语法
    • 泛型函数
    • 泛型类
    • 泛型接口
  • 通常,在尖括号中,使用一个字母来代表类型,例如E, T, S, K,和 V 等
    • 返回类型 函数名<输入类型>(参数类型 参数) { 函数体 }
    • T getData<T>(T value) { return value; }
  • 作用:使用泛型可以减少重复的代码

image.png

image.png

6.6.1. 泛型函数

// String getData(String value) {
//   return value;
// }

// int getData(int value) {
//   return value;
// }

// 不指定数据类型的函数
// getData(value) {
//   return value;
// }

// 泛型函数
// T getData<T>(T value) {
//   return value;
// }

// 只约定参数类型,不约定函数返回值的类型
getData<T>(T value) {
  return value;
}

void main() {
  // print(getData('Hello'));
  // print(getData(10));

  // 调用泛型函数
  print(getData<int>(20));
  print(getData<String>('Hello'));
}

6.6.2. 泛型类

class CommonClass {
  Set s = new Set<int>();

  void add(int value) {
    this.s.add(value);
  }

  void info() {
    print(this.s);
  }
}

// 泛型类
class GenericsClass<T> {
  Set s = new Set<T>();

  void add(T value) {
    this.s.add(value);
  }

  void info() {
    print(this.s);
  }
}

void main() {
  CommonClass c = new CommonClass();
  c.add(1);
  c.info();

  // 实例化泛型类
  GenericsClass g = new GenericsClass<int>();
  g.add(1);
  // g.add('2'); // 报错
  g.info();

  GenericsClass g1 = new GenericsClass<String>();
  g1.add('Hello');
  g1.add('World');
  g1.info();

  // Set s = new Set();
  // s.add(1);
  // s.add('Hello');
  // print(s);

  // Set s = new Set<int>();
  // s.add(1);
  // // s.add('Hello'); // 报错
  // print(s);

  // 字面量形式的泛型
  Set s = <int>{};
  s.add(1);
  // s.add('Hello'); // 报错
  print(s);
}

6.6.3. 泛型接口

abstract class ObjectCache {
  getByKey(String key);
  void setByKey(String key, Object value);
}

abstract class StringCache {
  getByKey(String key);
  void setByKey(String key, String value);
}

// 泛型接口
abstract class Cache<T> {
  getByKey(String key);
  void setByKey(String key, T value);
}

// 文件缓存
class FileCache<T> implements Cache<T> {
  @override
  getByKey(String key) {
    return null;
  }

  @override
  void setByKey(String key, T value) {
    print('文件缓存:key=${key} value=${value}');
  }
}

// 内存缓存
class MemoryCache<T> implements Cache<T> {
  @override
  getByKey(String key) {
    return null;
  }

  @override
  void setByKey(String key, T value) {
    print('内存缓存:key=${key} value=${value}');
  }
}

void main() {
  // 文件缓存 - 缓存字符串
  FileCache fc = new FileCache<String>();
  fc.setByKey('foo', 'bar');
  // fc.setByKey('f', 2); // 报错

  // 文件缓存 - 缓存Map
  FileCache<Map> fcm = new FileCache<Map>();
  fcm.setByKey('index', {'name': '张三丰', 'age': 218});

  // 内存缓存 - 缓存字符串
  MemoryCache mc = new MemoryCache<String>();
  mc.setByKey('foo', 'bar');

  // 内存缓存 - 缓存集合
  MemoryCache<Map> mcm = new MemoryCache<Map>();
  mcm.setByKey('home', {'name': '张三丰', 'age': 218});
}

6.6.4. 泛型类型限制

class SomeBaseClass {
  // ...
}

class Foo<T extends SomeBaseClass> {
  String toString() => "Instance of 'Foo<$T>'";
}

// 子类
class Extender extends SomeBaseClass {}

class AnotherClass {
  // ...
}

void main() {
  var someBaseClassFoo = Foo<SomeBaseClass>();
  print(someBaseClassFoo); // Instance of 'Foo<SomeBaseClass>'

  var extenderFoo = Foo<Extender>();
  print(extenderFoo); // Instance of 'Foo<Extender>'

  var foo = Foo();
  print(foo); // Instance of 'Foo<SomeBaseClass>'

  // var f = Foo<AnotherClass>(); // 类型不对,报错
  // print(f);
}

6.7. 枚举

  • 枚举是数量固定的常量值,通过 enum 关键字声明
    • enum Color {red, green, blue}
  • 枚举的 values 常量,可以获取所有枚举值列表
    • List<Color> colors = Color.values;
  • 可以通过 index 获取值的索引
    • assert(Colorgreen.index == 1)
enum Color { red, green, blue }

void main() {
  // 通过 index 返回枚举中具体常量的值
  print(Color.green.index); // 1

  // 通过 values 返回常量列表
  print(Color.values); // [Color.red, Color.green, Color.blue]
  List<Color> colors = Color.values;
  print(colors); // [Color.red, Color.green, Color.blue]

  // 通过下标,访问列表中的内容
  print(colors[0]); // Color.red
  // 通过 forEach 去遍历列表的内容
  colors.forEach((color) => print('value: $color, index: ${color.index}'));
}

7. 库与生态

  • Dart 中的库就是具有特定功能的模块
    • 可能包含单个文件,也可能包含多个文件
  • 按照库的作者进行划分,库可以分成三类
    • 自定义库 (工程师自己写的)
    • 系统库 (Dart 中自带的)
    • 第三方库 (Dart 生态中的)
  • Dart生态
    • pub.dev
    • pub 命令 (D:\flutter\bin\cacheldart-sdk\bin)

image.png

7.1. 自定义库

7.1.1. 通过 library 来声明库

  • 每个 Dart 文件默认都是一个库,只是没有使用 library 来显示声明
void main() {
    print('Hello World');
}

---------------------------------------------------------------------

library main; // 默认隐藏了一个 main 的 library 的声明
void main() {
    print('Hello World');
}
  • Dart 使用_(下划线)开头的标识符,表示库内访问可见 (私有)
  • library 关键字声明的库名称建议使用: 小写字母+下划线
// library MyCustom;
library my_custom; // 建议写成小写字母+下划线的形式

class MyCustom {
  String name = 'MyCustom';
  static num version = 1.0;

  void info() {
    print('我是自定义库');
  }
}

7.1.2. 通过 import 来引入库

  • 不同类型的库,引入方式不同
    • 自定义库 (import '库的位置/库名称.dart')
    • 系统库 (import 'dart:库名称')
    • 第三方库(后面单独讲)
  • 引入部分库 (仅引入需要的内容)
    • 包含引入 (show)
    • 排除引入 (hide)
  • 指定库的前缀
    • 当库名冲突时,可以通过 as 关键字,给库声明一个前缀
  • 延迟引入(懒加载)
    • 使用 deferred as 关键字来标识需要延时加载的库
// 引入自定义库

import 'lib/MyCustom.dart';

void main() {
  MyCustom mc = new MyCustom();
  mc.info(); // 我是自定义库

  print(MyCustom.version); // 1.0
}

7.1.3. 通过 part 和 part of 来组装库

7.2. 系统库的引入

// 引入系统库

import 'dart:math';
// import 'dart:core'; // core 库会被默认引入

void main() {
  print(pi); // 3.141592653589793

  print(min(3, 6)); // 3
  print(max(3, 6)); // 6
}

7.3. 引入部分库

// 部分引入

void f1() {
  print('f1 is running');
}

void f2() {
  print('f2 is running');
}

void f3() {
  print('f3 is running');
}

-------------------------------------------------------------------------

// show 后面指定包含引入的内容
import 'lib/common.dart' show f1, f3;

void main() {
  f1();

  // f2();

  f3();
}

-------------------------------------------------------------------------

// hide 会隐藏后面的内容
import 'lib/common.dart' hide f1, f3;

void main() {
  // f1();

  f2();

  // f3();
}

7.4. 引入冲突处理

import 'lib/common.dart';
import 'lib/function.dart' as func; // 给库添加前缀,解决命名冲突问题

void main() {
  f1(); // f1 is running

  func.f1(); // f1 is of function running
}

7.5. 延迟引入

import 'lib/function.dart' deferred as func;

void main() {
  // func.hello(); // 直接调用会报错

  print(1);
  greet();
  print(2);
  print(3);
}

Future greet() async {
  await func.loadLibrary();
  func.hello(); // Hello World
}

7.6. part 与 part of

image.png

7.7. 系统库

  • 系统库(也叫核心库)是 Dart 提供的常用内置库
    • 不需要单独下载,就可以直接使用
  • 引入
    • import 'dart:库名';
    • import 'dart:core'; // 会自动引入(无需手动引入)
  • 系统库列表
void main() {
  // 创建当前时间
  var now = new DateTime.now();
  print(now); // 2023-11-16 11:00:43.071

  // 通过普通构造函数创建时间
  var d = new DateTime(2021, 1, 20, 9, 30);
  print(d); // 2021-01-20 09:30:00.000

  // 创建标准时间
  var d1 = DateTime.parse('2021-01-20 12:30:30');
  print(d1); // 2021-01-20 12:30:30.000
  var d2 = DateTime.parse('2021-01-20 12:30:30+0800');
  print(d2); // 2021-01-20 04:30:30.000Z

  // 时间增量
  print(now.add(new Duration(hours: 2))); // 2023-11-16 13:00:43.071
  print(now.add(new Duration(hours: -3))); // 2023-11-16 08:00:43.071

  // 时间比较
  print(d1.isAfter(d2)); // d1 是否在 d2 之后
  print(d1.isBefore(d2)); // d1 是否在 d2 之前

  // 时间差
  var d3 = new DateTime(2021, 1, 1);
  var d4 = new DateTime(2021, 1, 4);
  var difference = d3.difference(d4);
  print([difference.inDays, difference.inHours]); // [-3, -72] d3 与 d4 相差的天数与小时
  
  // 时间戳
  print(now.millisecondsSinceEpoch); // 1700104215379 单位毫秒,13位时间戳
  print(now.microsecondsSinceEpoch); // 1700104215379000 单位微妙,16位时间戳
  
  // 格式化
  print(now.month.toString().padLeft(2, '0')); // 11
  
  String timestamp = "${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}";
  print(timestamp); // 2023-11-16 11:16
}

7.8. 第三方库

  • 来源
  • 使用
    • 在项目目录下创建 pubspec.yaml
    • pubspec.yaml 中声明第三方库(依赖)
    • 命令行中进入 pubspec.yaml 所在目录,执行 pub get 进行安装
    • 项目中引入已安装的第三方库(import 'package:xxxx/xxxx.dart';
  • 第三方库的结构