Flutter基础之Dart语言学习

395 阅读25分钟

Dart是谷歌开发的计算机编程语言,后来被Ecma (ECMA-408)认定为标准。它被用于web、服务器、移动应用 和物联网等领域的开发。它是宽松开源许可证(修改的BSD证书)下的开源软件。

Dart是面向对象的、类定义的、单继承的语言。它的语法类似C语言,可以转译为JavaScript,支持接口(interfaces)、混入(mixins)、抽象类(abstract classes)、具体化泛型(reified generics)、可选类型(optional typing)和sound type system () 。

Snipaste_2022-11-04_10-11-53.png

变量与常量

在程序中,经常用大量的数据来代表程序的状态,其中有些数据的值在程序运行过程中会发生改变,有些数据的值不能发生改变,这些数据在程序中分别叫做变量和常量.

变量

变量:就是用来存储值的对象,可能是一个整数,也可能是一段话.

使用var定义变量,适用于类型是固定,在变量一经赋值,类型将被固定

var name = 'tony';
print(name); //tony
print(name.runtimeType); //String

使用dynamic定义的变量,变量类型可改变,根据变量的值动态改变变量类型

dynamic name = 'tony';
print(name); //tony
print(name.runtimeType); //String
name = 123;
print(name); //123
print(name.runtimeType); //int

常量

简单的说,一旦一个对象成了常量,其引用的对象就不再可变.在Dart语言中,可以使用final或const关键字声明一个常量.

final double weight = 67.5;
final height = 180;

const int num = 10;
const num2 = 22.5;

常用数据类型

数字

num 是数字类型的父类,包含整数和浮点数

Dart中的数字类型用于表示数字文字。Dart中的数字类型有两种类型

  • 整数 - 整数值表示非小数值,即没有小数点的数值。例如,10是整数。使用int关键字表示整数文字。
  • 双精度数 - Dart还支持小数数值,即带小数点的值。Dart中的Double数据类型表示64位(双精度)浮点数。例如,10.10。关键字double用于表示浮点文字。
num num1 = -1.0; // 是数字类型的父类
num num2 = 2;
int int1 = 3; // 只能是整数
double d1 = 1.68; // 双精度
print("num:$num1 num:$num2 int:$int1 double:$d1");

数字类型常用方法

///类型转换
abs(): 对数字求绝对值
toInt(): 转化为 int 类型
toDouble(): 转化为 double 类型

num num2 = -10;
///abs()绝对值
print(num2.abs()); //10

///toDouble()转化为 double 类型
num num3 = 2022;
print(num3.toDouble()); //2022.0

///toInt()转换成int类型
double d1 = 12.12;
print(d1.toInt()); //12.12

///hashcode 返回一个整数, 表示数值的哈希码
num num4 = 4;
print(num4.hashCode); //4

///compareTo(num) 它返回一个整数, 表示两个数字之间的关系 
返回值:  
1. 如果值相等 返回 0;  
2. 如果当前数字对象大于指定的数值 返回 1;  
3. 如果当前数字对象小于指定的数字值 返回 -1num num1 = 123;
print(num1.compareTo(1)); //1

///sign 判断一个数是否是大于0
返回值:  
1. 如果当前数字小于零, 则此属性返回 -1;  
2. 如果当前数字等于零, 则此属性返回 0;  
3. 如果当前数字大于于零, 则此属性返回 1num num1 = -2;
print(num1.sign); //-1


///round()  四舍五入
num num1 = 12.6;
print(num1.round()); //13

///floor() 向下取整
num num1 = 12.1;
print(num1.floor()); //12

///ceil()  向上取整
num num1 = 12.1;
print(num1.ceil()); //13

///truncate() 丢弃任何小数位后返回一个整数
num num1 = 12.9;
print(num1.truncate()); //12

///roundToDouble() 四舍五入同时转为浮点型
num num1 = 12.9;
print(num1.roundToDouble()); //13.0

///floorToDouble() 向下取整同时转为浮点型
num num1 = 12.9;
print(num1.floorToDouble()); //12.0

///ceilToDouble() 向上取整同时转为浮点型
num num1 = 12.9;
print(num1.ceilToDouble()); //13.0

///truncateToDouble() 丢弃任何小数位后返回一个浮点数
num num1 = 12.9;
print(num1.truncateToDouble()); //12.0

///clamp(num lowerLimit, num upperLimit)  数字在区间内, 
///返回该数字; 数字不在区间内, 返回最接近该数字的边界值 lower / upper
num num1 = 12.9;
print(num1.clamp(12, 14)); //12.9

///isNaN 判断变量是否不是数字类型
num num1 = 0.0; 
print(num1.isNaN); //false

///isNegative 判断变量的值是否为负值
 num num1 = -0.0;
print(num1.isNegative); //true

///runtimeType 判断数据类型
num num1 = 0;
print(num1.runtimeType); //int

///remainder(nun) num为需要除的数 取余数
num num1 = 121111.125;
print(num1.remainder(2)); //1.125

///isEven 是否偶数
int num1 = 1;
print(num1.isEven); //false

///isOdd 是否奇数
int num1 = 1;
print(num1.isOdd); //true

字符串 String

关键字String用于表示字符串文字,字符串值嵌入单引号或双引号中或三引号表示。行字符串使用单引号或双引号表示。三引号用于表示多行字符串。

String str1 = 'this is a single line string'; 
String str2 = "this is a single line string"; 
String str3 = '''this is a multiline line string'''; 
String str4 = """this is a multiline
line string"""; 

字符串拼接

  • +号拼接
String str1 = '字符串';
print('str1:' + str1); //str1:字符串
  • $号拼接
String str1 = '字符串';
print('str1:$str1'); //str1:字符串

字符串常用方法/属性


///substring 截取字符串
String str1 = '常用数据类型';
print(str1.substring(1, 2)); //用

///indexOf 获取指定字符串位置
String str1 = "学习dart语法";
print(str1.indexOf('习'));

///codeUnits 返回给定索引处的16位UTF-16代码单元。
String str1 = "学习dart语法";
print(str1.codeUnits); //[23398, 20064, 100, 97, 114, 116, 35821, 27861] 

///characters 处理特殊字符的映射到的编码单元,解决特殊字符串的长度.
print('Hello👋'.length); // 7
print('Hello👋'.characters.length); //6


///hashCode 获取变量的hash码
String str1 = "学习dart语法";
print(str1.hashCode);

///isEmpty 判断字符串是否为空
String str1 = "学习dart语法";
print(str1.isEmpty);

///isNotEmpty 判断字符串是否不为空
String str1 = "学习dart语法";
print(str1.isNotEmpty);

///获取字符串的长度
String str1 = "学习dart语法";
print(str1.length);

///runes 显示 32 位的 10 进制数值
String str1 = "学习dart语法";
print(str1.runes); //(23398, 20064, 100, 97, 114, 116, 35821, 27861)

///allMatches 
String str1 = "学习dart语法";
Iterable<Match> matches = str1.allMatches('学习dart语法'); //(Instance of '_StringMatch')
for (Match m in matches) {
   print(m.group(0)); //学习dart语法
}

///codeUnitAt 返回给定索引处的16位UTF-16代码单元。
String str1 = "学习dart语法";
print(str1.codeUnitAt(1));


