1. 认识Dart
Google为Flutter选择了Dart语言已经是既定的事实,无论你多么想用你熟悉的语言,比如JavaScript、TypeScript、ArkTS等来开发Flutter,至少目前都是不可以的。
- Dart 是由谷歌开发的计算机编程语言,它可以被应用于 Web/服务器/移动应用和物联网等领域开发。
- Dart 也是 Flutter 的基础,Dart 作为 Flutter 应用程序的编程语言,为驱动应用运行提供了环境。
- 因此, 要学习Flutter, 则首先得会Dart, Dart 目前最新稳定版本:v3.4.0
好消息:如果你会 JavaScript、 Typescript、Java 中任何一门语言,你将很快且很容易就能学会Dart的使用。
2. 搭建Dart开发环境
为什么需要安装Dart呢?
事实上如果提前安装了Flutter SDK,它已经内置了Dart了,我们完全可以直接使用Flutter去进行Dart的编写并且运行。
但是,如果想单独学习Dart,并且运行自己的Dart代码,最好去安装一个Dart SDK。
要在本地开发Dart程序, 首先需要安装Dart SDK
官方文档: Get the Dart SDK | Dart , 中文文档: Dart 编程语言主页 | Dart 中文文档 | Dart
无论是什么操作系统,安装方式都是有两种:通过工具安装
和直接下载SDK,配置环境变量
a.通过工具安装
- Windows可以通过Chocolatey
- macOS可以通过homebrew
- 具体安装操作官网网站有详细的解释
b.直接下载SDK,配置环境变量
- 下载地址:https://dart.dev/tools/sdk/archive
- 我采用了这个安装方式。
- 下载完成后,根据路径配置环境变量即可。
1. Windows环境安装Dart
1.1 下载压缩包, 然后解压放在任意盘符下(注意不要是中文目录下)
1.2 找到bin目录, 复制完整路径, 配置环境变量
1.3 cmd 窗口执行 dart --version 看到dart-sdk的版本代表OK
2. Mac环境安装Dart
- macOS支持的架构: x64、ARM64
2.1 下载并解压Dart SDK,放到电脑的某个目录下面(不要是中文)
2.2 配置环境变量
打开终端,使用文本编辑器(如vi
或nano
)编辑~/.zshrc
和~/.bash_profile
文件。在文件的末尾添加以下行,将Dart SDK的路径添加到系统的PATH中:
在终端中运行vim ~/.zshrc
命令
export PATH="/path/to/dart/sdk/bin:$PATH"
注意:
- 将
/path/to/dart/sdk/bin
替换为你解压Dart SDK的实际路径。 - 保存并关闭文件:保存并关闭文本编辑器(按下esc,输入:wq)
2.3 使环境变量生效
在终端中运行source ~/.zshrc
命令,使新的环境变量设置立即生效。
2.4 验证安装
运行dart --version
命令来检查Dart的版本信息,如果正确显示版本信息,则表示Dart已经成功安装。
3. Dart初体验
3.1 VSCode中安装常用插件
- Dart插件:可以帮助我们在VSCode中更加高效的编写Dart代码
提供了友好的代码提示,代码高亮,以及代码的重构、运行和重载
- Flutter插件:为Flutter开发准备的
- Code Runner:可以点击右上角的按钮快速运行代码
3.2 第一个Dart程序
- 创建dart文件
- dart文件名必须以
.dart
结尾:01-第一个dart程序.dart
- 编写dart代码
需求:打印字符串 'hello dart!'
// 程序的入口:main函数
void main() {// 需求:打印字符串 'hello itcast'print('hello itcast'); // hello itcast
}
- 执行dart代码
- 方式一:终端中执行:终端打开dart文件所在目录
dart 01-dart初体验.dart
- 方式二:
-
- VSCode中执行
-
- VSCode中查看代码执行结果
小结:
1、Dart语言的入口也是main函数,并且必须显示的进行定义;
2、Dart的入口函数main
是没有返回值的;
3、定义字符串的时候,可以使用单引号或双引号;
4、每行语句必须使用分号结尾,很多语言并不需要分号,比如JavaScript;
4. Dart基础语法
4.1 变量和常量(存储数据)
4.1.1 变量(存储并修改数据)
需求:存储一个姓名和年龄并修改年龄?
实现:变量关键字:var
要点:
- 声明变量:var 变量名 = 表达式;
- 修改变量:变量名 = 新值;
- 类型推断:var关键字声明的变量支持类型推断,修改变量时会检查之前存储数据的类型
void main() {// 1. 声明变量:var 变量名 = 表达式;var name = 'itheima';print(name); // itheimavar age = 17;print(age); // 17// 2. 修改变量:变量名 = 新值;age = 18;print(age); // 18// 3. 类型推断:var关键字声明的变量支持类型推断,修改变量时会检查之前存储数据的类型// 报错:A value of type 'String' can't be assigned to a variable of type 'int'.// age = 'itcast';
}
4.1.2 常量(存储不变的数据)
需求:存储不可被修改的数据
实现:常量关键字:const
和 final
区别:final是运行时常量,值在运行时赋值;const是编译期常量,值在编译时赋值;
void main() {// 1. const声明常量const num1 = 10;print(num1); // 10// 报错:Constant variables can't be assigned a value.// num1 = 20;// 2. final声明常量final num2 = 30;print(num2); // 30// 报错:The final variable 'num2' can only be set once.// num2 = 40;// 3. const和final的区别:var x = 1;var y = 2;// 3.1 final:运行时常量,值在运行时赋值final ret1 = x + y;print(ret1);// 3.2 const:编译期常量,值在编译时赋值// 报错:Const variables must be initialized with a constant value.// const ret2 = x + y;const ret3 = 1 + 2;
}
注意: const 和 final的区别
final:运行时常量,值在运行时赋值
const:编译期常量,值在编译时赋值
4.2 数据类型(可存储数据类型)
4.2.1 num(数字)
需求:存储并修改整数和小数
实现:关键字:num
、int
、double
注意点:
num类型的变量既可以存整数也可以存小数
int类型的值可以赋值给double类型的变量,但是double类型的值不能赋值给int类型的变量
void main() {// 1. num类型 (整数、小数)num n1 = 10;print(n1); // 10n1 = 10.1;print(n1); // 10.1// 2. int类型 (整数)int n2 = 20;print(n2); // 20// 测试:将小数赋值给int// 报错:A value of type 'double' can't be assigned to a variable of type 'int'.// n2 = 20.2;// 3. double类型 (小数)double n3 = 30.3;print(n3); // 30.3// 测试:将整数赋值给doublen3 = 30;print(n3); // 30.0
}
4.2.2 String(字符串)
Dart字符串是UTF-16编码单元的序列。您可以使用单引号或双引号创建一个字符串
需求:声明字符串,修改字符串,拼接字符串
注意:模板字符串支持运算
实现:关键字:String
void main() {// 1. 定义字符串String name = '张三';String gender = "男";// 2. 修改字符串name = '李四';gender = '女';print(name);print(gender);// 3. 字符串拼接double money = 99991.99;String intro = '姓名:$name,性别:$gender,钱包:${money * 100}';print(intro);// 4. 字符串换行String tag = '''通知明天天气不好注意放假''';print(tag);
}
4.2.3 bool(布尔)
需求:存储并修改真或假
实现:关键字:bool
void main() {// 登录成功:truebool isLogin = true;print(isLogin); // true// 登录失败:falseisLogin = false;print(isLogin); // false
}
4.2.4 List(列表)
需求:使用一个变量有序的存储多个值
实现:列表(数组)关键字:List
- 定义列表 List 变量名 = [元素1, 元素2, ..., 元素n];
-
- 1.1 需求:按序存储数字 1 3 5 7
- 1.2 需求:按序存储字符串 '居家' '美食' '服饰'
- 1.3 列表中可以存储任意类型的数据
void main() {// 1. 定义列表:List 变量名 = [元素1, 元素2, ..., 元素n];// 1.1 需求:按序存储数字 1 3 5 7List nums = [1, 3, 5, 7];print(nums);// 1.2 需求:按序存储字符串 '居家' '美食' '服饰'List categories = ['居家', '美食', '服饰'];print(categories);// 1.3 列表中可以存储任意类型的数据List ret = [18,18.8,'美食',true,['itcast', 10],{}];print(ret);
}
- 使用列表:查改增删
-
- 查询列表长度
- 查询指定的元素
- 修改:列表[索引] = 新值
- 新增:列表.add(新元素)、列表.addAll(新列表)
- 指定位置添加:列表.insert(索引, 内容');
- 删除:使用元素删除、使用索引删除
- 遍历列表:读取出列表中每一个元素remove,removeAt
// 2. 使用列表:查改增删
// 2.1 查询:
// 查询列表的长度(列表内部元素的个数)
int len = categories.length;
print(len); // 3
// 查询指定的元素: 列表[索引]
String category = categories[1];
print(category); // 美食// 2.2 修改:列表[索引] = 新值
categories[1] = '美食家';
print(categories); // [居家, 美食家, 服饰]// 2.3 新增:
// 一次新增一个元素:列表.add(新元素)
categories.add('母婴');
print(categories); // [居家, 美食家, 服饰, 母婴]
// 一次新增多个元素:列表.addAll(新列表)
categories.addAll(['办公', '生鲜']);
print(categories); // [居家, 美食家, 服饰, 母婴, 办公, 生鲜]
// 在指定位置新增元素
categories.insert(2, '教育');
print(categories); // [居家, 美食家, 教育, 服饰, 母婴, 办公, 生鲜]// 2.4 删除
// 使用元素删除
categories.remove('生鲜');
print(categories); // [居家, 美食家, 教育, 服饰, 母婴, 办公]
// 使用索引删除
categories.removeAt(4);
print(categories); // [居家, 美食家, 教育, 服饰, 办公]
- 遍历列表:读取出列表中每一个元素
// 3. 遍历列表:读取出列表中每一个元素
// element: 列表中读取出来的每一个元素
categories.forEach((element) {print(element);
});
4.2.5 Map(字典)
需求:声明键值对的集合
,并使用与之相关联的键从中获取值(类似JS中的对象)
实现:字典关键字:Map
- 存储商品分类的编号 和 名称
- 对字典数据进行查改增删
2.1 查询:字典[key]
2.2 修改:字典[key] = 新值
2.3 新增:字典[新key] = 新值 (注意:key必须是当前字典中不存在的key,如果key已存在就是修改)
2.4 删除:remove(key) (注意:如果key不存在,不会报错,也不会执行删除操作)
- 遍历字典
- 定义字典语法
Map 变量名 = {'键1': 值1,'键2': 值2,...,
};
- 例子:存储商品分类的编号 和 名称
void main() {// 1. 例子:存储商品分类的编号 和 名称Map category = {'id': 1,'name': '居家',};print(category); // {id: 1, name: 居家}
}
- 使用字典:查改增删
// 2. 使用字典:查改增删
// 2.1 查询:字典[key]
String name = category['name'];
print(name); // 居家// 2.2 修改:字典[key] = 新值
category['name'] = '美食';
print(category); // {id: 1, name: 美食}// 2.3 新增:字典[新key] = 新值
// 注意:key必须是当前字典中不存在的key,如果key已存在就是修改
category['price'] = 199.9;
print(category); // {id: 1, name: 美食, price: 199.9}// 2.4 删除:remove(key)
// 注意:如果key不存在,不会报错,也不会执行删除操作
category.remove('name');
print(category); // {id: 1, price: 199.9}
- 遍历字典
// 3. 遍历字典
category.forEach((key, value) {print('$key -- $value');// id -- 1// price -- 199.9
});
4.2.6 Dart空安全机制
如何尽早的发现并解决null带来的异常?
Dart提供了健全的空安全机制
,默认所有的变量都是非空的,如果某个变量得到了一个null,则代码在编译期就会报错
- 无法正常执行的代码:在代码编译期就会报错
- 解决办法:使用 ? 显示的指定变量可以为空
- 使用可以为空的变量
void main() {// 1. 可以正常执行的代码String name1 = 'itcast';print(name1.length);// 2. 无法正常执行的代码:在代码编译期就会报错// String name2;// The non-nullable local variable 'name2' must be assigned before it can be used.// 报错:Null check operator used on a null value// print(name2.length);// 3. 解决办法:使用 ? 显示的指定变量可以为空String? name3 = null;print(name3);// 4. 使用可以为空的变量// name3? : 表示非空检查,如果name3为空,不去调用属性或方法,如果name3不为空,就去调用属性或方法print(name3?.length);
}
4.3 运算符(数据如何做运算)
4.3.1 算术运算符
如何对数字做加减乘除等运算?
多了取整 ~/
void main() {int n1 = 10;int n2 = 3;// 加 +print(n1 + n2); // 13// 减 -print(n1 - n2); // 7// 乘 *print(n1 * n2); // 30// 除 /print(n1 / n2); // 3.3333333333333335// 取整:取除法结果的整数部分 ~/print(n1 ~/ n2); // 3// 取模:取除法结果的余数 %print(n1 % n2); // 1// 案例:计算购物车商品总价格:商品A一件,每件289.0元;商品B二件,每件39.0元double total = 289.0 + (2 * 39.0);print(total); // 367.0
}
4.3.2 赋值运算符(同TS)
如何对数字做赋值运算?
void main() {// 等于 =int n1 = 10;// 加等于 +=// n1 = n1 + 5;n1 += 5;print(n1); // 15// 减等于 -=n1 -= 5;print(n1); // 10// 乘等于 *=n1 *= 5;print(n1); // 50// 除等于 /=// 注意:double类型的数据才能做除等于的操作// A value of type 'double' can't be assigned to a variable of type 'int'.// n1 /= 5;double n2 = 50;n2 /= 5;print(n2); // 10.0// 取余等于 %=int n3 = 10;n3 %= 3;print(n3); // 1// 自增:在原有数值上加1 ++int a = 10;a++;print(a); // 11// 自减:在原有数值上减1 --int b = 20;b--;print(b); // 19
}
4.3.3 比较运算符
如何比较数字大小?
没有 === 和 !== 运算符
void main() {int n1 = 10;int n2 = 20;// 大于 >print(n1 > n2); // false// 小于 <print(n1 < n2); // true// 大于等于 >=print(n1 >= n2); // false// 小于等于 <=print(n1 <= n2); // true// 等于 ==print(n1 == n2); // falseprint('itcast' == 'itheima'); // false// 不等于 !=print(n1 != n2); // trueprint('itcast' != 'itheima'); // true
}
4.3.4 逻辑运算符(同TS)
如果表示数据之间的逻辑关系?
void main() {// 年龄int age = 33;// 工作年限int years = 10;// 1. 逻辑与:一假则假// 年龄大于28岁,并且工作年限大于4年bool ret1 = age > 35 && years > 4;print(ret1); // false// 2. 逻辑或:一真则真// 年龄大于23岁,或者工作年限大于2年bool ret2 = age > 35 || years > 2;print(ret2); // true// 3. 逻辑非:真变假,假变真print(!true); // falseprint(!false); // true// 工作年限不小于9年bool ret3 = years >= 9;// bool ret3 = !(years < 9);print(ret3); // true
}
4.4 流程控制(选择或重复执行)
4.1.1 if分支语句(同TS)
单分支、双分支、多分支
void main() {// 1. if单分支语句// 准备高考成绩,如果分数大于等于700分,则输出 '恭喜考入黑马程序员'int score1 = 699;if (score1 >= 700) {print('恭喜考入黑马程序员');}// 2. if双分支语句// 准备高考成绩,如果分数大于等于700分,则输出 '恭喜考入黑马程序员',反之,则输出 '继续努力'int score2 = 699;if (score2 >= 700) {print('恭喜考入黑马程序员');} else {print('继续努力');}// 3. if多分支语句// 根据学生分数划分学生成绩等级:// 优秀:分数大于等于90分// 良好:分数小于90分,且大于等于80分// 中等:分数小于80分,且大于等于60分// 不及格:分数小于60分int score3 = 58;if (score3 >= 90) {print('优秀');} else if (score3 >= 80) {print('良好');} else if (score3 >= 60) {print('中等');} else {print('不及格');}
}
4.1.2 三元运算符(同TS)
简化简单的if双分支语句
void main() {// 需求:准备高考成绩,如果分数大于等于700分,则输出 '恭喜考入黑马程序员',反之,则输出 '继续努力'// 思考:以下代码可以简化吗?int score1 = 699;// if (score1 >= 700) {// print('恭喜考入黑马程序员');// } else {// print('继续努力');// }// 1. 使用三元运算符简化if双语句:条件表达式 ? 表达式1 : 表达式2score1 >= 700 ? print('恭喜考入黑马程序员') : print('继续努力');// 2. 思考:以下代码适合使用三元运算符改写吗?int score2 = 88;if (score2 >= 90) {print('优秀');} else if (score2 >= 80) {print('良好');} else if (score2 >= 60) {print('中等');} else {print('不及格');}
}
4.1.3 switch case 语句(同TS)
如果分支很多,且条件是判断相等,则switch case 语句性能比 if 分支语句要好
void main() {// 根据订单状态,打印出订单状态描述信息// 订单状态:1为待付款、2为待发货、3为待收货、4为待评价int orderState = 3;switch (orderState) {case 1:print('待付款');break;case 2:print('待发货');break;case 3:print('待收货');break;case 4:print('待评价');break;default:print('其他');}
}
4.1.4 循环语句(同TS)
如何让代码重复执行?
循环语句:while循环
和 for循环
void main() {// 1. while循环// 重复打印10次 '月薪过万'int n = 0;while (n < 10) {print('$n -- 月薪过万');n++;}// 2. for循环// 重复打印5次 '李白姓白'for (var i = 0; i < 5; i++) {print('$i -- 李白姓白');}// 3. 使用循环遍历列表// 3.1 遍历列表:for循环List categories = ['居家', '美食', '服饰'];for (var i = 0; i < categories.length; i++) {String name = categories[i];print(name);}// 3.2 遍历列表:for ... infor (var item in categories) {// item就是遍历出来的元素print(item);}// 4. 终止循环// 4.1 break:中断整个循环for (var i = 0; i < 5; i++) {if (i == 2) {// 吃到第三个苹果发现了虫子,剩下的苹果没胃口都不吃了break;}print('我把第 ${i + 1} 个苹果吃了');}// 4.2 continue:跳过本次循环直接进入下一次循环for (var i = 0; i < 5; i++) {if (i == 2) {// 吃到第三个桃子发现了虫子,第三个桃子不吃了,剩下的桃子接着吃continue;}print('我把第 ${i + 1} 个桃子吃了');}
}
4.5 基础语法 - 综合案例
需求:计算购物车数据中,被勾选商品的总价
void main() {// 准备购物车数据List carts = [{"count": 2, "price": 10.0, "selected": true},{"count": 1, "price": 30.0, "selected": false},{"count": 5, "price": 20.0, "selected": true}];// 记录总金额double totalAmount = 0.0;// 遍历购物车数据carts.forEach((element) {// 读取商品的勾选状态bool selected = element['selected'];// 如果商品被勾选 ,读取该商品的单价和数量,并计算价格小计if (selected) {double amount = element['count'] * element['price'];// 累加价格小计,计算总价totalAmount += amount;}});print(totalAmount);
}
五、函数(复用代码)
5.1 函数的定义
- 定义函数:无参数无返回值函数
- 定义函数:有参数有返回值函数
- 函数的特点:
- 返回值类型和参数类型是可以省略的
void main() {// 2. 调用无参数无返回值函数func();// 4.调用有参数有返回值函数 int ret = sum(10, 20);print(ret); // 30
}// 1. 定义函数:无参数无返回值函数
void func() {print('这是一个无参数无返回值函数');
}// 3. 定义函数:有参数有返回值函数
// 需求:定义函数,计算任意两个整数的和,并返回计算结果
int sum(int a, int b) {int ret = a + b;return ret;
}
- 函数的特点:
-
- 返回值类型和参数类型是可以省略的
void main() {// 2. 调用无参数无返回值函数// func();// 4. 调用有参数有返回值函数int ret = sum(10, 20);print(ret); // 30
}// 1. 定义函数:无参数无返回值函数
func() {print('这是一个无参数无返回值函数');
}// 3. 定义函数:有参数有返回值函数
// 需求:定义函数,计算任意两个整数的和,并返回计算结果
// 特点2:返回值类型和参数类型是可以省略的
sum(a, b) {int ret = a + b;return ret;
}
5.2 函数的参数
函数的参数可以分为:必传参数(位置参数)、可选参数(关键字参数)
注意点:必传参数不能为空,可选参数可以为空,且参数都可以设置默认值
void main() {printString('张三丰');printString('李四', age: 18);printString('王五', location: '昌平区');printString('赵六', age: 18, location: '海淀区');
}/*** name:必传参数* age:可选参数* location:可选参数,并有默认值*/
void printString(String name, {int? age, String? location = '昌平区'}) {print('$name - $age - $location');
}
5.3 函数对象
函数可以作为对象赋值给其他变量
函数可以作为参数传递给其他函数
void main() {// 1.2 定义一个变量接收函数// var f = funcDemo1;Function f = funcDemo1;f();// 2.2 函数作为参数funcDemo2(funcDemo3);
}// 1.1 函数可以作为对象赋值给其他变量
void funcDemo1() {print('funcDemo1');
}// 2.1 函数可以作为参数传递给其他函数
void funcDemo2(Function func) {// 调用外界传入的函数func();
}// 定义作为参数的函数: 把funcDemo3传入到funcDemo2
void funcDemo3() {print('funcDemo3');
}
5.4 匿名函数
- 匿名函数赋值给变量,并调用
- 可以作为参数传递给其他函数去调用(回调函数)
void main() {// 匿名函数// 1. 匿名函数赋值给变量,并调用Function f = () {print('这是一个匿名函数');};f();// 2. 可以作为参数传递给其他函数去调用(回调函数)funcDemo(() {print('这个匿名函数是个参数');});
}// 定义一个接收函数作为参数的函数
void funcDemo(Function func) {func();
}
5.5 箭头函数
当函数体只有一行代码时,可以使用箭头函数简写
void main() {int ret1 = sum1(10, 20);print(ret1);int ret2 = sum2(30, 40);print(ret2);
}// 思考:以下代码可以简写吗?
sum1(a, b) {return a + b; // 函数体只有一行代码
}// 箭头函数简写函数体:简写只有一行代码的函数体
sum2(a, b) => a + b;
5.6 函数 - 综合案例
需求:计算购物车中商品是否全选
void main() {// 准备购物车数据List carts = [{"count": 2, "price": 10.0, "selected": true},{"count": 1, "price": 30.0, "selected": false},{"count": 5, "price": 20.0, "selected": true}];// 调用封装的函数bool isSelectedAll = getSelectedState(carts);if (isSelectedAll) {print('全选');} else {print('非全选');}
}// 核心逻辑:只要有任何一个商品是未勾选的,那么就是非全选
bool getSelectedState(List carts) {// 购物车初始的状态:假设默认是全选bool isSelectedAll = true;carts.forEach((element) {bool selected = element['selected'];// 核心代码:只要有任何一个商品是非勾选的,则购物车就是非全选if (selected == false) {isSelectedAll = selected;}});// 返回是否全选结果return isSelectedAll;
}
六、类(面向对象编程)
6.1 类的定义
需求:定义Person类,属性:名字和年龄,方法:吃饭
void main() {// 创建Person对象Person person = Person();// 1.1 属性赋值person.name = 'itheima';person.age = 17;// 1.2 读取属性print(person.name); // itheimaprint(person.age); // 17// 2.1 调用方法person.eat(); // 我是干饭人
}// 定义Person类,属性:名字和年龄,方法:吃饭
class Person {// 1. 属性String? name;int? age;// 2. 方法void eat() {print('我是干饭人');}
}
存在问题: 我们new一个对象时, 不能直接给对象绑定属性?
答案: 借助构造函数
6.2 构造函数
6.2.1 默认构造函数
无参数,默认隐藏
void main() {// 使用默认的构造函数创建对象Person person1 = Person();// 属性赋值person1.name = '张三';person1.age = 18;// 读取属性print(person1.name); // 张三print(person1.age); // 18// 调用方法person1.eat();
}// 定义Person类,属性:名字和年龄,方法:吃饭
class Person {// 默认的构造函数(无参数,默认隐藏)Person() {print('我是默认的构造函数');}// 属性String? name;int? age;// 方法void eat() {print('我是干饭人');}
}
6.2.2 自定义与类同名构造函数
自定义与类同名的构造函数时,可以有参数
注意点:与类同名的构造函数只能有一个,如果自定义了该类名构造函数,那么默认的构造函数就失效
void main() {// 使用自定义与类同名构造函数创建对象Person person2 = Person('李四', 19);// 读取属性print(person2.name); // 李四print(person2.age); // 19// 调用方法person2.eat();
}// 定义Person类,属性:名字和年龄,方法:吃饭
class Person {// 自定义与类同名构造函数:可以有参数// Person(String name, int age) {// this.name = name;// this.age = age;// }// 简写自定义与类同名构造函数:自定义与类同名构造函数时,如果函数的参数和类的属性同名可以简写Person(this.name, this.age);// 属性String? name;int? age;// 方法void eat() {print('我是干饭人');}
}
6.2.3 命名构造函数
实际开发中, 经常会发现这么一种写法: 类名.方法(参数...)
, 然后返回一个实例化对象, 这种写法在Dart中被称为命名构造函数
void main() {// 使用命名构造函数创建对象Person person3 = Person.withInfo('王五', 20);// // 读取属性print(person3.name); // 王五print(person3.age); // 20// 调用方法person3.eat();
}// 定义Person类,属性:名字和年龄,方法:吃饭
class Person {// 定义命名构造函数// Person.withInfo(String name, int age) {// this.name = name;// this.age = age;// }// 简写命名构造函数Person.withInfo(this.name, this.age);// 属性String? name;int? age;// 方法void eat() {print('我是干饭人');}
}
6.3 私有属性和方法
公有属性和方法:供类自身或者其他外部文件和类使用的属性和方法
私有属性和方法:仅供自身使用的属性和方法,其他外部文件和类无法访问
class Dog {// 公有属性String? name;// 私有属性int? _age;// 公有方法void eat() {print('dog eat');}// 私有方法void _run() {print('dog run');}
}
其他dart文件中使用:28-类-私有属性和方法2-使用.dart
// 导入Dart文件、库
import 'lib/Dog.dart';void main() {// 创建Dog对象Dog dog = Dog();dog.name = '旺财';print(dog.name);// 私有属性调用失败// print(dog._age);dog.eat();// 私有方法调用失败// dog._run();
}
6.4 继承(extends)
- [思考]:如下定义的的两个类
Man
和Woman
是否有重复的部分,是否可以优化?
void main() {// 创建男人对象Man man = Man('李雷', 13);print(man.name);man.eat();// 创建女人对象Woman woman = Woman('韩梅梅', 14);print(woman.name);woman.eat();
}/// 男人类
class Man {Man(this.name, this.age);String? name;int? age;void eat() {print('$name -- eat');}
}/// 女人类
class Woman {Woman(this.name, this.age);String? name;int? age;void eat() {print('$name -- eat');}
}
- 使用继承优化代码:继承的基本使用
[思考]:如下定义的的两个类Man和Woman是否有重复的部分,是否可以优化?
[解决]:定义父类Person
,用于封装公共的属性和方法,Man
和Woman
类作为子类去继承父类Person
的属性和方法
void main() {// 创建男人对象Man man = Man('李雷', 13);print(man.name);man.eat();// 创建女人对象Woman woman = Woman('韩梅梅', 14);print(woman.name);woman.eat();
}/// 人类:父类
class Person {Person(this.name, this.age);String? name;int? age;void eat() {print('$name -- eat');}
}/// 男人类:子类
class Man extends Person {// 定义子类构造函数// Man(String name, int age) : super(name, age);Man(super.name, super.age);
}/// 女人类:子类
class Woman extends Person {// 定义子类构造函数Woman(super.name, super.age);
}
- 提示:子类中可以重写父类的方法
void main() {// 创建男人对象Man man = Man('李雷', 13);print(man.name);man.eat();// 创建女人对象Woman woman = Woman('韩梅梅', 14);print(woman.name);woman.eat();
}/// 人类:父类
class Person {Person(this.name, this.age);String? name;int? age;void eat() {print('$name -- eat');}
}/// 男人类:子类
class Man extends Person {// 定义子类构造函数// Man(String name, int age) : super(name, age);Man(super.name, super.age);// 提示:子类中可以重写父类的方法,执行子类自己的逻辑@overridevoid eat() {print('我是$name,我爱吃肉');}
}/// 女人类:子类
class Woman extends Person {// 定义子类构造函数Woman(super.name, super.age);// 提示:子类中可以重写父类的方法,执行子类自己的逻辑@overridevoid eat() {print('我是$name,我爱吃蔬菜');}
}
6.5 混入(mixin、with)
- [思考]:以下代码如何让子类
Woman
也有唱歌的方法?
void main() {// 创建男人对象Man man = Man('李雷', 13);print(man.name);man.eat();man.sing();// 创建女人对象Woman woman = Woman('韩梅梅', 14);print(woman.name);woman.eat();
}/// 人类
class Person {Person(this.name, this.age);String? name;int? age;void eat() {print('$name -- eat');}
}/// 男人类
class Man extends Person {// 定义子类构造函数Man(super.name, super.age);// 唱歌的方法void sing() {print('$name -- 爱唱歌');}
}/// 女人类
class Woman extends Person {// 定义子类构造函数Woman(super.name, super.age);
}
- 使用Mixin扩展优化代码:Mixin扩展的基本使用
[思考]:如何让子类Woman
也有唱歌的方法?
[解决]:
方式1:将唱歌的方法,定义到子类Woman中(代码冗余)
方式2:将唱歌的方法,定义到父类Person中(代码扩展性不好,唱歌的方法不能被其他类复用)
方式3:使用mixin扩展一个类,扩展类中定义唱歌的方法
[mixin] 表示一个没有构造函数的类,这个类的方法可以组合到其他类中实现代码复用
[mixin] 可以同时对某个类设置多个扩展类,也可以扩展属性
void main() {// 创建男人对象Man man = Man('李雷', 13);// 继承的属性和方法print(man.name);man.eat();// 通过mixin扩展的方法man.sing(man.name);// 创建女人对象Woman woman = Woman('韩梅梅', 14);// 继承的属性和方法print(woman.name);woman.eat();// 通过mixin扩展的方法woman.sing(woman.name);woman.dance(woman.name);woman.danceType = '街舞';print(woman.danceType);
}/// 人类:父类
class Person {Person(this.name, this.age);String? name;int? age;void eat() {print('$name -- eat');}
}// 唱歌方法的扩展类
mixin SingMixin {// 唱歌的方法void sing(name) {print('$name -- 爱唱歌');}
}// 跳舞方法的扩展类
mixin DanceMixin {// 舞种String? danceType;// 跳舞的方法void dance(String? name) {print('$name -- 爱跳舞');}
}/// 男人类:子类
class Man extends Person with SingMixin {// 定义子类构造函数Man(super.name, super.age);
}/// 女人类:子类
class Woman extends Person with SingMixin, DanceMixin {// 定义子类构造函数Woman(super.name, super.age);
}
七、异步编程
7.1 Dart是单线程的
7.1.1 程序中的耗时操作
开发中的耗时操作
- 在开发中,我们经常会遇到一些耗时的操作需要完成,比如网络请求、文件读取等等;
- 如果我们的主线程一直在等待这些耗时的操作完成,那么就会进行阻塞,无法响应其它事件,比如用户的点击;
- 显然,我们不能这么干!!
如何处理耗时的操作呢?
- 针对如何处理耗时的操作,不同的语言有不同的处理方式。
- 处理方式一: 多线程,比如Java、C++、鸿蒙中,我们普遍的做法是开启一个新的线程(Thread),在新的线程中完成这些异步的操作,再通过线程间通信的方式,将拿到的数据传递给主线程。
- 处理方式二: 单线程+事件循环,比如JavaScript、Dart都是基于单线程加事件循环来完成耗时操作的处理。
接下来, 我们一起来看看在Dart中如何去处理一些耗时的操作吧!
7.2 Future
7.2.1 同步网络请求
我们先来看一个例子吧:
- 在这个例子中,我使用getNetworkData来模拟了一个网络请求;
- 该网络请求需要5秒钟的时间,之后返回数据;
import 'dart:io';void main() {print('开始执行main函数');print(getNetworkData());print('这是不能被阻塞的代码');
}String getNetworkData() {sleep(Duration(seconds: 5));return '返回的网络数据';
}
这段代码会运行怎么的结果呢?
- getNetworkData会阻塞main函数的执行
开始执行main函数
// 等待5秒
返回的网络数据
这是不能被阻塞的代码
显然,上面的代码不是我们想要的执行效果,因为网络请求阻塞了main函数,那么意味着其后所有的代码都无法正常的继续执行。
7.2.2 Future基本使用
我们来对上面的代码进行改进,代码如下:
- 和刚才的代码唯一的区别在于使用了Future对象来将耗时的操作放在了其中传入的函数中;
- 稍后,我们会讲解它具体的一些API,我们就暂时知道我创建了一个Future实例即可;
import 'dart:io';void main() {print('开始执行main函数');print(getNetworkData());print('这是不能被阻塞的代码');
}Future<String> getNetworkData() {return Future<String>(() {sleep(Duration(seconds: 5));return '返回的网络数据';});
}
我们来看一下代码的运行结果:
- 这一次的代码顺序执行,没有出现任何的阻塞现象;
- 和之前直接打印结果不同,这次我们打印了一个Future实例;
- 结论:我们将一个耗时的操作隔离了起来,这个操作不会再影响我们的主线程执行了。
- 问题:我们如何去拿到最终的结果呢?
开始执行main函数
Instance of 'Future<String>'
这是不能被阻塞的代码
7.2.3 Future链式调用
有了Future之后,如何去获取请求到的结果:通过.then的回调
import 'dart:io';void main() {print('开始执行main函数');// print(getNetworkData());getNetworkData().then((value) {print(value);});print('这是不能被阻塞的代码');
}Future<String> getNetworkData() {return Future<String>(() {sleep(Duration(seconds: 3));return '返回的网络数据';});
}
上面代码的执行结果:
开始执行main函数
这是不能被阻塞的代码
// 3s后执行下面的代码
返回的网络数据
执行中出现异常
如果调用过程中出现了异常,拿不到结果,如何获取到异常的信息呢?
import 'dart:io';void main() {print('开始执行main函数');getNetworkData().then((value) {print(value);}).catchError((e) {print(e);});print('这是不能被阻塞的代码');
}Future<String> getNetworkData() {return Future<String>(() {sleep(Duration(seconds: 3));// 不再返回结果,而是出现异常// return '返回的网络数据';throw Exception('网络请求出现错误');});
}
上面代码的执行结果:
开始执行main函数
这是不能被阻塞的代码
// 3s后没有拿到结果,但是我们捕获到了异常
Exception: 网络请求出现错误
补充一:上面内容的小结
我们通过一个案例来学习了一些Future的使用过程:
1、创建一个Future(可能是我们创建的,也可能是调用内部API或者第三方API获取到的一个Future,总之你需要获取到一个Future实例,Future通常会对一些异步的操作进行封装);
2、通过.then(成功回调函数)的方式来监听Future内部执行完成时获取到的结果;
3、通过.catchError(失败或异常回调函数)的方式来监听Future内部执行失败或者出现异常时的错误信息;
补充二:Future的两种状态
事实上Future在执行的整个过程中,我们通常把它划分成了两种状态:
状态一:未完成状态(uncompleted)
- 执行Future内部的操作时(在上面的案例中就是具体的网络请求过程,我们使用了延迟来模拟),我们称这个过程为未完成状态
状态二:完成状态(completed)
- 当Future内部的操作执行完成,通常会返回一个值,或者抛出一个异常。
- 这两种情况,我们都称Future为完成状态。
Dart官网有对这两种状态解析,之所以拿出来说是为了区别于Promise的三种状态
7.2.4 Future链式调用小练习
例子:用户先登录,登录成功之后拿到token,然后再保存token到本地
// 用户先登录,登录成功之后拿到token,然后再保存token到本地
import 'dart:io';void main() {print('开始执行main函数');login().then((token) {setToken(token).then((res) {if (res == 'ok') {print('本地存储token成功, token是$token');}}).catchError((e) {print(e);});}).catchError((e) {print(e);});print('这是不能被阻塞的代码');
}// 1. 模拟耗时的登录操作
Future<String> login() {return Future<String>(() {sleep(Duration(seconds: 3));print('假装登录成功');String token = '666888';return token;});
}// 2. 模拟耗时的本地存储操作
Future<String> setToken(String token) {return Future<String>(() {sleep(Duration(seconds: 3));print('假装本地存储token');return 'ok';});
}
打印结果如下:
开始执行main函数
这是不能被阻塞的代码
// 3s后打印
假装登录成功
// 3s后打印
假装本地存储token
本地存储token成功, token是666888
7.3 async和await
如果你已经完全搞懂了Future,那么学习await、async应该没有什么难度。
await、async是什么呢?
- 它们是Dart中的关键字
- 它们可以让我们用
同步的代码格式
,去实现异步的调用过程
。 - 并且,通常一个async的函数会返回一个Future。
我们已经知道,Future可以做到不阻塞我们的线程,让线程继续执行,并且在完成某个操作时改变自己的状态,并且回调then或者errorCatch回调。
如何生成一个Future呢?
- 1、通过我们前面学习的Future构造函数,或者后面学习的Future其他API都可以。
- 2、还有一种就是通过async的函数。
通常使用 async await
解决Future链式调用带来的回调地狱的问题
// 用户先登录,登录成功之后拿到token,然后再保存token到本地
import 'dart:io';void main() {print('开始执行main函数');doLogin();print('这是不能被阻塞的代码');
}void doLogin() async {String token = await login();String res = await setToken(token);if (res == 'ok') {print('本地存储token $token 成功');}
}// 1. 模拟耗时的登录操作
Future<String> login() {return Future<String>(() {sleep(Duration(seconds: 3));print('假装登录成功');String token = '666888';return token;});
}// 2. 模拟耗时的本地存储操作
Future<String> setToken(String token) {return Future<String>(() {sleep(Duration(seconds: 3));print('假装本地存储token');return 'ok';});
}
八、泛型
[思考]:为什么List
和Map
中可以存储任意类型的数据?
[原因]:Dart在封装List和Map时,使用了泛型去限定了List和Map中数据的类型为dynamic
类型
[泛型]:可用于限定数据的类型,比如可以限定List和Map中数据的类型
abstract class List<E> implements EfficientLengthIterable<E>
[问题演示]:保存商品分类名称时,不应该出现100、true这样类型的数据
List categories = ['居家', '美食', 100, true];
[解决]:使用[泛型]限定List或者Map中元素的类型
void main() {// 1. 使用泛型限定List中元素的类型// 1.1 让列表中的元素只能是字符串类型// List categories = ['居家', '美食', 100, true];// List<String> categories = ['居家', '美食'];// 1.2 让列表中的元素只能是数字类型// List<num> nums = [100, 89, 10.99];// 2. 使用泛型限定Map中键和值的类型// 2.1 键和值都可以是任意类型Map categories1 = {1: 2, true: false, 'name': '张三', 'id': 1};print(categories1);// 2.2 键和值都只能是字符串类型Map<String, String> categories2 = {'id': '1','name': '居家',};print(categories2);// 2.3 键字符串类型, 值是任意类型Map<String, dynamic> categories3 = {'id': 1, 'name': '居家', 'isMan': true};print(categories3);
}
[泛型的作用]:在程序设计中提供一种机制,使得代码能够在编写时不指定具体类型,从而实现更灵活、可复用、类型安全的代码结构,能适应多种不同类型的数据处理需求。
比如:List和Map中的元素使用泛型限定为可以是任意类型的,从而不需要单独去封装存储某一种数据类型的List和Map
/*泛型的作用:使用泛型可以减少重复的代码封装函数:接收字符串就返回字符串,接收数字就返回数字,接收bool就返回bool*/
void main() {// 1. 普通封装// String demoString(String str) {// return str;// }// int demoInt(int a) {// return a;// }// bool demoBool(bool b) {// return b;// }// 2. 基于泛型封装T demo<T>(T parm) {return parm;}// 调用String ret1 = demo<String>('itcast');print(ret1);int ret2 = demo<int>(17);print(ret2);bool ret3 = demo<bool>(true);print(ret3);
}
九、异常处理
void main() {// 1. 捕获异常:try catch// try {// dynamic name = 'itheima';// name.haha();// } catch (e) {// print(e);// } finally {// // 无论是否有异常都会执行这个代码块// print('finally');// }// 2. 手动抛出异常:判断字符串是否相等,如果不相等手动抛出异常try {String str = 'itcast';if (str == 'yjh') {print('ok');} else {// 手动抛出异常throw Exception('字符串不相等');}} catch (e) {print(e);}
}
📎dart_code.zip
面试参考话术:
- 说一下 flutter?flutter 你会吗?
前提:flutter 项目都是比较早了 ,1 年及以前:
- flutter 是一个跨平台的开发框架,由谷歌推出,一次开发发布多平台,ios,安卓,鸿蒙,web 都是可以适配的。
- 开发的语言用的是 Dart,和 ArkTS 非常的类似,但是也有不同的地方,我挑几个说一下:
-
- 声明变量:
-
-
- dart:var,const,final
- ArkTS:let,const
-
-
- 类型
-
-
- 基本类型差不多
- ArkTS 中的 Record 在dart中叫做 map
- ArkTS 中的 数组在 dart 中叫 list
-
-
- 异步管理
-
-
- ArkTS 中用的是 Promise,可以简写为 async 和 await
- dart 中是 future,也可以简写为 async 和 await
- 他俩基本一样
-
-
- 还有一些 运算符,类子类的语法都是一样的,所以当时学习 ArkTS,包括之前学习前端的时候
- 之前学习 dart 的经验给了我很多的帮助
- 而且 dart,flutter 现在 ai 的语料已经很充足了,就算现在让我写,也问题不大