Flutter 基础知识总结

1、Flutter 介绍与环境安装

为什么选择 Dart:

  • 基于 JIT 快速开发周期:Flutter 在开发阶段采用 JIT 模式,避免每次改动都进行编译,极大的节省了开发时间
  • 基于 AOT 发布包:Flutter 在发布时可以通过 AOT 生成高效的 ARM 代码以保证应用性能
  • UI 帧率可达 120 FPS:为了快速流畅的用户体验需要能够在每个动画帧运行大量的代码,不能有周期性的停顿,否则会造成掉帧
  • 单线程:不需要锁,不存在数据竞争和变量状态同步,也没有线程上下文切换的性能损耗和锁导致的卡顿
  • 垃圾回收:多生代(参考了 JVM)无锁垃圾回收器,专门为 UI 框架中常见的大量 Widgets 对象创建和销毁优化

JIT(Just In Time)即时编译,在程序执行期间实时编译为本地机器码;AOT(Ahead Of Time)静态编译,程序运行前编译成本地机器码。在代码的执行效率上,JIT 不如 AOT。

2、Dart 基础语法

Dart 不用编译了,dart xxx.dart 直接就执行,不像 Java 需要先 javac 编译出 class 文件再 java xxx.class 执行。

2.1 变量

类型与声明

变量都是引用类型,未初始化的变量的值是 null。

声明变量的方式通常有三种:

  1. Object:与 Java 一样 Object 是所有类的基类,Object 声明的变量可以是任意类型。比如数字(包括 int 类型的数字)、方法和 null 都是对象
  2. var:声明的变量在赋值的那一刻,决定了它是什么类型
  3. dynamic:不是在编译时候确定实际类型的,而是在运行时。dynamic 声明的变量行为与 Object 一样,使用一样,关键在于运行时原理不同

示例代码:

dart 复制代码
void test1() {
	// 1.通过类型声明变量
	Object i = "Test";
	
	// 2.通过 var 声明变量
	var j = "Test";
	// 报错,声明时已经确定了变量类型,不可更改
	// j = 100;
	
	// 3.声明时没有具体指明是什么类型,那么就是默认的 Object 类型
	var k;
	// 可以为 k 赋值为 Object 的子类型
	k = "Test";
	k = 100;
	
	// 4.dynamic 动态类型可以赋值不同类型
	dynamic z = "Test";
	z = 100;
}

需要注意的地方:

  • 所有类型,没有初始化的变量自动获取一个默认值为 null
  • 声明变量时,可以选择加上具体类型,如int a = 1;,但对于局部变量,按照 Dart 代码风格,使用 var 而不是具体类型

final 与 const

final 声明运行时常量,const 声明编译器常量(相比于运行时常量可让代码运行更高效),二者都用于声明常量,可以替代任何类型,只能在声明时初始化,且不能改变:

dart 复制代码
void test2() {
	// 1.const 与 final 可以替代任何类型
	const a = 1;
	final b = 1;
	const int c = 1;
	final int d = 1;
	
	// 2.不能通过运行时常量构造编译时常量
	final m = 1;
	// 企图通过运行时常量构造编译时常量,导致 const 值无法确定
	// const n = m + 1;
	
	// 使用编译时能够确定的值构造 const 常量是可以的
	const x = 1;
	const y = x + 1;
}

类的变量可以是 final 但不能是 const。如果 const 变量在类中,需要定义为 static const 静态常量:

dart 复制代码
class T {
    static const i = 2; // 正确
    const j = 1; // 错误
}

2.2 内置类型

Dart 内置以下类型:

  • numbers
  • strings
  • booleans
  • lists(也被称之为 arrays)
  • maps
  • runes(用于在字符串中表示 Unicode 字符)
  • symbols

数值 num

num 是数字类型的父类,有两个子类 int 和 double。

int 的默认实现是 64 位,如果编译成 JavaScript,就是 32 位。在编码时,如果 int 长度超过 4 个字节,那么 Dart 会将其编译为类似 Java 的 long 类型,否则编译成 Java 中的 short 或 int。

也就是说,int 的长度是动态确定的,可以通过 int 的 bitLength() 确定存储该 int 变量所需要的最小的位数。