///compareTo 返回表示两个字符串之间关系的整数。
`0` - 当字符串相等时。
`1` - 当第一个字符串大于第二个字符串时;
`-1` - 当第一个字符串小于第二个字符串时;

String str1 = "A";
String str2 = "A";
String str3 = "B";
print("str1.compareTo(str2): ${str1.compareTo(str2)}"); //0
print("str1.compareTo(str3): ${str1.compareTo(str3)}"); //-1
print("str3.compareTo(str2): ${str3.compareTo(str2)}"); //1


///contains 判断该字符串中是否包含某个字符,符合则返回true
String str1 = "123";
print(str1.contains('1')); //true

///endsWith 字符串中最后一位是否是某个字符,符合则返回true
String str1 = "123";
print(str1.endsWith('3'));

///lastIndexOf 从后获取指定字符串位置
String str1 = "123";
print(str1.lastIndexOf('3')); //2


///padLeft 字符串长度不够往左进行补位,默认补空字符串
String str1 = "123"; 
print(str1.padLeft(6, 'x')); //xxx123

///padRight 字符串长度不够往右进行补位,默认补空字符串
String str1 = "123";
print(str1.padRight(6, 'x')); ///123xxx

///replaceAll 替换指定内容的字符串
String str1 = "123123";
print(str1.replaceAll('1', 'a')); //a23a23

///replaceAllMapped 替换指定内容的字符串
String str1 = "123123";
print(str1.replaceAllMapped('1', (match) => "b")); //a23a23

///replaceFirst 替换符合内容的首个字符串
String str1 = "123123";
print(str1.replaceFirst('1', 'b')); //b23123

///replaceFirstMapped 替换符合内容的首个字符串
String str1 = "123123";
print(str1.replaceFirstMapped('2', (match) => 'c')); //1c3123

///replaceRange 替换指定索引范围内的字符串
String str1 = "123123";
print(str1.replaceRange(0, 2, 'aa')); //aa3123

///split 字符串以指定字符分割位数组
String str1 = "123-123";
print(str1.split('-')); // [123, 123]

///splitMapJoin 可根据指定内容以外的数据进行全局替换
///通过调用 `onMatch` 将每个匹配项转换为字符串。如果省略`onMatch`,则使用匹配的子字符串。
///通过调用 `onNonMatch` 将每个不匹配的部分转换为字符串。如果省略`onNonMatch`,则使用不匹配的子字符串本身。
String str1 = "123-123";
final result = str1.splitMapJoin('-',
        onMatch: (m) => '${m[0]}', 
        onNonMatch: (n) => '1');
print(result); // 1-1

///startsWith  字符串中第一位是否是某个字符,符合则返回true 
String str1 = "123-123";
print(str1.startsWith("-")); //false

///substring 返回指定范围内的字符
String str1 = "123-123";
print(str1.substring(1)); //23-123

///toLowerCase 字符串转为小写
String str1 = "A";
print(str1.toLowerCase()); //a

///toUpperCase 字符串转为大写
String str1 = "a";
print(str1.toUpperCase()); //A

///trim 清除字符串的左右空格
String str1 = "  a ";
print(str1.trim()); //a

///trimLeft 清除字符串的左空格
String str1 = "  a ";
print(str1.trimLeft()); //a


///trimRight 清除字符串的右空格
String str1 = "  a ";
print(str1.trimRight()); //  a

布尔 bool

布尔类型有两个结果:true和false,用于表示真和假

bool isChecked1 = true;
bool isChecked2 = false;

集合

List 列表对象

就是js中的数组

///基础语法
var list = [1, 'q', true];
    list.add('100');
    print(list); //[1, q, true, 100]
 }
 

Map 映射对象

就是js中的对象

var details = {"Username": "tony", "Password": "123"};
details.addAll({"sex": '男'});
print(details); // {Username: tony, Password: 123, sex: 男}
print(details["Username"]); //tony

Set 集合对象

var setExp = {'Alice', 'Bob'};
print(setExp); //{'Alice', 'Bob'}

集合常见用法

///length 列表的长度
var list = [1, 'q', true];
print(list.length); //3

///reversed 反转数组
var list = [1, 'q', true, 2];
print(list.reversed); //(2, true, q, 1)

///hashCode 返回数值的哈希码
var list = [1, 'q', true, 2];
print(list.hashCode); //991443348

///first 数组第一位
var list = [1, 'q', true, 2];
print(list.first); // 1

///isEmpty 数组是否为空,返回bool
var list = [1, 'q', true, 2];
print(list.isEmpty); // false

///isNotEmpty 数组是否不为空,返回bool
var list = [1, 'q', true, 2];
print(list.isNotEmpty); // true

///iterator 迭代器
var list = [1, 'q', true, 2];
final result = list.iterator;
print(result.moveNext()); // true
print(result.moveNext()); // true
print(result.moveNext()); // true
print(result.moveNext()); // true
print(result.moveNext()); // false

///last 数组最后一位
var list = [1, 'q', true, 2];
print(list.last); //2

///single 获取数组中只有1位时候的数据,大于1位或者空数组会报错 
var list = [2];
print(list.single);

///add() 末尾添加数据


///addAll() 数组拼接
var list = [2];
var list2 = [1];
var list3 = [3];
list.addAll(list2); //只拼接一个
list.addAll([...list2, ...list3]); //拼接多个数组 和js一样
print(list); //[2, 1, 1, 3]

///any() 判断数组是否满足条件,返回bool
var list = [2, 1];
var result = list.any((element) => element > 3); //像js的filter
print(result); //false

///asMap() 返回列表的索引作为键,使用相应的对象作为值
var list = [2, 1];
var result = list.asMap();
print(result); //{0: 2, 1: 1}
print(result.keys.toList()); //[0,1]

///clear() 清空数组
var list = ['a', "b"];
list.clear();
print(list); //[]

///contains() 数组中是否包含指定内容, 返回bool
var list = ['a', "b"];
print(list.contains('c')); //false

///elementAt() 通过下标获取数组中对应的值
var list = ['a', "b"];
print(list.elementAt(1)); //b

///every() 数组中是否包含指定内容, 返回bool
var list = ['a', "b"];
print(list.every((element) => element != 'c')); //true

///expand() 添加两个或多个列表来创建新列表
var list = ['a', "b"];
var list2 = [1, 2];
var list3 = [true, false];
var newList = [list, list2, list3].expand((element) => element).toList();
print(newList); //[a, b, 1, 2, true, false]

///fillRange() 修改数组
var list = ['a', "b", 'c', 'd'];
list.fillRange(0, 2, '1');
print(list); //[1, 1, c, d]

///firstWhere()   返回数组中第一个满足条件的元素
var list = ['a', "b", 'c', 'd'];
var result =
     list.firstWhere((element) => element == 'a', orElse: () => '-1');
print(result); //a
var result =
     list.firstWhere((element) => element == 'e', orElse: () => '-1');
print(result); //-1

///fold() 根据一个现有数组和一个初始参数值 initValue,指定参数条件操作现有数组的所有元素,并返回处理的结果
import "dart:math";
var list = [121, 12, 33, 14, 3];
var resultMin = list.fold(list[0], min);
var resultMax = list.fold(list[0], max);
print(resultMin); //3
print(resultMax); //121

///followedBy() 继承原有数组,并添加新的数据组成新的数组
var list = [121, 12, 33, 14, 3];
var list2 = list.followedBy([100, 200]).toList();
print(list2); //[121, 12, 33, 14, 3, 100, 200]

