Flutter之Dart语言简述
Flutter之Dart语言简述
最近开始接触Flutter相关的项目,通过源码、例子和一些资料的阅读,发现如果掌握了Dart的一些语法和一些基础对于Flutter的学习有着事半功倍的效果。下面是我在查阅一些资料和实际的开发中的一些总结。希望对今后的学习有所帮助。
Dart 诞生于 2011.10.10 日, 谷歌 Dart 语言项目的领导人 Lars Bak 在丹麦举行的Goto 会议上 布, Dart 种“结构化的 Web 程”语言, Dart 编程语言在所有现代浏览器和环境中提供高性能。
Dart语言的特性:
- Dart 是AOT(Ahead Of Time)编译的,编译成快速、可预测本地的代码。
- Dart也可以JIT(Just In Time)编译,开发周期异常快,包括Flutter流行的亚秒级有状态热重载。
- Dart可以更轻松地创建以60fps运行的流畅动画和状态,Dart可以在没有锁的情况下进行对象分配和垃圾回收。
- Dart使Flutter不需要单独的声明式布局语言,或单独的可视化界面构建器,因为Dart的声明式编程布局易于阅读和可视化。
Dart重要概念:
- 一切皆对象,无论是变量、数字、函数等都是对象,所有的对像都继自Object, 这点类似于 Java 语言。
- 程序中指定数据类型使得程序合理地分配内存空间,并帮助编绎器进行语法检查,由于Dart 言是弱数据类型, 所以类型不是必须的 。
- Dart 代码在运行前解析 指定数据类型和编译时的常量, 可以提高运行速度。
- Dart 程序有统 的程序人口: main () 这一点与 Java C 和 C++ 语言很像。
- Dart 没有
public
protected
private
这些修饰符的概念,私有特性通过变量或函数加上下划线来表示。 - Dart 工具可以检查出警告信息(
warning
) 和 错误信息(errors
), 警告信息只是表明代码可能不工作,但是不会妨碍程序运行,错误信息可以是编译时的错误,也可能是运行时的错误, 编译时的错误将阻止程序运行,运行时的错误将会以异常(exception )的方式呈现。 - Dart 支持 anync/await 异步处理。
- 关键字( 56 个)如下:
- 带有上标 1 的关键字是 内置关键字。避免把内置关键字当做标识符使用。 也不要把内置关键字 用作类名字和类型名字。
- 带有上标 2 的关键字,是在 Dart 1.0 发布以后又新加的,用于支持异步相关的特性。
变量与基本类型
变量的声明
在Dart里,变量的声明有三种方式:
-
用
var
关键字用var声明变量时,如果未指定初始值,可以变成任意类型。如果指定了类型,则类型会被锁定。如:
void main() { var v; // 初始值未指定类型 v = '2'; // 可以变为任意类型 v = 3; print('v: $v'); var v1 = '111'; // 初始化时已经指定类型 var v1 = 222; // 此时无法赋值成功,编译时报错 }
-
用
dynamic
关键字dynamic声明的变量,表示动态任意类型,编译时不检查类型。如:
void main() { dynamic d1 = '111'; d1 = 222; d1 = 333.0; print("d1: $d1"); // dynamic修饰的变量,在编译时不做检查 d1.test(); // 这行还代码,在编译时是不会检查错误信息的,运行时会报错 }
-
用
Object
Object声明的变量,表示任意动态类型,编译时检查类型。如:
void main() { Object o1 = "sss"; o1 = 222; o1.test(); // 编译时会做检查,报错 }
变量的默认值
- 没有初始化的变量会自动获取一个默认值null
- 一切皆为对象,对象的默认值为null
final 和 const
final 或 const 修饰的变量为常量或固定值。
- 共同点:
- 声明的类型可以省略
- 初始化后不能再赋值
- 不能和
var
同时使用
- 不同点:
- 类级别常量,使用
static const
- const 可以使用其它const常量的值来初始化其值
- 使用const赋值声明,const可省略
- 可以更改非final、非const变量的值,即使曾经具有const值
- const 导致的不可变性是可传递的
- 相同的const常量不会在内存中重复创建
- const需要编译时常量
- 类级别常量,使用
基本类型
Dart语言常用的基本数据类型包括: Number
,String
,Boolean
,List和Map
-
Number类型
Number 类型包括 int 整形,double浮点类型,他们都是
num
类型的子类 -
String类型
- Dart 字符串是 utf-16 编码的字符序列,可以使用单引号或者双引号来创建字符
- 可以使用三个单引号或者双引号创建多行字符串对象
- 可以使用r前缀创建原始字符串,即输出原始字符,字符串里的转义等符号不会生效
- 可以在字符串中使用表达式:
"${expression}"
,如果表达式是一个标识符,可以省略{}
,如果表达式的结果为一个对象,则 Dart会调用对象的toString
方法
-
Boolean类型
Dart是强bool类型检查,只有bool类型的值是true才被认为是true, 如果未赋初值,则为null
-
List类型
在Dart语言中,具有一系列相同类型的数据称为List对象。Dart中List可以直接打印出出各个元素,而java中则是地址。
-
Map类型
与java类似。
-
Set
- set1.difference(set2): 返回 set1 集合里有但 set2 里没有的元素集合
- set1.intersection(set2):返回set1和set2的交集
- set1.union(set2):返回set1和set2的并集
- set1.retainAll():set1只保留某些元素(要保留的元素要在原set中存在)
-
Runs
- 用在符串中表示Unicode字符
- 使用String.fromCharCodes显示字符图形
- 如果非4个数值,需要把编码值放到大括号中
函数
Dart是一个面向对象的语言,所以函数也是对象,函数属于Function对象。函数可以像参数一样传递给其他函数。
函数定义
-
定义函数时可以省略类型(不建议)
void func(a, b){} // 参数a,b都省略了类型
-
支持缩写语法
=>
void main() => runApp(MyApp()); // 当函数体只有一条语句时可以使用此种写法
-
可在函数内定义函数
可以在函数里定义函数,如:
int func(int a, int b) { int sum(int a, int b, int c) { return a + b + c; } return sum(a, b, 1); }
dart 函数支持闭包
Function addFunction(int a) { return (y) => a + y; } void main() { var addFunc = addFunction(12); print(addFunc(22)); }
函数返回值
- 所有的函数都有返回值
- 如果没有指定函数返回值,则默认的返回值是null
- 没有返回值的函数,系统会在最后添加隐式的return语句
可选参数
-
可选命名参数
使用
{param1, param2,...}
的形式来指定命名参数。main() { func(a:1); func(b:1); func(a:1, b:1); } int func({int a, int b}) { return a + b; }
-
可选位置参数
将参数使用中括号[]括起来,用来表明是可选位置参数,必填参数要放在可选参数前面。
String getUserInfo(String name, String sex, [String from]) {}
其中name和sex是必须传入的参数,from参数可以不传
-
参数默认值
- 如果参数指定了默认值,当不传入值时,函数就会使用这个默认值。通常默认值参数为null。
String getUserInfo(String name, String sex, [String from = '中国']) {}
- 可选命名参数默认值(默认值必须是编译时常量),可以使用等号
=
或冒号:
,Dart SDK 1.21 之前只能用冒号,冒号的支持以后会移除,所以建议使用等号。 - 可选位置参数默认值(默认值必须是编译时常量),只能使用等号
=
。 - 可使用list或map作为默认值,但必须是const。
int fun([List list = const [1, 2, 3]]) {}
匿名函数
-
可赋值给变量,通过变量调用
main() { var func = (int a, int b) => a + b; func(1, 2); }
-
可在其他函数中直接调用或传递给其他函数
函数别名
函数别名使用typedef
表示,如:
typedef Fun1(int a, int b);
typedef Fun2<T, K>(T a, K b);
- typedef给函数起一个别名,使用比较方便。例如定义一个方法的回调,直接使用别名定义。
- 没返回值,则只要参数匹配就行了,如果定义了返回值,则返回值不一样会报错。
main函数
Flutter应用程序必须要有一个main函数作为程序的入口函数。
运算符
Dart所有的运算符如下表所示, 与java不同的有8个,如下:
?.
,条件成员访问 和 . 类似,但是左边的操作对象不能为 null,例如 foo?.bar 如果 foo 为 null 则返回 null,否则返回 bar 成员。
~/
,除后取整。
as
,类型转换。
is
,如果对象是指定类型返回true。
is!
,如果对象是指定类型返回false。
??
,双问号左边为true返回左边结果,否则返回右边结果。
..
,级联语法。严格来说, 两个点的级联语法不是一个操作符。 只是一个 Dart 特殊语法。
??:
,如果左边是 null,则右边赋值给左边;如果不是 null,则左边的值保持不变。
描述 | 操作符 |
---|---|
后缀操作 | expr++ expr-- () [] . ?. |
前缀操作 | -expr !expr ~expr ++expr --expr |
乘除 | * / % ~/ |
加减 | + - |
位移 | << >> |
按位与 | & |
按位异或 | ^ |
按位或 | | |
类型操作 | >= > <= < as is is! |
相等 | == != |
逻辑与 | && |
逻辑或 | || |
是否为空 | ?? |
三目运算 | expr1 ? expr2 : expr3 |
级联 | .. |
赋值 | = *= /= ~/= %= += -= <<= >>= &= ^= |= ??= |
流程控制语句
Dart中控制流程语句和Java类似。List和Set等实现了Iterable接口的类支持for-in遍历元素。
- if else
- for forEach
for-in
- while do-while
- break continue
- switch case
- assert
- try-catch throw
异常处理
异常是表示发生意外错误,如果没有捕获异常,引发异常的隔离程序将被挂起,并且程序终止。
Dart代码可抛出并捕获异常,但Dart的所有异常都是未检查异常,方法不声明他们可能抛出的异常,也不需要捕获任何异常。
Dart 代码可以抛出任何非 null 对象为异常,不仅仅是实现了 Exception 或者 Error 的对象。
可以使用on 或者 catch 来声明捕获语句,也可以 同时使用。使用 on 来指定异常类型,使用 catch 来 捕获异常对象。
-
抛出异常
// 抛出Exception对象 throw FormatException('抛出一个formatException') // 抛出Error对象 throw OutMemoryError(); //或自定义异常 throw '数据非法'
-
捕获异常
catch()
可以指定一个或两个参数来捕获异常,第一个是抛出的异常,第二个是堆栈跟踪,如:try { ... } on Error catch (e) { // 捕获异常详细信息 } catch (e, s) { // 堆栈跟踪信息 }
rethrow
把捕获的异常重新抛出 -
Finally
面向对象
Dart作为高级语言支持面向对象的很多特性并且支持基于mixin
的继承方式。基于mixin的继承方式是值:一个类可以继承多个父类,相当于其他语言里的多继承,所有的类都有同一个基类Object
。
实例化成员变量
类定义中所有的变量都会隐式的定义Setter方法,针对非空的变量会额外增加getter方法
构造函数
dart的构造函数有多种形式,如下:
-
常规构造函数
class User { String name; int age; User(String name, int age) { this.name = name; this.age = age; } // 或 User(this.name, this.age); }
-
命名的构造函数
class User { String name; int age; User.fromJson(Map json) { name = json['name']; age = json['age']; } }
使用命名构造函数可以为一个类实现多个构造函数, 或者使用命名构造函数来更清晰的表明你的意图。
-
重定向构造函数
class User { String name; int age; User(this.name, this.age); User.create(String name): this(name,12); }
一个重定向构造函数是没有代码的,在构造函数声明后,使用 冒号调用其他构造函数。
-
构造函数初始化列表
class User { // final 修饰的成员变量,只能通过构造方法进行赋值 final String name; final int age; // 初始化列表 User(name, age) : name = name, age = age; }
在构造函数体执行之前可以初始化实例参数。 使用逗号分隔初始化表达式。初始化列表非常适合用来设置 final 变量的值。
-
调用超类构造函数
在构造方法处使用
super
关键字,如:class User extends Person { // final 修饰的成员变量,只能通过构造方法进行赋值 final String name; final int age; // 初始化列表 User(this.name, this.age):super(name,age); }
-
常量构造函数
class User { // 定义const构造函数要确保所有的成员变量都是final修饰 final String name; final int age; static final User user = const User('111', 12); // const 关键字放在构造函数名前,且不能有函数体 const User(this.name, this.age); }
-
工厂构造函数(单例)
class User { String name; //工厂构造函数无法访问this,所以这里要用static static User _user; //工厂方法构造函数,关键字factory factory User([String name]) { return User._user ??= User._(name); } //定义一个命名构造函数用来生产实例 User._(this.name); }
读取和写入对象(Getter、Setter)
- get()和set()方法是专门用于读取和写入对象的属性的方法。
- 每一个类的实例,系统都隐式地包含有get()和set()方法。
- final 修饰的变量没有setter方法。
- 可以使用get 和 set 关键字定义getter和setter。
class User {
String name;
User(this.name);
String get username => 'getter ${this.name}';
set username(String name) => this.name = name;
}
重载操作
采用operator修饰,如:
class Vector {
final int x;
final int y;
const Vector(this.x, this.y);
Vector operator +(Vector v) {
return Vector(x + v.x, y + v.y);
}
}
抽象类
- 抽象类采用
abstract
关键字修饰class
。f - 不能被实例化,除非定义一个工厂构造函数。
- 抽象类通常用来定义接口, 以及部分实现。
- 抽象类通常具有抽象方法,抽象方法不需要关键字,以分号结束即可。
- 接口方式使用时,需要重写抽象类的成员变量和方法,包括私有的。
- 一个类可以implement一个普通类。Dart任何一个类都是接口。
- 一个类可以implement多个接口。
可调用类
类实现 call()
方法可以让类像函数一样能够被调用。
class ClassFunction {
call(String a, String b, String c) => '$a, $b, $c';
}
main() {
var cf = ClassFunction();
var out = cf('a1', 'b1', 'c1');
print('out: $out');
print(cf.runtimeType);
print(out.runtimeType);
print(cf is Function);
}
枚举类
与java一致,采用enum
修饰。
enum Type {
A, B, C
}
Mixins
Mixins(混入功能)相当于多继承,使用with关键字来实现Mixins的功能
class S {
a() => print('S.a');
}
class A {
a() => print('A.a');
b() => print('A.b');
}
class T = A with S;
- 子类没有重写超类A方法的前提下,如果2个或多个超类拥有相同签名的A方法,那么子类会以继承的最后一个超类中的A方法为准。
- 如果子类自己重写了A方法则以本身的A方法为准。
泛型
Dart1.21开始可以使用泛型函数。泛型函数可以在以下几个地方使用类型参数:
- 函数的返回值类型。
- 参数的类型。
- 局部变量的类型。
main() {
K addCache<K, V>(K key, V value) {
K temp = key;
print('${key}: ${value}');
return temp;
}
var key = addCache('key', 'value');
print(key);
}
要在使用构造函数时指定一个或多个类型,可将类型放在类名称后面的尖括号<…>中:
main() {
var p = Phone<String>('123456');
print(p.mobileNumber);
}
class Phone<T> {
final T mobileNumber;
Phone(this.mobileNumber);
}
实现泛型类型时,您可能希望限制其参数的类型,可以在<>
里面使用extends
。
main() {
var man = Man();
var m = User<Man>(man);
m.man.doMassage();
}
class User<T extends Man> {
final T man;
User(this.man);
}
class Man {
void doMassage() {}
}
与java一致, 唯一区别:Java泛型是编译时的,在运行时泛型信息会被擦除,Dart的泛型类型是固化的,在运行时也可以判断具体类型。
var names = List<String>();
print(names is List<String>);//true
print(names.runtimeType); // List<String>
库的使用
引用库
通过import语句在一个库中引入另一个库文件:
- 在import语句后面需要接上库文件的路径
- 对dart语言提供的库文件使用dart:xx格式
- 第三方的库文件使用package:xx格式
import 'dart:io';
import 'package:mylib/mylib.dart';
import 'package :utils/utils.dart' ;
指定一个库的前缀
当引用的库拥有相互冲突的名字,可以为其中一个或几个指定不一样的前缀。
import 'package:libl/ libl.dart ';
import 'package:lib2 / lib2.dart ' as lib2 ;
引用库的一部分
如果只需要使用库的一部分内容,可以有选择性地引用,有如下关键字:
- show 关键字: 只引用一点
- hide 关键字: 除此之外都引用
// 导入 foo
import 'package:libl/libl.dart' show foo;
// 除了 foo 导入其他所有内容
import 'package:lib2 / lib2.dart' hide foo;
库的延迟载入
- 使用
deferred as
导入 - 使用标识符调用
loadLibrary()
加载库
import 'dart:io' deferred as io;
lazyLoad() async {
//使用 await 关键字暂停代码执行一直到库加载完成。
await io.loadLibrary();
}
main() {
lazyLoad();
}
- 优点:
- 可提高程序启动速度。
- 用在不常使用的功能。
- 用在载入时间过长的包。
- 执行 A/B 测试,例如 尝试各种算法的 不同实现。
异步使用
Dart支持异步操作,一般使用async
函数和await
表达式实现异步操作,Dart库提供asynchronous
功能,该功能提供接口来消耗时间的操作,比如文件读写,网络请求。该功能返回Future
或Stream
对象。
可以通过如下的方式来获取asynchronous功能返回的Future对象值:
- 使用async函数和await表达式
- 使用Future功能提供的API
可以通过如下方式来获取asynchronous功能返回的Stream对象的值:
- 使用async和一个异步的循环(await for)
- 使用Stream的相关API
async / await
await关键字必须在async函数内部使用,await表达式可以使用多次
void main(){
getName1();
getName2();
getName3();
}
Future getName1() async {
await getStr1();
await getStr2();
print('getName1’);
}
getStr1() {
print('getStr1’);
}
getStr2() {
print('getStr2’);
}
getName2() {
print('getName2’);
}
getName3() {
print('getName3’);
}
/// 输出结果: getStr1 getName2 getName3 getStr2 getName1
then,catchError,whenComplete
如果需要监听“完毕”这个状态,那么用whenComplete
,需要监听“成功”这个状态,用then
,需要监听“失败”这个状态,用catchError
。
如果重写了test方法,test返回true就可以在catchError的onError方法里捕获到异常,如果test返回false,就把该异常继续抛出而不会在catchError方法里被捕获,如果不写test默认实现一个返回true的test方法
void main() {
Future(() => futureTask()) //异步任务的函数
.then((m) => "result:$m") //任务执行完后的子任务
.then((m) => m.length) //其中m为上个任务执行完后的返回的结果
.then((m) => printLength(m))
.catchError(print) // 拦截错误,如果实现了test方法,只有return true时才会拦截,否则不会
.whenComplete(() => whenTaskCompelete()); //所有任务完成后的回调函数
}
whenTaskCompelete() {
print('task complete');
}
futureTask() async {
return 'future';
}
printLength(dynamic name) {
if (name is! String || name.length < 3 || name.length > 15) {
throw '长度错误';
}
}
Event-Looper
- 一个消息循环的职责就是不断从消息队列中取出消息并处理他们直到消息队列为空。
- 消息队列中的消息可能来自用户输入,文件I/O消息,定时器等。例如上图的消息队列就包含了定时器消息和用户输入消息。
- Dart中的
Main Isolate
只有一个Event Looper
,但是存在两个Event Queue:Event Queue
以及Microtask Queue
。
Event Queue和Microtask Queue
- 优先全部执行完Microtask Queue中的Event。
- 直到Microtask Queue为空时,才会执行Event Queue中的Event。
- 当Event Looper正在处理Microtask Queue中的Event时候,Event Queue中的Event就停止了处理了,此时App不能绘制任何图形,不能处理任何鼠标点击,不能处理文件IO等等。
- 绘制图形,处理鼠标点击,处理文件IO等都是在Event Queue里完成的。
任务调度
- 使用Future类,可以将任务加入到Event Queue的队尾
- 使用scheduleMicrotask函数,将任务加入到Microtask Queue队尾
- 优先全部执行完Microtask Queue中的Event,直到Microtask Queue为空时,才会执行Event Queue中的Event。
Future
- 使用 Future()将任务加入event队列。
- Future中的
then
并没有创建新的Event丢到Event Queue
中,而只是一个普通的Function Call
,在FutureTask执行完后,立即开始执行。 - 如果在then()调用之前Future就已经执行完毕了,那么任务会被加入到
microtask
队列中,并且该任务会执行then()中注册的回调函数。 - 使用Future.value构造函数的时候,就会上一条一样,创建Task丢到microtask Queue中执行then传入的函数。
- Future.sync构造函数执行了它传入的函数之后,也会立即创建Task丢到microtask Queue中执行。
- 当任务需要延迟执行时,可以使用new Future.delay()来将任务延迟执行。
void main(){
testFuture();
}
void testFuture() {
Future f = new Future(() => print('f1'));
Future f1 = new Future(() => null);
//Future f1 = new Future.delayed(Duration(seconds: 1) ,() => null);
Future f2 = new Future(() => null);
Future f3 = new Future(() => null);
f3.then((_) => print('f2'));
f2.then((_) {
print('f3');
new Future(() => print('f4'));
f1.then((_) {
print('f5');
});
});
f1.then((m) {
print('f6');
});
print('f7');
}
scheduleMicrotask()
- 如果可以,尽量将任务放入event队列中。
- 使用Future的then方法或whenComplete方法来指定任务顺序。
- 为了保持你app的可响应性,尽量不要将大计算量的任务放入这两个队列。
- 大计算量的任务放入额外的isolate中。
import 'dart:async';
void main(){
testScheduleMicrotask();
}
void testScheduleMicrotask(){
scheduleMicrotask(() => print('s1'));
new Future.delayed(new Duration(seconds: 1), () => print('s2'));
new Future(() => print('s3')).then((_) {
print('s4');
scheduleMicrotask(() => print('s5'));
}).then((_) => print('s6'));
new Future(() => print('s7'));
scheduleMicrotask(() => print('s8'));
print('s9');
}
隔离(isolate)
所有Dart代码都在隔离区内运行,而不是线程。每个隔离区都有自己的内存堆,确保不会从任何其他隔离区访问隔离区的状态。
Dart没有共享内存的并发,没有竞争的可能性所以不需要锁,也就不用担心死锁的问题。
isolate之间没有共享内存,所以他们之间的通信唯一方式只能是通过Port进行,而且Dart中的消息传递总是异步的。
isolate神似Thread,但实际上两者有本质的区别。操作系统内的线程之间是可以有共享内存的而isolate没有,这是最为关键的区别。
元数据
使用元数据给代码添加更多的信息。元数据是以@开始的修饰符,在@后面接着编译时的常量或者一个常量构造函数。
- @deprecated 被弃用
- @override 重写
- @proxy 代理
元数据可以修饰 library、class、typedef、type parameter、constructor、factory、function、field、parameter、variable declaration。
注释
-
单行注释以//开头。Dart编译器会忽略//和行尾之间的所有内容。
// 这是单行注释
-
多行注释以/开头,以/结尾。介于/*和 */两者之间的内容会被编译器忽略(除非该注释是一个文档注释)。
/* * 这是多行注释 * 这是多行注释 */
多行注释可以嵌套
-
文档注释以///或者/**开头。可以通过dartdoc命令导出文档。
/// 这是文档注释 /** * 这是文档注释 */