但实际上,不应该将 Dart 的 int 和 Java 的 int 做类比,因为前者是一个类,后者是一个基本类型的关键字。从本质上说,二者不是一个东西,没有可比性。

字符串 String

Dart 字符串是 UTF-16 编码的字符序列,使用方法如下:

  • 可以使用单引号或者双引号来创建字符串:

    dart 复制代码
    var name = 'lance';
    // 如果插入一个简单的标识符,而后面没有紧跟更多的字母数字文本,那么 {} 应该被省略
    var a = "my name is $name!";
    var b = "my name is ${name.toUpperCase()}!";
  • 与 Java 一样可以使用 + 操作符来把拼接字符串,也可以把多个字符串放到一起来实现同样的功能:

    dart 复制代码
    var a  = "my name is " "lance";
  • 使用三个单引号或者双引号可以创建多行字符串对象:

    dart 复制代码
    var s1 = '''
    You can create
    multi-line strings like this one.
    ''';
    
    var s2 = """This is also a
    multi-line string.""";
  • 可以通过单引号嵌套双引号,或双引号嵌套单引号进行转义:

    dart 复制代码
    print("'Test'"); // 'Test'
    print('"Test"'); // "Test"
  • 也可以使用 Java 的方式转义,或者使用 Dart 的 r 前缀创建一个原始字符串实现转义:

    dart 复制代码
    // 两行输出结果均为 换行符 \n
    print("换行符 \\n");
    print(r"换行符 \n");

布尔类型 bool

Dart 的布尔类型 bool 有 true 和 false 两个对象。

列表 List

Dart 的数组是 List 对象,它有两种声明方式:

  1. 当作 List 对象声明:

    dart 复制代码
    // new 可以省略
    var list = new List(1);
  2. 当作数组声明:

    dart 复制代码
    var list = [1, 2, 3];

通过 for 循环遍历 List 也有两种方式:

dart 复制代码
for(var item in list) {
    print(item);
}

for(var j = 0;j < list.length; ++j) {
    print(list[j]);
}

当数组与 const 相结合时,需要注意:

dart 复制代码
	List<int> list1 = const[1,2,3];
	// Unsupported operation: Cannot add to an unmodifiable list
	//list1.add(4);
	
	const List<int> list2 = [1,2];
	// Error: Can't assign to the const variable 'list2'.
	//list2 = list1;
	// Unsupported operation: Cannot add to an unmodifiable list
	//list2.add(4);

const 修是谁,谁就不可变:

  • list1 指向不可变的 [1,2,3],那么就不能修改数组,但是可以指向其他数组对象
  • list2 本身是一个常量引用,那么它就只能指向 [1,2],不能修改索引,也不能修改索引的内容

映射集合 Map

两种声明方式:

dart 复制代码
var companys = {'a': '阿里巴巴', 't': '腾讯', 'b': '百度'};
var companys2 = new Map();
// 添加元素
companys2['a'] = '阿里巴巴';
companys2['t'] = '腾讯';
companys2['b'] = '百度';

// 获取与修改元素
var c = companys['c']; // 没有对应的 key 返回null
companys['a'] = 'alibaba'; 

const 与 Map 结合的情况与 List 样。

Runes

Runes 主要用于获取特殊字符的 Unicode 编码,或者需要将 32 位的 Unicode 编码转换为字符串。

Dart 表达 Unicode 代码点的常用方法是 \uXXXX,其中 XXXX 是 4 位十六进制值。要指定多于或少于 4 个十六进制数字,需要将值放在大括号中:

dart 复制代码
var clapping = '\u{1f44f}'; // 5 个 16 进制 需要使用 {}
print(clapping); //👏
// 获得 16 位代码单元
print(clapping.codeUnits); // [55357, 56399]
// 获得 Unicode 代码
print(clapping.runes.toList()); // [128079]

// fromCharCode 根据字符码创建字符串
print(String.fromCharCode(128079));
print(String.fromCharCodes(clapping.runes));
print(String.fromCharCodes([55357, 56399]));
print(String.fromCharCode(0x1f44f));

Runes input = new Runes(
  '\u2665  \u{1f605}  \u{1f60e}  \u{1f47b}  \u{1f596}  \u{1f44d}');
print(String.fromCharCodes(input));

这里要清楚一个代码点和代码单元的概念:

代码点(Code Point)和代码单元(Code Unit)

代码单元与代码点

简言之,代码点就是字符集中每个字符的值,比如上面代码中👏符号在 Unicode32 中的值为 0x1f44f。

代码单元指编码集中具有最短比特组合的单元。对于 UTF-8 来说,代码单元是 8 比特长;对于 UTF-16 来说,代码单元是 16 比特长。换一种说法就是 UTF-8 的是以一个字节为最小单位的,UTF-16 是以两个字节为最小单位的。

我们在 Java 中常说 String.length() 是获取字符串长度,实际上是不严谨的,应该说是 UTF-16 编码表示下的代码单元数量,而不是字符个数。例如:

java 复制代码
String a = "\uD83D\uDC4F";
printf(a); // 👏
printf(a.length()); // 2

你看打印输出的长度为代码单元个数 2,而不是 a 中字符的个数。charAt() 也是类似的情况。

Symbols

操作符标识符,可以看作C中的宏。表示编译时的一个常量:

dart 复制代码
var i = #A; // 常量
print(i.runtimeType); // Symbol

main() {
  print(i);
  switch(i) {
    case #A:
      print("A");
      break;
    case #B:
      print("B");
      break;
  }
  var b = new Symbol("b");
  print(#b == b); // true
}

2.3 操作符

主要看 Java 没有的操作符:

  1. 类型判定操作符:is!is 用于判断对象是否为某种类型,as 用于将对象转换为特定类型

  2. 赋值操作符:??= 用来指定值为 null 的变量的值,比如:

    dart 复制代码
    // 如果 b 是 null,则 value 赋值给 b,否则 b 的值保持不变
    b ??= value;
  3. 条件表达式:

    • condition ? expr1 : expr2:如果 condition 为 true 则执行 expr1,否则执行 expr2
    • expr1 ?? expr2:如果 expr1 不为 null 则取 expr1,否则返回 expr2 的值
  4. 级联操作符:..可以在同一个对象上连续调用多个函数以及访问成员变量,这样可以避免创建临时变量,代码看起来也更加流畅:

    dart 复制代码
    // StringBuffer write() 相当于 Java 的 append
    var sb = new StringBuffer();
    sb..write('foo')..write('bar');
  5. 安全操作符:?.左值如果为 null 则返回 null:

    dart 复制代码
    String sb;
    // 报空指针异常
    print(sb.length);
    // 打印输出 null
    print(sb?.length);

3、方法

3.1 一等方法对象

Dart 是一个真正的面向对象语言,方法也是对象,类型为Function。 这意味着,方法可以赋值给变量,也可以当做其他方法的参数。可以把方法当做参数调用另外一个方法。

在 Java 中如果需要能够通知调用者或者其他地方方法执行过程的各种情况,可能需要指定一个接口,比如 View 的 OnClickListener。而在 Dart 中,我们可以直接指定一个回调方法给调用的方法,由调用的方法在合适的时机执行这个回调:

dart 复制代码
void setListener(Function listener) {
    listener("Success");
}

// 或者
void setListener(void listener(String result)){
    listener("Success");
}

// 两种方式,第一种调用者根本不确定回调函数的返回值、参数是些什么
// 第二种则需要写这么一大段,太麻烦

// 第三种:类型定义,将返回值为 void,参数为一个 String 的方法定义为一个类型
typedef void Listener(String result);
void setListener(Listener listener){
  listener("Success");
}

上面演示了方法作为参数的三种形式:

  1. 第一种使用 Function 表示一个方法,但是这种形式无法确定方法的参数以及返回值类型,因此不好
  2. 第二种直接将方法的原型写在方法参数中,写起来麻烦,看起来也不舒服,因此也 pass
  3. 第三种将方法定义为一个类型,使用该类型作为方法参数,推荐这种写法

3.2 可选命名参数

将方法的参数放到 {} 中就变成可选命名参数:

dart 复制代码
int add({int? i, int? j}) {
  if (i == null || j == null) {
    return 0;
  }
  return i + j;
}

调用方法时使用 key-value 形式指定参数:

dart 复制代码
void main() {
  print(add()); // 0
  print(add(i: 1, j: 2)); // 3
}

3.3 可选位置参数

将方法的参数放到 [] 中就变成可选位置参数,传值时按照参数位置顺序传递:

dart 复制代码
int add([int? i, int? j]) {
  if (i == null || j == null) {
    return 0;
  }
  return i + j;
}

调用时可以不传入全部的参数,参数按照参数声明的顺序赋值:

dart 复制代码
void main() {
  print(add()); // 0
  print(add(1)); // 0
  print(add(1, 2)); // 3
}

可选命名参数与可选位置参数的出现使得方法重载的实现更容易。在 Java 中,方法重载需要写出多个不同参数的方法,但是在 Dart 中通过将方法声明为可选命名参数或可选位置参数,写一个方法,在调用时传入所需参数即可。

3.4 默认参数值

定义方法时,可选参数可以使用 = 来定义可选参数默认值:

dart 复制代码
int add([int i = 1, int j = 2]) => i + j;
int add({int i = 1, int j = 2}) => i + j;

3.5 匿名方法

没有名字的方法,称之为匿名方法,也可以称之为 lambda 或者 closure 闭包。匿名方法的声明方式为:

dart 复制代码
([Type] param1, ...) { 
  codeBlock; 
}; 

比如:

dart 复制代码
var list = ['apples', 'oranges', 'grapes', 'bananas', 'plums'];
list.forEach((i) {
	print(i);
});

4、异常

Dart 的异常机制也像 Kotlin 一样非常灵活,不像 Java 那样强制你捕获异常。

所有的 Dart 异常是非检查异常,方法不一定声明了他们所抛出的异常, 并且不要求你捕获任何异常。

Dart 的异常类型有ExceptionError两种根类型还有若干个它们的子类型,在抛出异常时,可以抛出任何非 null 对象,不局限于ExceptionError以及它们的子类型:

dart 复制代码
throw new Exception('这是一个异常');
throw '这是一个异常';
throw 123;

Dart 虽然也支持 try-catch-finally 捕获异常,但是 catch 无法指定类型,需要结合 on 使用:

dart 复制代码
try {
	throw 123;
} on int catch(e) {
	// 使用 on 指定捕获 int 类型的异常对象,on TYPE catch(e)      
} catch(e,s) { // 两个参数的类型分别为 _Exception 和 _StackTrace
    rethrow; // 使用 `rethrow` 关键字可以把捕获的异常给重新抛出
} finally {}

catch() 可以接收两个参数:

  • 第一个参数 e 是被抛出的异常对象,类型是 _Exception
  • 第二个参数 s 是堆栈信息对象,类型是 _StackTrace,通过 print(s) 可以输出异常堆栈信息

骚操作,抛出异常时抛出一个方法,catch 的时候可以通过捕获 Function 类型来执行该方法。

5、类

Dart 是面向对象的语言,所有类都继承自 Object。

命名风格:

  • 使用 lowercase_with_underscores 风格命名库和文件名
  • 使用 upperCamelCase 命名类型名称
  • 使用 lowerCamelCase 命名其他标识符
  • 推荐使用 lowerCamelCase 命名常量

每个实例变量会自动生成一个隐含的 getter 方法,非 final 实例变量还会自动生成一个 setter 方法:

dart 复制代码
class Point {
    // 公有变量
    num x = 0;
    // _开头的是私有变量
    num _y = 0;
}

Dart 在作用域上并没有 Java 那样 public、private 的关键字,作用域只有公有与私有之分,用 _ 开头表示私有变量或私有类,不以 _ 开头的就是公有的类或变量。

5.1 构造函数

常规构造函数

dart 复制代码
class User {
  // 初始值一定要给,否则编译不通过
  String name = "";
  int age = 0;
  User(String name, int age) {
    this.name = name;
    this.age = age;
  }
}

由于把构造函数的参数赋值给实例变量的场景太常见,因此 Dart 提供了语法糖来简化操作:

dart 复制代码
class User {
  String name = "";
  int age = 0;
  
  User(this.name, this.age);
}

也可以使用 {} 将构造函数的参数声明为可选位置参数,只不过此时不能用 this:

dart 复制代码
class User {
  // 成员变量要有初始值
  String name = "";
  int age = 0;
  // 使用可选命名参数,由于 name 和 age 都不可为 null,因此
  // 参数也需要设置默认值,防止没有为其传参时将成员变量赋值为 null
  User({String name = "", int age = 0}) {
    this.name = name;
    this.age = age;
  }
}

void main() {
  var user0 = User(name: "User0"); // name = User0, age = 0
  var user1 = User(age: 30); // name = , age = 30
  var user2 = User(age: 22, name: "User2"); // name = User2, age = 22
}

命名构造函数

Dart 不允许任何函数的重载,不论是构造函数还是成员函数还是顶级函数。但有时我们确实有重载构造函数的需求,此时可以使用命名构造函数为一个类实现多个构造函数:

dart 复制代码
class User {
  String name = "";
  int age = 0;
  User(this.name, this.age);
  // 命名构造函数,在 . 后面随意取名,调用时也使用改名字进行构造即可
  User.fromJson(Map json) {
    name = json['name'];
    age = json['age'];
  }
}

void main() {
  var map = {'name': 'User', 'age': 33};
  var user = User.fromJson(map);
}

好处是可以通过名字判断出构造函数的大致意图和内容,更加直观。比如 User.fromJson() 就能看出是通过 Json 数据构造 User 对象。

构造函数初始化列表

这一点跟 C++ 很像:

dart 复制代码
class User {
  String name = "";
  int age = 0;
  User(String name, int age)
      : name = name,
        age = age;
  User.fromJson(Map json)
      : name = json['name'],
        age = json['age'];
}

重定向构造函数

dart 复制代码
class View {
  View(int context, int attr);
  // 会调用上面的构造函数
  View.a(int context) : this(context, 0);
}

常量构造函数

这里的常量指的是编译器常量,首先需要使用 const 修饰构造函数:

dart 复制代码
class ImmutablePoint {
  final int x;
  final int y;
  // 常量构造函数要求成员必须是 final 的
  const ImmutablePoint(this.x, this.y);
}

然后在构造对象时,不使用 new,而是使用 const,并且要求构造不同对象时传入的参数必须是一样的:

dart 复制代码
void main() {

  var p1 = const ImmutablePoint(1, 1);
  var p2 = const ImmutablePoint(1, 1);
  var p3 = const ImmutablePoint(1, 2);
  var p4 = new ImmutablePoint(1, 1);

  print('''p1 == p2:${p1 == p2}
p1 == p3:${p1 == p3}
p1 == p4:${p1 == p4}''');
}

输出结果为:

shell 复制代码
p1 == p2:true
p1 == p3:false
p1 == p4:false

主要用于同一个对象被多次使用时,比如 UI 上显示三个相同的 ListItem,使用常量构造函数就可以创建出一个对象,而不是三个对象,节省了内存。

工厂构造函数

使用 factory 关键字修饰,必须返回一个本类或子类的实例对象:

dart 复制代码
class Person {
  // 返回本类对象
  factory Person.get() {
    return new Person();
  }

  // 返回子类对象
  factory Person.getStudent() {
    return new Student();
  }

  // 如果想要被,需要有一个常规构造函数
  Person();
}

class Student extends Person {}

在 Dart 中使用单例模式时就可以用到 factory:

dart 复制代码
class Person {
  // 使用 _ 让静态对象私有化,并且类型后面加问号表示为可空
  // 类型,否则就要在声明 Person 对象时立即为其初始化
  static Person? _instance;

  // 定义工厂构造函数返回单例
  factory Person.getInstance() {
    // 如果 _instance 为 null 才创建对象
    _instance ??= Person._newInstance();
    // 返回 _instance,后接的感叹号表示非 null 
    return _instance!;
  }
    
  // 创建一个私有的常规构造函数,这样默认的构造函数 Person() 就没有了 
  Person._newInstance();
}

这样在 Person 类所在的文件之外,就无法访问到私有的 _instance 和 _newInstance(),只能通过 getInstance() 获取到 Person 的单例:

dart 复制代码
var person = Person.getInstance();

5.2 getter & setter

Dart 中每一个实例属性都会有一个隐式的 getter,非 final 还有 setter。

首先来看一个错误示例:

dart 复制代码
class Point {
  int x = 0;
  int get x => x + 10;
}

在定义 x 的 getter 时编译器会报错,说 x 已经定义过。因此如果想自定义属性的 getter 或 setter 需要将属性声明为私有的:

dart 复制代码
class Point {
  int _x = 0;
  int _y = 0;
  int get x => _x + 10;
  int get y => _y + 20;
}

在一个需要注意,getter 与 setter 是方法,而不是属性,因此可以在方法名后面加上 {} 在里面写相关逻辑:

dart 复制代码
class Point {
  int _x = 0;
  int get x {
    return _x + 10;
  }

  // setter 需要有一个参数
  set x(int value) {
    _x = value;
  }
}

5.3 操作符重载

重载 + 运算符:

dart 复制代码
class Point {
  int x = 0;
  int y = 0;
  Point(this.x, this.y);
  // 用 operator 接上要重载的操作符
  Point operator +(Point other) => Point(x + other.x, y + other.y);
}

这样可以用 + 连接两个 Point 对象:

dart 复制代码
void main() {
  var point = Point(10, 20) + Point(30, 50);
  print("x = ${point.x}, y = ${point.y}"); // x = 40, y = 70
}

Dart 的操作符重载非常灵活,返回值的类型不受限制,比如上面重载 + 时返回一个 Point 是我们的常规操作,但是你也可以根据自己需要返回其他类型,比如 String、int 等等。

5.4 抽象类与接口

使用 abstract 定义抽象类,抽象类中允许出现无方法体的方法:

dart 复制代码
abstract class Parent {
  String name = "";
  // 抽象方法前面不能加 abstract
  void printName();
}

Dart 没有 interface 关键字,Dart 中的每个类都隐式定义了一个包含所有实例成员的接口:

dart 复制代码
class A {
  void a() {}
}

class B implements A {
  @override
  void a() {}
}

5.5 其他语法

可调用的类

如果类中定义了 call 方法,可以通过该类实例对象后接 () 的形式快速调用 call:

dart 复制代码
void main() {
  var a = A();
  a();
}

class A {
  void call() {print("invoke call method.");}
}

call 方法可以带参数。

混合 mixins

mixins 是一种在多类继承中重用一个类代码的方法,基本形式如下:

dart 复制代码
void main() {
  var c = C();
  c.a();
  c.b();
  c.c();
}

mixin A {
  void a() {}
}

mixin B {
  void b() {print("B");}
}

class C with A, B {
  void c() {}
  @override
  void b() {print("C");}
}

注意:

  • 被混入的类需要用 mixin 声明,并且不能有构造函数,否则就无法作为被混入的类出现在 with 后面

  • 混合结果的 C 类中,可以重写,也可以重新定义 A、B 中的方法

  • 如果 A、B 内定义了同名方法,且 C 也定义了同名方法,那么 C 的实例在调用该方法时实际上调用的是 C 中的方法;如果 C 中没有定义同名方法,那么 C 调用的就是 B 中的方法(根据 with 后面的排序,优先取顺位靠后的)

  • 在 C 中可以通过 super 调用 A 或 B 中的方法,比如:

    dart 复制代码
    mixin A {
      void a() {}
    }
    
    class C with A, B {
      void a() {
        super.a();
      }
    }
  • 上述几点能发现 mixin 与多继承的表现有很多相似之处

相关推荐
ALLIN16 小时前
Flutter 三种方式实现页面切换后保持原页面状态
flutter
Dabei16 小时前
Flutter 国际化
flutter
Dabei16 小时前
Flutter MQTT 通信文档
flutter
Dabei19 小时前
Flutter 中实现 TCP 通信
flutter
孤鸿玉19 小时前
ios flutter_echarts 不在当前屏幕 白屏修复
flutter
前端 贾公子21 小时前
《Vuejs设计与实现》第 16 章(解析器) 上
vue.js·flutter·ios
tangweiguo030519871 天前
Flutter 数据存储的四种核心方式 · 从 SharedPreferences 到 SQLite:Flutter 数据持久化终极整理
flutter
0wioiw01 天前
Flutter基础(②④事件回调与交互处理)
flutter
肥肥呀呀呀1 天前
flutter配置Android gradle kts 8.0 的打包名称
android·flutter
吴Wu涛涛涛涛涛Tao1 天前
Flutter 实现「可拖拽评论面板 + 回复输入框 + @高亮」的完整方案
android·flutter·ios