///forEach() 遍历数组
var list = [121, 12, 33, 14, 3];
list.forEach((element) {
   print(element);
});

///getRange() 获取指定范围内的数据,并返回
var list = [121, 12, 33, 14, 3];
print(list.getRange(0, 3).toList()); //[121, 12, 33]

///indexOf() 获取指定数据的数组索引 不满足则返回-1
var list = [121, 12, 33, 14, 3];
print(list.indexOf(12)); //1

///indexWhere() 返回第一个满足条件的元素的索引
var list = [121, 12, 33, 14, 3];
print(list.indexWhere((element) => element == 14)); //3

///insert() 在指定索引处插入一个值
var list = [121, 12, 33, 14, 3];
list.insert(0, 1); //在索引0的位置插入数据1
print(list); //[1, 121, 12, 33, 14, 3]

///insertAll() 在指定索引处插入一个数组
var list = [121, 12, 33, 14, 3];
var list1 = [12, 1, 3, 5, 7];
list.insertAll(0, list1);
print(list); //[12, 1, 3, 5, 7, 121, 12, 33, 14, 3]

///join()  用指定字符连接数组中每个元素,返回 String
var list = [121, 12, 33, 14, 3];
var result = list.join('-'); 
print(result); //121-12-33-14-3

///lastIndexOf() 从后向前查找指定元素在数组中的索引,不满足则返回-1
var list = [121, 12, 33, 14, 3];
print(list.lastIndexOf(14)); //3

///lastIndexWhere() 从后向前找,返回第一个满足条件的元素的索引
var list = [121, 12, 33, 14, 3];
print(list.lastIndexWhere((element) => element == 14)); //3

///lastWhere() 从后向前查找第一个满足条件的元素
var list = [121, 12, 33, 14, 3];
print(list.lastWhere((element) => element > 3)); //14

///map() 遍历数组中的所有元素,可以对元素进行处理,并返回新的 Iterable
var list = [121, 12, 33, 14, 3];
Iterable<bool> l1 = list.map((e) => e > 10);
Iterable<String> l2 = list.map((e) => e > 30 ? '大' : '小');
print(l1); //(true, true, true, true, false)
print(l2); //(大, 小, 大, 小, 小)

///reduce() **用指定的函数方式对数组中的所有元素做连续操作,并将结果返回**
var list = [1, 2, 3, 4, 5];
var result = list.reduce((value, element) => value * element);
print(result); // 1 * 2 * 3 * 4 * 5 =  120

///remove() 删除指定元素
var list = [1, 2, 3, 4, 5, 6];
list.remove(6);
print(list); //[1, 2, 3, 4, 5]

///removeAt() 删除指定索引位置处的元素
var list = [1, 2, 3, 4, 5, 6];
list.removeAt(1);
print(list); //[1, 3, 4, 5, 6]

///removeLast() 删除数组的最后一个元素
var list = [1, 2, 3, 4, 5, 6];
list.removeLast();
print(list); //[1, 2, 3, 4, 5]

///removeRange() 删除指定索引范围内的元素(含头不含尾)
var list = [1, 2, 3, 4, 5, 6];
list.removeRange(0, 2);
print(list);

///removeWhere() 根据指定条件删除元素
var list = [1, 2, 3, 4, 5, 6];
list.removeWhere((element) => element < 4);
print(list);

///replaceRange() 用某一数组替换指定索引范围内的所有元素(含头不含尾)
var list = [1, 2, 3, 4, 5, 6];
list.replaceRange(0, 2, [10]);
print(list); //[10, 3, 4, 5, 6]

///replaceRange() 用某一数组替换指定索引范围内的所有元素(含头不含尾)
var list = [1, 2, 3, 4, 5, 6];
list.replaceRange(0, 2, [10]);
print(list); //[10, 3, 4, 5, 6]

///retainWhere() 根据指定条件筛选元素(改变了原数组) 
var list = [1, 2, 3, 4, 5, 6];
list.retainWhere((element) => element < 2);
print(list); //1

///setAll() 从指定索引位置开始,使用第二个数组内的元素依次替换掉第一个数组中的元素
var list = [1, 2, 3, 4, 5, 6, 'f'];
var list2 = ['a', 'b', 'c'];
list.setAll(1, list2);
print(list); // [1, a, b, c, 5, 6, f]

///setRange() 范围替换数组中的值(含头不含尾)
final list1 = <int>[1, 2, 3, 4];
final list2 = <int>[5, 6, 7, 8, 9];
list1.setRange(0, 3, list2);
print(list1); // [5, 6, 7, 4]

///shuffle() 随机排列指定数组(修改了原数组)
final list = <int>[1, 2, 3, 4];
list.shuffle();
print(list); //[1, 3, 4, 2]

///singleWhere() 获取满足指定条件的唯一元素
inal list = <int>[1, 2, 3, 4];
int result = list.singleWhere((element) => element > 3, orElse: () => -1);
print(result); //4

///skip() 跳过指定个数的元素,返回后面的元素
final list = <int>[1, 2, 3, 4];
final result = list.skip(1);
print(result);

///skipWhile() 根据指定条件,找到第一个不符合条件的元素,然后将该元素后面的所有元素返回
final list = <int>[1, 2, 3, 4];
final result = list.skipWhile((value) => value < 2).toList();
print(result); //[2, 3, 4]

///sublist() 从指定索引处截取数组
final list = [3, 1, 2, 4];
list.sort();
print(list); //[1,2,3,4]

///take() 从索引 0 位置处,取指定个数的元素
final list = [3, 1, 2, 4, 5, 6];
final result = list.take(3);
print(result);

///takeWhile() 从索引 0 位置处,查询满足指定条件的元素,直到出现第一个不符合条件的元素,然后返回前面符合条件的元素
final list = [3, 1, 2, 4, 5, 6];
final result = list.takeWhile((value) => value < 4);
print(result); //(3, 1, 2)

///where() 根据指定条件,函数筛选每个元素,符合条件的元素组成一个新的 Iterable
final list = [3, 1, 2, 4, 5, 6];
var result = list.where((element) => element > 2);
print(result); //(3, 4, 5, 6)

///whereType() 从混合类型的数组中,筛选出指定类型的元素
final list = [3, 1, 2, 4, 5, 6, 'q', true];
Iterable<String> result1 = list.whereType();
Iterable<bool> result2 = list.whereType();
print(result1); // q
print(result2); //true

var 动态数据类型

运算符

算术运算符

运算符定义
+相加
-相减
-expr取反
*相乘
/除法
~/除法(返回整数)
%取余
++var自增
var++自增
--var自减
var--自减

关系运算符

运算符定义
==相等
!=不等
>大于
<小于
>=大于等于
<=小于等于

类型判断运算符

类型判断运算符是在运行时判断两个对象类型是否一致.

运算符定义
as类型转换
is是指定类型
is!非指定类型
//转换类型
var stringObj = "String";
print((stringObj as Object).runtimeType);

int intObj = 100;
//判断是否为指定类型
print(intObj is int); //true
//判断是否为非指定类型
print(intObj is! int); //false

逻辑运算符

运算符定义
!expr取反
&&逻辑与
ll逻辑或

位操作运算符

位操作都是先将其转换为二进制数.

运算符定义
&按位与
l按位或
^按位异或
~expr一元位补码
<<左移位
>>右移位
//按位与
int num = 10; //二进制等于 1 0 1 0
print(0x0a & num); //10

//按位异或
int num = 10; //二进制等于 1 0 1 0
int num1 = 20;
num = num ^ num1;
num1 = num ^ num1;
num = num ^ num1;
print(num); //20
print(num1); //10

条件语句与循环语句

条件语句

int num = 10;
if (num > 9) {
    print('$num大于10');
 } else {
    print('$num小于10');
 }
 
int num = 10;
switch (num) {
  case 10:
    print(10);
    break;
  default:
}
属性描述
if语句if语句由一个布尔表达式后跟一个或多个语句组成。
if…else语句if后面跟一个可选的else块。如果if块测试的布尔表达式求值为false,则执行else块。
else…if语句else...if可用于测试多个条件。
switch…case语句switch语句计算表达式,将表达式的值与case子句匹配,并执行与该case相关的语句。

循环语句

循环的分类:

image.png

确定(Definite)循环 。迭代次数是明确/固定的循环称为确定循环。

int num = 10;
for (var i = 0; i < num; i++) {
  print(i);
}

final obj = ['a', 'b', 'c', 'd'];
for (var prop in obj) {
  print(prop);
}
循环描述
for循环for循环是一个确定循环的实现,用于执行代码块指定的次数。
for…in循环for...in循环用于循环对象的属性。

无限循环。当循环中的迭代次数不确定或未知时使用不定循环。可以使用以下方式实现无限循环:

int num = 1;
while (num < 5) {
  print('第$num次打印');
  num++;
}

do {
  print('第$num次打印');
  num++;
} while (num < 5);
循环描述
while循环每次指定的条件求值为true时,while循环都会执行指令。在执行代码块之前评估条件。
do…while循环do...while循环类似于while循环,只是do...while循环不会在第一次循环执行时评估条件。

控制语句

int num = 5;
for (var i = 0; i < num; i++) {
  if (i == 3) break;
  print('$i次打印');
}
flutter: 0次打印
flutter: 1次打印
flutter: 2次打印

int num = 5;
for (var i = 0; i < num; i++) {
  if (i == 3) continue;
  print('$i次打印');
}
flutter: 0次打印
flutter: 1次打印
flutter: 2次打印
flutter: 4次打印
循环描述
break语句break语句用于将控件从构造中取出。在循环中使用break会导致程序退出循环。
continue语句continue语句跳过当前迭代中的后续语句,并将控制权带回循环的开头。

函数/方法

Dart 是一个面向对象语言,其特性之一为一切皆对象

/// 返回类型为整数
int getNumber() {
  return 1;
}
///简化写法
int getNumber2() => 1;

参数

必选参数

在参数列表中,必须参数在最前面

//void 代表不返回任何类型
///新朋友
///
///name 名字
///
///num 年龄
///
void newFriend(String name, num age) {
  print('我的朋友名字叫:$name,年龄:$age');
}

可选参数

可选参数分为可选命名参数和基于位置的参数,二者是互斥关系,不能同时出现.

可选命名参数

/**
 * 获取价格
 * 
 * return 价格
 */
double getPrice({var bookName: '非热门图书'}) {
  if (bookName == 'Dart 编程入门') {
    return 78.5;
  }
  return 50.0;
}

double price = getPrice(bookName: 'Dart 编程入门');

基于位置参数

/**
 * 获取价格
 * 
 * return 价格
 */
double getPrice([String bookName = '非热门图书']) {
  if (bookName == 'Dart 编程入门') {
    return 78.5;
  }
  return 50.0;
}
double price = getPrice('Dart 编程入门');

断言

为了方便开发,Dart语言提供了断言(assert). 因此,断言只在开发模式下起作用,在正式环境中无效.

int num = 10;
assert(num == 10);

传入assert的参数,可以是任意表达式或者方法,只要返回值是bool就可以, 当断言失败时(返回false),会抛出AssertionError异常。

异常

在代码运行的过程中,难免会出现一些问题导致程序发生错误甚至崩溃 , 我们称之为异常.

Throw

通过throw 抛出异常

//已知数据类型
throw new FormatException("数据格式错误");

//自定义异常
throw '异常结果';

Try Catch

捕获异常,避免程序崩溃

//在某些情况下,由于被try包裹起来的代码不一定只发生在一种类型异常,需要分别对每一种异常进行处理,只需要捕捉不同的异常处理即可.
//以下代码的逻辑走到on RangeError分支, 而输出详细的错误信息是作为默认异常处理才会被执行.
var intArry = [10, 20, 30, 40, 50];
try {
  print(intArry[5]);
} on RangeError {
  print('=====');
  print(intArry[4]);
} on FormatException {
  print('FormatException');
} catch (e) {
  print(e.toString());
} 

Finally

当在运行完一些可能引起异常的逻辑代码后,仍需要执行一些语句完成后面的工作,就需要使用finally

var intArry = [10, 20, 30, 40, 50];
try {
  print(intArry[5]);
} on RangeError {
  print('=====');
  print(intArry[4]);
} on FormatException {
  print('FormatException');
} catch (e) {
  print(e.toString());
} finally {
  //不管业务走到try 还是catch finally永远都是最后一个执行
  print('finally');
}

面向对象

Dart是一种面向对象的语言。它支持面向对象的编程功能,如类,接口等。OOP方面的类是创建对象的蓝图/模板。类封装了对象的数据。Dart为类概念提供了内置支持。

//语法
class class_name { 
    <fields> 
    <getters/setters> 
    <constructors> 
    <functions> 
 }

class关键字后跟类名。在命名类时必须考虑标识符的规则。类定义可包括以下内容 -

  • 字段(fields) - 字段是类中声明的任何变量,字段表示与对象有关的数据。
  • setters和getters - 允许程序初始化和检索类字段的值,默认的getter/setter与每个类相关联。但是可以通过显式定义setter/getter来覆盖默认值。
  • 构造函数(constructors) - 负责为类的对象分配内存。
  • 函数(functions) - 函数表示对象可以采取的操作,它们有时也称为方法。

基本使用

///声明一个类
class Car {
  String carName = 'EA';

  void disp() {
    print(carName);
  }
}

///创建类的实例
var car = new Car();
//使用类的属性
print(car.carName);
//使用类的方法
car.disp();

构造函数

///定义Person , 所有类都继承自Object
class Persion {
  String? name;
  int? age;
  String? gender;

  /// 标准构造方法
  Persion(String name, int age, {String gender: '男'}) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
///简化写法
Persion(this.name, this.age, {this.gender: '男'});

  ///重写父类方法ss
  @override
  String toString() {
    return 'name:$name,age:$age,gender:$gender';
  }
}

var persion = new Persion('tony', 29, gender: '女');
print(persion.toString());

getter() 和 setter() 方法,

Dart是不需要手动写getter() 和 setter() 方法,它会为实例变量生成getter() 方法.当然,Dart 支持自定义getter() 和 setter() 方法, 方法是使用 get和set关键字.

class Persion {
  String name = '';
  int age = 0;
  String? gender;

  /// 标准构造方法
  Persion(this.name, this.age, {this.gender: '男'});

  int get getAge {
    return this.age;
  }

  set setAge(value) {
    this.age = value;
  }
}

var persion = new Persion('tony', 29, gender: '女');
persion.setAge = 100;
print(persion.getAge);

静态变量

静态变量,又称为类变量.和实例变量不同的是,静态变量是对于一个类而言. 在Person类中,声明一个静态变量的方法使用static 关键字

class Persion {
  String name = '';
  int age = 0;
  String? gender;
  static var notice = '静态变量';

  /// 标准构造方法
  Persion(this.name, this.age, {this.gender: '男'});
}

var persion = new Persion('tony', 29, gender: '女');
//无法使用persion对象访问notice , 只能通过Persion类使用
print(Persion.notice);

调用父类

子类继承父类

///定义Person , 所有类都继承自Object
class Persion {
  String name = '';
  int age = 0;
  String gender;

  /// 标准构造方法
  Persion(this.name, this.age, {this.gender: '男'});
  @override
  String toString() {
    return 'name:$name,age:$age,gender:$gender';
  }
}


class Student extends Persion {
  late String _school; //通过下划线来标识私有字段(变量)
  String? city;
  String? country;
  Student(this._school, String name, int age,
      {this.city, this.country = 'China', gender: '男'})
      : super(name, age, gender: gender);
  @override
  String toString() {
    return 'name:$name, age:$age';
  }
}

var student = new Student('万松园小学', 'tony', 8, city: '广州', gender: '女');
print(student.toString());

工厂构造函数

工厂函数就是提供缓存,如果一个对象已经被实例过,那么从缓存中取出来返回即可,不需要再生成一个新的对象.

class Persion2 {
  final String name;
  //在创建缓存区时使用了Map的组织形式 , 在类的内部使用了_cache对象 , 前面的String 即Key.
  static final Map<String, Persion2> _cache = <String, Persion2>{};
  
  //使用 factory 关键字标识类的构造函数将会令该构造函数变为工厂构造函数
  factory Persion2(String name) {
    if (_cache.containsKey(name)) {
      print(_cache[name]);
      return _cache[name] as Persion2;
    } else {
      final person = new Persion2.newPerson(name);
      _cache[name] = person;
      return person;
    }
  }

  Persion2.newPerson(this.name);
  void say(String content) {
    print(content);
  }
}

final david = new Persion2("david");
print(david.name.hashCode); //151658943
final david2 = new Persion2("david"); 
print(david2.name.hashCode); //151658943
final tony = new Persion2("tony");
print(tony.name.hashCode); //178834785

//通过调用对象的.hasCode可以判断是不是两个不同的实例.
class Person {
  String? name;
  int? age;

  /// 标准的构造方法
  Person(this.name, this.age);

  ///重写父类方法
  @override
  String toString() {
    return 'name:$name, age:$age';
  }
}
class Student extends Person {
  //定义类的变量
  late String _school; //通过下划线来标识私有字段(变量)
  String? city;
  String? country;
  String? name;

  ///构造方法
  ///同this._school初始化自有参数
  ///name,age交给父类进行初始化
  ///city为可选参数
  ///country为默认参数
  Student(this._school, String? name, int? age,
      {this.city, this.country = 'China'})
      : //初始化列表:除了调用父类构造器,在子类构造器方法体之前,也可以初始化实例变量,不同的初始化变量之前用逗号隔开。
        name = '$country.$city',
        //父类没有默认构造方法(无参的构造方法),则需要在初始化列表中调用父类的构造方法,进行初始化
        super(name, age) {
    //构造方法体不是必须的
    print('构造方法体不是必须的');
  }
  // 可以为私有字段设置getter来让外界获取到私有字段
  String get school => _school;
  //可以为私有字段设置setter来控制对私有字段的修改
  set school(String value) {
    _school = value;
  }

  // 静态方法 static
  static doPoint(String str) {
    print(str);
  }

  ///命名构造方法:[类名+.+方法名]
  ///使用命名构造方法为类实现多个构造方法
  Student.cover(Student stu) : super(stu.name, stu.age) {
    print('命名构造方法');
  }
  // 命名工厂构造方法:factory [类名+.+方法名]
  factory Student.stu(Student stu) {
    return Student(stu._school, stu.name, stu.age);
  }

  @override
  String toString() {
    return 'name:$name school:${this._school}';
  }
}

抽象类

使用 abstract 修饰符来定义一个抽象类,该类不能被实例化。抽象类在定义接口的时候非常有用,实际上抽象中也包含一些实现

abstract class Study {
  String? name;
  void studyEnglish();
}

class Student extends Study {
  @override
  void studyEnglish() {
    print('学习英语');
  }
}

final student = new Student();
student.studyEnglish();

接口

Dart语言不支持多个类继承, 因此在这种情况,就用到接口的概念, 实现借口的关键字 implememnts.

abstract class MobilePhone {
  var model;

  void call(num number);
  void sendSms(var number, var content) {
    print("发短信给: $number, 内容是:$content");
  }
}

class GetBrand {
  void printMyBrand(var brandStr) {
    print("brandStr的值为:$brandStr");
  }
}

class Iphone extends MobilePhone implements GetBrand {
  @override
  void call(number) {
    print('Iphone手机打电话给$number');
  }

  @override
  void printMyBrand(brandStr) {
    print('我是$brandStr手机');
  }
}

final iphone = new Iphone();
iphone.call(123456);
iphone.printMyBrand('Iphone');

混入

Mixin 是复用类代码的一种途径, 复用的类可以在不同层级,之间可以不存在继承关系。 简单说就是在类中混入其他的功能,实现类似多继承的功能

说明:

  1. 使用mixin关键字替换class定义混入类
  2. 作为Mixin的类只能继承Object,不能继承其他的类
  3. Mixin 类似于多继承,实在多类继承中重用一个类代码的方式。
  4. 作为mixins的类不能声明构造函数,不能调用 super
  5. 一个类可以混入多个Mixin
  6. 使用with 方法实现类似于多继承
  7. Mixin绝不是继承 也不是接口, 而是一种全新的特性
// 定义普通混入类
class A {
  String name = "AA";
  printA() {
    print("这是普通类A");
  }
}

class B {
  String name = "BB";
  printB() {
    print("这是普通类B");
  }
}

// 混入两个类A,B
class Student with A, B {}

final student = new Student();
print(student.name); //BB
student.printA();
student.printB();

枚举

枚举 , 即enums 或 enumerations, 是一种特殊的类. 通常用来表示具有固定数目的常量, 无法继承,无法使用Mixin特性, 无法实例.

enum AndroidBrand { Samsung, Huawei, Xiaomi, Oppo, Vivo }

print(AndroidBrand.values); //[AndroidBrand.Samsung, AndroidBrand.Huawei, AndroidBrand.Xiaomi, AndroidBrand.Oppo, AndroidBrand.Vivo]

print(AndroidBrand.Oppo.index); //3

泛型

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

泛型的定义

  1. 使用 <…> 来声明泛型
  2. 通常情况下,使用一个字母来代表类型参数, 例如 ETSK, 和 V 等。
  3. List 定义的泛型*(或者 参数化) 类型,定义为List<E>

使用泛型的原因

  1. 在 Dart 中类型是可选的,可以通过泛型来限定类型。
  2. 使用泛型可以有效地减少重复的代码。
  3. 泛型可以在多种类型之间定义同一个实现,同时还可以继续使用检查模式和静态分析工具提供的代码分析功能。
  4. 如果你需要更加安全的类型检查,则可以使用 参数化定义。
/**
* `T`(表示getInfo返回值的类型) getInfo`<T>`这个T就是接受使用getInfo时指定的类型 (`T` (参数类型) value) {
*
*
* }
*
**
/
T getInfo<T>(T value) {
  return value;
}
var str1 = getInfo<String>('abc');
print(str1);


// 限定List 泛型为String
List list2 = List<String>();
list2.add("李子");
list2.add(333);
print(list2);  //报错: type 'int' is not a subtype of type 'String' of 'value'

类泛型

dart中可以在定义类是使用泛型.

在类名字后面使用尖括号(<...>)来指定 泛型类型。

class Person<T>{

    T sayhello(T value){
        print("hello,我是$value");
        return value;
    }
}

void main(){
    Person student = new Person<String>();
    var name = student.sayhello("小明");      // hello,我是小明
    print(name);                             // 小明
    print(name is String);                   // true
}

限制泛型的类型

在需要的使用通过extends类限定泛型的类型

将上例中的泛型加以限制.

class Person<T extends String>{

    T sayhello(T value){
        print("hello,我是$value");
        return value;
    }
}

异步

Dart 是单线程的,但通过事件循环可以实现异步。

而 Future 是异步任务的封装,借助于 await 与 async,我们可以通过事件循环实现非阻塞的同步等待;

Isolate 是 Dart 中的多线程,可以实现并发,有自己的事件循环与 Queue,独占资源。Isolate 之间可以通过消息机制进行单向通信,这些传递的消息通过对方的事件循环驱动对方进行异步处理。

import 'dart:io';
//阻塞 
doComplexJob(){
  print('开始' + new DateTime.now().toString());
  sleep(new Duration(seconds: 12));
  print('结束' + new DateTime.now().toString()); //结束
}
doComplexJob();
print('方法后面的打印');
//flutter: 开始2022-11-19 22:36:25.066623
//flutter: 结束2022-11-19 22:36:37.073736
//flutter: 方法后面的打印

async/asait

使用async await 解决异步

import 'dart:io';
doComplexJob() async {
  print('开始' + new DateTime.now().toString());
  sleep(await new Duration(seconds: 12));
  print('结束' + new DateTime.now().toString()); //结束
}
doComplexJob();
print('方法后面的打印');
//flutter: 开始2022-11-19 22:42:38.985132
//flutter: 方法后面的打印
//flutter: 结束2022-11-19 22:42:51.003959

Dart常用API总结

数字类型API

数字支持的属性

属性描述
hashcode返回数值的哈希码。
isFinite如果数字有限,则返回为true; 否则返回false
isInfinite如果数字为正无穷大或负无穷大,则返回为true; 否则返回false
isNaN如果是非数字值,则返回为true; 否则返回false
isNegative如果数字为负,则返回为true; 否则返回false
sign返回-10或加1,具体取决于数字的符号和数值。
isEven如果数字是偶数,则返回为true; 否则返回false
isOdd如果数字是奇数,则返回为true; 否则返回false

数字支持的常用方法

方法描述
abs()返回数字的绝对值。
ceil()向上取整。
compareTo()返回一个整数, 表示两个数字之间的关系
floor()向下取整
remainder()取余数
round()四舍五入
toDouble()转化为double类型
toInt()转化为int类型
toString()转化为string类型
truncate()丢弃任何小数位后返回一个整数
roundToDouble()四舍五入同时转为浮点型
floorToDouble()向下取整同时转为浮点型
ceilToDouble()向上取整同时转为浮点型
truncateToDouble()丢弃任何小数位后返回一个浮点数
clamp()数字在区间内,返回该数字; 数字不在区间内, 返回最接近该数字的边界值 lower / upper

字符串类型API

字符串支持的属性

属性描述
codeUnits返回给定索引处的16位UTF-16代码单元。
characters处理特殊字符的映射到的编码单元,解决特殊字符串的长度.
hashCode获取变量的hash码
isEmpty如果此字符串为空,则返回true
isNotEmpty如果此字符串不为空,则返回true
length获取字符串的长度

字符串支持的方法

属性描述
substring()截取字符串
indexOf()获取指定字符串位置
allMatches()
codeUnitAt()返回给定索引处的16位UTF-16代码单元。
compareTo()返回表示两个字符串之间关系的整数。
contains()判断该字符串中是否包含某个字符,符合则返回true
endsWith()字符串中最后一位是否是某个字符,符合则返回true
lastIndexOf()从后获取指定字符串位置
padLeft()字符串长度不够往左进行补位,默认补空字符串
padRight()字符串长度不够往右进行补位,默认补空字符串
replaceAll()替换指定内容的字符串
replaceAllMapped()替换指定内容的字符串
replaceFirst()替换符合内容的首个字符串
replaceFirstMapped()替换符合内容的首个字符串
replaceRange()替换指定索引范围内的字符串
split()字符串以指定字符分割位数组
splitMapJoin()可根据指定内容以外的数据进行全局替换
startsWith()字符串中第一位是否是某个字符,符合则返回true
substring()返回此字符串的子字符串,字符串从startIndex(包括)延伸到endIndexexclusive
toLowerCase()字符串转为小写
toUpperCase()字符串转为大写
trim()清除字符串的左右空格
trimLeft()清除字符串的左空格
trimRight()清除字符串的右空格

集合类型API

集合 支持的属性

属性描述
length列表的长度
reversed反转数组
hashcode返回数值的哈希码。
first数组第一位
isEmpty数组是否为空,返回bool
isNotEmpty数组是否不为空,返回bool
iterator迭代器
last数组最后一位
single获取数组中只有1位时候的数据,大于1位或者空数组会报错

集合 支持的方法

添加数据的方法

属性描述
add()末尾添加数据
addAll()数组拼接
insert()在指定索引处插入一个值
insertAll()在指定索引处插入一个数组

删除数据的方法

属性描述
clear()清空数组
remove()删除指定元素
removeAt()删除指定索引位置处的元素
removeLast()删除数组的最后一个元素
removeRange()删除指定索引范围内的元素(含头不含尾)
removeWhere()根据指定条件删除元素

修改数据的方法

属性描述
fillRange()修改数组
replaceRange()用某一数组替换指定索引范围内的所有元素(含头不含尾)
setAll()从指定索引位置开始,使用第二个数组内的元素依次替换掉第一个数组中的元素
setRange()范围替换数组中的值(含头不含尾)
shuffle()随机排列指定数组(修改了原数组)

查询数据的方法

属性描述
any()判断数组是否满足条件,返回bool
contains()数组中是否包含指定内容, 返回bool
elementAt()通过下标获取数组中对应的值
every()数组中是否包含指定内容, 返回bool
firstWhere()返回数组中第一个满足条件的元素
getRange()获取指定范围内的数据,并返回
indexOf()获取指定数据的数组索引 不满足则返回-1
indexWhere()返回第一个满足条件的元素的索引
lastIndexOf()从后向前查找指定元素在数组中的索引,不满足则返回-1
lastIndexWhere()从后向前找,返回第一个满足条件的元素的索引
retainWhere()根据指定条件筛选元素(改变了原数组)
singleWhere()获取满足指定条件的唯一元素
skip()跳过指定个数的元素,返回后面的元素
skipWhile()根据指定条件,找到第一个不符合条件的元素,然后将该元素后面的所有元素返回
sublist()从指定索引处截取数组
take()从索引 0 位置处,取指定个数的元素
takeWhile()从索引 0 位置处,查询满足指定条件的元素,直到出现第一个不符合条件的元素,然后返回前面符合条件的元素
where()根据指定条件,函数筛选每个元素,符合条件的元素组成一个新的 Iterable
whereType()从混合类型的数组中,筛选出指定类型的元素

其他方法

属性描述
asMap()将 List 转换为 Map,key 为原数组的索引,value 为原数组的元素
expand()添加两个或多个列表来创建新列表
fold()根据一个现有数组和一个初始参数值 initValue,指定参数条件操作现有数组的所有元素,并返回处理的结果
followedBy()继承原有数组,并添加新的数据组成新的数组
forEach()遍历数组
join()用指定字符连接数组中每个元素,返回 String
map()遍历数组中的所有元素,可以对元素进行处理,并返回新的 Iterable
reduce()用指定的函数方式对数组中的所有元素做连续操作,并将结果返回
sort()数组排序(原数组发生改变)
toList()转成列表
toSet()转成Set集合

常用API

方法/属性使用描述
print()print("num:$num");打印命令,$num代表是变量
runtimeType变量.runtimeType判断变量类型

空安全

什么是空安全

空安全已经是一个屡见不鲜的话题,目前像主流的编程语言Kotlin、Swift、Rust 等都对空安全有自己的支持。Dart从2.12版本开始支持了空安全,通过空安全开发人员可以有效避免null错误崩溃。空安全性可以说是Dart语言的重要补充,它通过区分可空类型和非可空类型进一步增强了类型系统。

引入空安全的好处

  • 可以将原本运行时的空值引用错误将变为编辑时的分析错误;
  • 增强程序的健壮性,有效避免由Null而导致的崩溃;
  • 跟随Dart和Flutter的发展趋势,为程序的后续迭代不留坑;

空安全最小必备知识

  • 空安全的原则
  • 引入空安全前后Dart类型系统的变化
  • 可空(?)类型的使用
  • 延迟初始化(late)的使用
  • 空值断言操作符(!)的使用

空安全的原则

Dart 的空安全支持基于以下三条核心原则:

  • 默认不可空:除非您将变量显式声明为可空,否则它一定是非空的类型;

  • 渐进迁移:您可以自由地选择何时进行迁移,多少代码会进行迁移;

  • 完全可靠:Dart 的空安全是非常可靠的,意味着编译期间包含了很多优化,

    • 如果类型系统推断出某个变量不为空,那么它 永远 不为空。当您将整个项目和其依赖完全迁移至空安全后,您会享有健全性带来的所有优势——更少的 BUG、更小的二进制文件以及更快的执行速度。

引入空安全前后Dart类型系统的变化

在引入空安全前Dart的类型系统是这样的:

image.png

这意味着在之前,所有的类型都可以为Null,也就是Nul类型被看作是所有类型的子类。

在引入空安全之后:

image.png

可以看出,最大的变化是将Null类型独立出来了,这意味着Null不在是其它类型的子类型,所以对于一个非Null类型的变量传递一个Null值时会报类型转换错误。

提示:在使用了空安全的Flutter或Dart项目中你会经常看到?.、!、late的大量应用,那么他们分别是什么又改如何使用呢?请看下文的分析

可空(?)类型的使用

我们可以通过将?跟在类型的后面来表示它后面的变量或参数可接受Null:

class CommonModel {
  String? firstName; //可空的成员变量
  int getNameLen(String? lastName /*可空的参数*/) {
    int firstLen = firstName?.length ?? 0;
    int lastLen = lastName?.length ?? 0;
    return firstLen + lastLen;
  }
}

对于可空的变量或参数在使用的时候需要通过Dart 的避空运算符?.来进行访问,否则会抛出编译错误。

当程序启用空安全后,类的成员变量默认是不可空的,所以对于一个非空的成员变量需要指定其初始化方式:

class CommonModel {
    List names=[];//定义时初始化 
    final List colors;//在构造方法中初始化 
    late List urls;//延时初始化 
    CommonModel(this.colors); ...

延迟初始化(late)的使用

对于无法在定义时进行初始化,并且又想避免使用?.,那么延迟初始化可以帮到你。通过late修饰的变量,可以让开发者选择初始化的时机,并且在使用这个变量时可以不用?.

late List urls;//延时初始化 
setUrls(List urls){ 
    this.urls=urls;
 } 
 int getUrlLen(){ 
     return urls.length;
 }

延时初始化虽然能为我们编码带来一定便利,但如果使用不当会带来空异常的问题,所以在使用的时候一定保证赋值和访问的顺序,切莫颠倒

延迟初始化(late)使用范式

在Flutter中State的initState方法中初始化的一些变量是比较适合使用late来进行延时初始化的,因为在Widget生命周期中initState方法是最先执行的,所以它里面初始化的变量通过late修饰后既能保障使用时的便利,又能防止空异常,具体的用法:

class _SpeakPageState extends State<SpeakPage> 
   with SingleTickerProviderStateMixin { 
  String speakTips = '长按说话'; 
  String speakResult = ''; 
  late Animation<double> animation; 
  late AnimationController controller; 
  
  @override 
  void initState() { 
    controller = AnimationController( 
      super.initState(); 
      vsync: this, duration: Duration(milliseconds: 1000)); 
    animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)       
      ..addStatusListener((status) { 
            if (status == AnimationStatus.completed) { 
              controller.reverse(); 
            } else if (status == AnimationStatus.dismissed) { 
              controller.forward(); 
            } 
       }); 
     } 
...

空值断言操作符(!)的使用

当我们排除变量或参数的可空的可能后,可以通过!来告诉编译器这个可空的变量或参数不可空,这对我们进行方法传参或将可空参数传递给一个不可空的入参时特别有用:

    Widget get _listView { 
        return ListView( 
            children: <Widget>[ 
                _banner, 
                Padding( 
                    padding: EdgeInsets.fromLTRB(7, 4, 7, 4), 
                    child: LocalNav(localNavList: localNavList), 
                ), 
                if (gridNavModel != null) 
                    Padding( 
                        padding: EdgeInsets.fromLTRB(7, 0, 7, 4), 
                        child: GridNav(gridNavModel: gridNavModel!)), 
                    Padding( 
                        padding: EdgeInsets.fromLTRB(7, 0, 7, 4), 
                        child: SubNav(subNavList: subNavList)), 
                    if (salesBoxModel != null) 
                        Padding( 
                            padding: EdgeInsets.fromLTRB(7, 0, 7, 4), 
                            child: SalesBox(salesBox: salesBoxModel!)), 
                   ], 
                 ); 
               }

除此之外,!还有一个常见的用处:

bool isEmptyList(Object object) { 
    if (object is! List) return false; 
    return object.isEmpty; 
 }

用在这里表示取反,上述代码等价于:

bool isEmptyList(Object object) { 
    if (!(object is List)) return false; 
    return object.isEmpty; 
 }

Flutter如何做空安全适配

启用空安全

Flutter 2默认启用了空安全,所以通过Flutter 2创建的项目是已经开启了空安全的检查的,另外,小伙伴也可以可以通过下面命令来查看你的Flutter SDK版本:

flutter doctor

那么,如何手动开启和关闭空区安全的?

# pubspec.yaml
environment:
  sdk: ">=2.12.0 <3.0.0" //sdk >=2.12.0表示开启空安全检查

一旦项目开启了空安全检查,那么你的代码包括项目所依赖的三方插件必须是要支持空安全的否则是无法正常编译的。

如果想关闭空安全检查,可以将SDK的支持范围调整到2.12.0以下即可,如:

# pubspec.yaml
environment: sdk: ">=2.7.0 <3.0.0"

自定义Widget的空安全适配技巧

自定义Widget的空安全适配分两种情况:

  • Widget的空安全适配
  • State的空安全适配

Widget的空安全适配

对于自定的Widget无论是页面的某控件还是整个页面,通常都会为Widget定义一些属性。在进行空安全适配时要对属性进行一下分类:

  • 可空的属性:通过?进行修饰
  • 不可空的属性:在构造函数中设置默认值或者通过required进行修饰
class WebView extends StatefulWidget { 
    String? url; 
    final String? statusBarColor; 
    final String? title; 
    final bool? hideAppBar; 
    final bool backForbid; 
    
    WebView( 
        {this.url, 
         this.statusBarColor, 
         this.title, 
         this.hideAppBar, 
         this.backForbid = false}) 
         ...

提示:如果构造方法中使用了@required那么需要改成required

State的空安全适配

State的空安全适配主要是根据它的成员变量是否可空进行分类:

  • 可空的变量:通过?进行修饰

  • 不可空的变量:可采用以下两种方式进行适配

    • 定义时初始化
    • 使用late修饰为延时变量
class _TravelPageState extends State<TravelPage> with TickerProviderStateMixin { 
    late TabController _controller; //延时初始 
    List<TravelTab> tabs = []; //定义时初始化 
    ...
    @override void initState() { 
        super.initState(); 
        _controller = TabController(length: 0, vsync: this); 
        ...

数据模型(Model)空安全适配技巧

数据模型(Model)空安全适配主要以下两种情况:

  • 含有命令构造函数的模型
  • 含有命名工厂构造函数的模型

含有命令构造函数的模型

适配前:

class TravelItemModel { 
    int totalCount; 
    List<TravelItem> resultList; 
    TravelItemModel.fromJson(Map<String, dynamic> json) { 
        totalCount = json['totalCount']; 
        if (json['resultList'] != null) { 
            resultList = new List<TravelItem>(); 
            json['resultList'].forEach((v) { 
                resultList.add(new TravelItem.fromJson(v)); 
            }); 
         } 
 } 
 
     Map<String, dynamic> toJson() { 
         final Map<String, dynamic> data = new Map<String, dynamic>(); 
         data['totalCount'] = this.totalCount; 
         if (this.resultList != null) { 
             data['resultList'] = this.resultList.map((v) => v.toJson()).toList(); 
         } 
         return data; 
      } 
}

适配之前首先要和服务端协商好,模型中那些字段可空,那些字段是一定会下发的。对于这个案例假如:totalCount字段是一定会下发的,resultList字段是不能保证一定会下发,那么我们可以这样来适配:

适配后:


class CommonModel { 
    final String? icon; 
    final String? title; 
    final String url; 
    final String? statusBarColor; 
    final bool? hideAppBar;
    
    CommonModel( 
        {this.icon, 
        this.title, 
        required this.url, 
        this.statusBarColor, 
        this.hideAppBar}); 
    //命名工厂构造函数必须要有返回值,类似static 函数无法访问成员变量和方法 
    factory CommonModel.fromJson(Map<String, dynamic> json) { 
        return CommonModel( 
            icon: json['icon'], 
            title: json['title'], 
            url: json['url'], 
            statusBarColor: json['statusBarColor'],
            hideAppBar: json['hideAppBar'] 
         ); 
       }
  }
  • 对于可空的字段通过?进行修饰
  • 对于不可空的字段,需要在构造方法中在对应的字段前面添加required修饰符来表示这个参数是必传参数

单例空安全适配技巧

适配前:

class HiCache { 
    SharedPreferences prefs; 
    static HiCache _instance;
    HiCache._() { 
        init(); 
    } 
    HiCache._pre(SharedPreferences prefs) { 
        this.prefs = prefs; 
    } 
    static Future<HiCache> preInit() async { 
        if (_instance == null) { 
            var prefs = await SharedPreferences.getInstance(); 
            _instance = HiCache._pre(prefs); 
        } 
      return _instance; 
    } 
    static HiCache getInstance() { 
        if (_instance == null) { 
            _instance = HiCache._(); 
        } return _instance; 
    } 
    void init() async { 
        if (prefs == null) { 
            prefs = await SharedPreferences.getInstance(); 
         } 
    } 
    setString(String key, String value) { 
        prefs.setString(key, value); 
    } 
    setDouble(String key, double value) { 
        prefs.setDouble(key, value); 
    } 
    setInt(String key, int value) { 
        prefs.setInt(key, value); 
    } 
    setBool(String key, bool value) { 
        prefs.setBool(key, value); 
    } 
    setStringList(String key, List<String> value) { 
        prefs.setStringList(key, value); 
     } 
     T get<T>(String key) { 
         return prefs?.get(key) ?? null; 
     } 
}

适配后:

class HiCache {
  SharedPreferences? prefs;
  static HiCache? _instance;
  HiCache._() {
    init();
  }
  HiCache._pre(SharedPreferences prefs) {
    this.prefs = prefs;
  }
  static Future<HiCache> preInit() async {
    if (_instance == null) {
      var prefs = await SharedPreferences.getInstance();
      _instance = HiCache._pre(prefs);
    }
    return _instance!;
  }

  static HiCache getInstance() {
    if (_instance == null) {
      _instance = HiCache._();
    }
    return _instance!;
  }

  void init() async {
    if (prefs == null) {
      prefs = await SharedPreferences.getInstance();
    }
  }

  setString(String key, String value) {
    prefs?.setString(key, value);
  }

  setDouble(String key, double value) {
    prefs?.setDouble(key, value);
  }

  setInt(String key, int value) {
    prefs?.setInt(key, value);
  }

  setBool(String key, bool value) {
    prefs?.setBool(key, value);
  }

  setStringList(String key, List<String> value) {
    prefs?.setStringList(key, value);
  }

  remove(String key) {
    prefs?.remove(key);
  }

  T? get<T>(String key) {
    var result = prefs?.get(key);
    if (result != null) {
      return result as T;
    }
    return null;
  }
}

插件的空安全适配问题

三方插件的空安全适配问题

目前在Dart的官方插件平台上的主流插件都陆续进行了空安全支持,如果你的项目开启了空安全那么所有使用的插件也必须是要支持空安全的,否则会导致无法编译:

Xcode's output: Error: Cannot run with sound null safety, because the following dependencies don't support null safety: 
        - package:flutter_splash_screen

遇到这个问题后可以到Dart的官方插件平台查看这个flutter_splash_screen插件是否有支持了空安全的版本。如果插件支持了空安全插件平台会为其打上空安全的标:

image.png

如果你所使用的某个插件还不支持空安全,而且你又必须要使用这个插件,那么可以通过关闭空安全检查。

学习任何一门编程语言都需要自己感兴趣,不能强迫,不能有三分钟热度,为自己加油打call.