学习Flutter的第一步是学习Dart语言,然后才是学习Flutter本身。如果你学习Kotlin Or Java ,那么学习起来很容易入门。因为你会看到很多你熟悉的语法。
仅做学习笔记,详细内容可品尝:
英文官网:https://dart.dev/guides/language/language-tour
中文:https://dartcn.com/guides/language/language-tour
英文6的可以直接阅读英文官网的学习资源。
一、入门须知
- Dart是一门强类型的语言。在Dart中定义一个变量,既可以直接指定变量的类型,也可以通过var声明一个变量,尽管Dart是强类型的,但是Dart可以推断类型,所以类型注释是可选的。当然,如果要明确说明不需要任何类型,需要使用特殊类型dynamic - 代表动态类型。
String name = "Tom";
var name = "Tom";
- Dart中一切皆对象。无论是数字,函数(Function类型)和null。所有对象均继承自Object类。
- Dart支持泛型,比如List<int>或者List<dynamic>(任何类型的对象列表)。
- Dart支持顶级函数、顶级变量。
- 与Java不同,Dart中没有关键字public、protected和private。如果想定义私有方法或者私有变量等,则以"_"开头即可。
二、变量
未初始化的变量默认值是null。即使变量是数字类型默认值也是null,因为Dart中一切皆对象,数字类型也不例外。
tip:开发环境可以使用assert()来断言一个变量是否是null,比如
int count = null
assert(count==null)
生产环境代码中assert()函数会被忽略,不会被调用。在开发过程中assert(condition)会在false条件下抛出异常。
final 与 const的变量声明:
final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';
const bar = 1000000; // 压力单位 (dynes/cm2)
const double atm = 1.01325 * bar; // 标准气压
三、内建类型
- Number
- Dart语言的Number类型有两种:int、double;注:从Dart2.1开始,必要的时候int字面量会自动转换成double类型【在 2.1 之前,在 double 上下文中使用 int 字面量是错误的】;
- String
- 通过单引号或者双引号创建;支持${expression}的方式内嵌表达式;
使用连续三个单引号或者三个双引号实现多行字符串对象的创建;
var s1 = '''
You can create
multi-line strings like this one.
''';
使用r前缀,可以创建“原始raw”字符串:
void main() {
var s = r"In a raw string, even \n isn't special.";
print(s);
}
输出:
In a raw string, even \n isn't special.
- Boolean
- List (也被称为 Array)
在Dart中的Array就是List对象;Dart中的List字面量非常像JS中的array字面量。示例:
var lits = [1,2,3]
Dart推断list的类型是List<int>.如果尝试将非整数对象添加到此List中,则分析器或者运行时将会引发错误
List的访问:list[0]
- Map
Map的创建:
var gifts = {
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings'
}
或者
var gifts = Map(); - Dart创建对象, new关键字是可选的
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';
Dart推断gifts的类型是Map<String, String>,尝试添加错误的类型会导致报错
- Set
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
Dart 推断halogens 类型为Set<String>,如果尝试为它添加一个错误类型,分析器或者运行时会报错。
要创建一个空集,使用前面带有类型参数的{},或者将{}赋值给Set类型的变量:
var names = <String>{};
或者
Set<String> name = {}
注意:var names = {}这样创建的是一个Map,而不是Set。
-----------------------------------------------------------
Map和Set的字面量语法非常相似。因为先有的Map字面量语法,所以{}默认是Map类型。如果忘记在{}上注释类型或者赋值到一个未声明类型的变量上,那么Dart会创建一个类型为Map<dynamic, dynamic>的对象
- Rune (用于在字符串中表示 Unicode 字符,例如'\u{1f44f}')
- Symbol - 表示 Dart 程序中声明的运算符或者标识符。
四、函数
Dart是一门真正面向对象的语言,甚至函数也是对象。这也意味着函数可以被赋值给变量或者作为参数传递(函数是一等对象)给其他函数。
如果函数只有一句表达式,则可以使用简写的语法:
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
=> expr 语法是 { return expr; } 的简写。 => 符号 有时也被称为 箭头 语法。
(在kotlin中,这样的函数被称为单表达式函数)
4.1、可选参数
可选参数可以是命名参数或者是位置参数。支持设置默认值。
命名可选参数(推荐)
定义:
void xxxx({Type1 name1,Type2 name2,......})
如果在Type2前面添加了@required,则name2是必须传入的
使用:
xxxx(name1:xx) 或 xxxx(name2:xx) 或 xxxx(name1:xx,name2:xx)
调用函数时,可以使用指定命名参数paramName:value。例如
enableFlags(bold:true);
注:Flutter创建实例的表达式可能很复杂, 因此Widget构造函数仅使用命名参数。 这样创建实例的表达式更易于阅读。
位置可选参数
将参数放到[]中来标记参数是可选的:
String say(String msg,[String extra])
如上,其中extra就是位置可选参数。
4.2、main函数
任何应用都必须有一个顶级main函数,作为应用服务的入口。main函数的返回值为空,参数为一个可选的List<String>。
void main() {
querySelector('#sample_text_id')
..text = 'Click me!'
..onClick.listen(reverseText);
}
//注:..是级联操作符,是一个语法糖,可以简化在一个对象上执行的多个操作。
上述函数体中的代码等价于:
var button = querySelector('#confirm');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));
4.3、匿名函数
创建没有名字的函数,这种函数被称为匿名函数,有时候也称为lambda或者closure(闭包)。
([[Type] param1[, …]]) {
codeBlock;
};
示例:
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});
等价于
list.forEach(
(item) => print('${list.indexOf(item)}: $item'));
4.4、词法闭包
一个函数对象,即使函数对象的调用在它原始作用域之外,依然可以访问在它词法作用域内的变量。
函数可以封闭定义到它作用域内的变量。
如下示例,无论在什么时候执行返回函数,函数都会使用捕获的addBy变量。
/// 返回一个函数,返回的函数参数与 [addBy] 相加。
Function makeAdder(num addBy) {
return (num i) => addBy + i;
}
void main() {
// 创建一个加 2 的函数。
var add2 = makeAdder(2);
// 创建一个加 4 的函数。
var add4 = makeAdder(4);
assert(add2(3) == 5);
assert(add4(3) == 7);
}
tip:所有的函数都会返回一个值。如果没有明确指定返回值,函数体会隐式的添加return null;语句。例如:
foo() {}
5、运算符
仅列出几个比较特殊的运算符,其余的运算符都比较常见,和Java或Kotlin的一样。
- .. - 级联运算符,4.2示例中有具体用法;
- ??
?? 是if null的意思
示例:
// 如果b为空时,将变量赋值给b,否则,b的值保持不变。
b ??= value;
-------------------------------
(1)expr1 ?? expr2
如果 expr1 是 non-null, 返回 expr1 的值; 否则, 执行并返回 expr2 的值。
(2)condition ? expr1 : expr2
==> 如果赋值根据布尔值,考虑使用2;如果赋值基于判定是否为null,考虑使用1
- ?. 与Koltin中的用法一毛一样
- as
(emp as Person).firstName = 'Tom';
如果emp是null,则会抛出异常
- is
if(emp is Person){
......
}
如果emp为null 或者 不是Person,则不执行
6、switch case
Dart中switch语句使用==比较整数、字符串、或者编译时常量。枚举类型也可以用于switch语句。
除break外,支持continue,throw或者return。
continue结合Label:
var command = 'CLOSED';
switch (command) {
case 'CLOSED':
executeClosed();
continue nowClosed;
// Continues executing at the nowClosed label.
nowClosed:
case 'NOW_CLOSED':
// Runs for both CLOSED and NOW_CLOSED.
executeNowClosed();
break;
}
7、异常
在Java和OC中,如果程序发生异常且没有被捕获,那么程序将会终止,但在Dart或JavaScript中则不会,究其原因,这和它们的运行机制有关系,Java和OC都是多线程模型的编程语言,任意一个线程触发异常且没被捕获时,整个进程就退出了。但Dart和JavaScript不同,它们都是单线程模型,运行机制很相似(但有区别),下面我们通过Dart官方提供的一张图来看看dart大致运行原理:
Dart中,try-catch-finally的具体用法示例:
try {
breedMoreLlamas();
} on OutOfLlamasException {
//指定的异常类型
buyMoreLlamas();
} on Exception catch (e) {
// 其他任何异常
print('Unknown exception: $e');
} catch (e) {
// 没有指定的类型,处理所有异常
print('Something really unknown: $e');
} finally{
print('......')
}
或者
try {
breedMoreLlamas();
} catch (e) {
print('Error: $e'); // Handle the exception first.
} finally {
cleanLlamaStalls(); // Then clean up.
}
和 Java 有所不同, Dart 中的所有异常是非检查异常。 方法不会声明它们抛出的异常, 也不要求捕获任何异常。Dart程序可以抛出任何非null对象,不仅限Exception和Error对象。示例:
throw FormatException('Expected at least 1 section');
也可以抛出任意的对象:
throw 'Out of llamas!';
提示:高质量的生产环境代码通常是实现Error或Exception类型的异常抛出
可以通过rethrow重新抛出异常。catch可以指定1到两个参数,第一个参数为抛出的异常对象,第二个为堆栈信息。
try {
// ···
} on Exception catch (e) {
print('Exception details:\n $e');
} catch (e, s) {
print('Exception details:\n $e');
print('Stack trace:\n $s');
rethrow;
}
8、类
Dart是一种基于类和mixin继承机制的面向对象的语言。每个对象都是一个类的实例,所有的类都继承于Object。基于*Mixin继承*意味着每个类(除了Object外)都只有一个超类,一个类中的代码可以在其他多个继承类中重复使用。
8.1、构造函数 - 概述
构造函数的名字可以是ClassName 或者 ClassName.identifier。后者被称为命名构造函数。
var p1 = Point(1,2);
var p2 = Point.fromJson({'x':1,'y':2});
var p3 = Point.fromMap(map);
var p4 = Point.origin();
以上均是通过构造函数创建一个Point实例。
------------------------------------------
class Point {
num x, y;
// 命名构造函数
Point.origin() {
x = 0;
y = 0;
}
Point(num x, num y) {
this.x = x;
this.y = y;
}
Dart为我们提供了语法糖,如下所示:
// 在构造函数体执行前,
// 语法糖已经设置了变量 x 和 y。
Point(this.x, this.y); //与上面的构造函数(Point(num x, num y))效果完全一致
}
常量构造函数是在构造函数名之前加const关键字。构造两个相同的编译时常量会产生一个唯一的,标准的实例:
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
assert(identical(a, b)); // 它们是同一个实例。
-------------------------------------------
在常量上下文中,构造函数或者字面量前的const可以省略。
// 这里有很多的 const 关键字。
const pointAndLine = const {
'point': const [const ImmutablePoint(0, 0)],
'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};
等价于
// 仅有一个 const ,由该 const 建立常量上下文。
const pointAndLine = {
'point': [ImmutablePoint(0, 0)],
'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};
如果常量构造函数在常量上下文之外,且省略了const关键字,此时创建的对象是非常量对象:
var a = const ImmutablePoint(1, 1); // 创建一个常量对象
var b = ImmutablePoint(1, 1); // 创建一个非常量对象
assert(!identical(a, b)); // 两者不是同一个实例!
子类不会继承父类的构造函数。 子类不声明构造函数,那么它就只有默认构造函数 (匿名,没有参数) 。
切记,构造函数不能够被继承, 这意味着父类的命名构造函数不会被子类继承。 如果希望使用父类中定义的命名构造函数创建子类, 就必须在子类中实现该构造函数。
8.2、对象类型
使用对象的runtimeType属性即可获取。
8.3、实例变量
class Point{
num x;
num y;
}
所有的实例变量都生成隐式的gettter方法。非final的实例变量同样会生成隐式setter方法。
8.4、初始化列表
默认情况下,无名构造函数的执行顺序如下所示:
- initializer list (初始化参数列表)
- superclass’s no-arg constructor (父类的无名构造函数)
- main class’s no-arg constructor (主类的无名构造函数)
如果父类中没有无名构造函数,则需要手动调用父类的其他构造函数。如下示例:
class Person {
String firstName;
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
// Person does not have a default constructor;
// you must call super.fromJson(data).
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}
main() {
var emp = new Employee.fromJson({});
// Prints:
// in Person
// in Employee
if (emp is Person) {
// Type check
emp.firstName = 'Bob';
}
(emp as Person).firstName = 'Bob';
}
由于父类的构造函数参数在构造函数之前执行,所以参数可以是一个表达式或一个方法调用。
class Employee extends Person {
Employee() : super.fromJson(getDefaultData());
// ···
}
除了调用超类构造函数之外,还可以在构造函数体执行之前初始化实例变量。各参数的初始化用逗号分隔。
import 'dart:math';
class Point {
final num x;
final num y;
final num distanceFromOrigin;
Point(x, y)
: x = x,
y = y,
distanceFromOrigin = sqrt(x * x + y * y);
}
main() {
var p = new Point(2, 3);
print(p.distanceFromOrigin);
}
---------------------------------
开发期间,可以使用assert验证输入的参数
Point.withAssert(this.x, this.y) : assert(x >= 0) {
print('In Point.withAssert(): ($x, $y)');
}
8.5、常量构造函数
如果该类生成的对象是固定不变的,那么就可以把这些对象定义为编译时常量。为此,需要定义一个const构造函数,并且声明所有的实例变量为final。
class ImmutablePoint {
static final ImmutablePoint origin =
const ImmutablePoint(0, 0);
final num x, y;
const ImmutablePoint(this.x, this.y);
}
注:常量构造函数创建的实例并不总是常量。详见8.1节最后部分。
8.6、工厂构造函数
在构造函数前添加factory关键字即为工厂构造函数。
当执行构造函数并不总是创建这个类的一个新实例时,则可以使用工厂构造函数。
8.7、Getter和Setter
Getter 和 Setter 是用于对象属性读和写的特殊方法。 回想之前的例子,每个实例变量都有一个隐式 Getter ,通常情况下还会有一个 Setter 。 使用 get
和 set
关键字实现 Getter 和 Setter ,能够为实例创建额外的属性。
class Rectangle {
num left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
// 定义两个计算属性: right 和 bottom。
num get right => left + width;
set right(num value) => left = value - width;
num get bottom => top + height;
set bottom(num value) => top = value - height;
}
void main() {
var rect = Rectangle(3, 4, 20, 15);
assert(rect.left == 3);
rect.right = 12;
assert(rect.left == -8);
}
最开始实现 Getter 和 Setter 也许是直接返回成员变量; 随着需求变化, Getter 和 Setter 可能需要进行计算处理而使用方法来实现; 但是,调用对象的代码不需要做任何的修改。
8.8、隐式接口
每个类都隐式的定义了一个接口,接口包含了该类所有的实例成员及其实现的接口。
如果要创建一个类A,A要支持B类的API,但是不需要继承B的实现,那么可以通过A实现B的隐式接口。
Dart中实现接口用implements,继承一个类用extends。和Java的用法一毛一样。
// person 类。 隐式接口里面包含了 greet() 方法声明。
class Person {
// 包含在接口里,但只在当前库中可见。
final _name;
// 不包含在接口里,因为这是一个构造函数。
Person(this._name);
// 包含在接口里。
String greet(String who) => 'Hello, $who. I am $_name.';
}
// person 接口的实现。
class Impostor implements Person {
get _name => '';
String greet(String who) => 'Hi $who. Do you know who I am?';
}
String greetBob(Person person) => person.greet('Bob');
void main() {
print(greetBob(Person('Kathy')));
print(greetBob(Impostor()));
}
8.9、为类添加功能:Mixin
Mixin是复用类代码的一种途径,复用的类可以在不同层级,之间可以不存在继承关系。
通过with后面跟一个或者多个混入的名称,来使用Mixin。
class Musician extends Performer with Musical {
// ···
}
class Maestro extends Person
with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
通过创建一个继承自Object且没有构造函数的类,来实现一个Mixin。如果Mixin不希望作为常规类被使用,使用关键字mixin替换class。示例:
mixin Musical {
bool canPlayPiano = false;
bool canCompose = false;
bool canConduct = false;
void entertainMe() {
if (canPlayPiano) {
print('Playing piano');
} else if (canConduct) {
print('Waving hands');
} else {
print('Humming to self');
}
}
}
指定只有某些类型可以使用的Mixin - 比如, Mixin 可以调用 Mixin 自身没有定义的方法 - 使用 on
来指定可以使用 Mixin 的父类类型:
mixin MusicalPerformer on Musician {
// ···
}
9、泛型
Dart 中泛型类型是 固化的,也就是说它们在运行时是携带着类型信息的。
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true
相反,Java中的泛型会被 擦除 ,也就是说在运行时泛型类型参数的信息是不存在的。 在Java中,可以测试对象是否为 List 类型, 但无法测试它是否为 List<String>
。
除此之外,泛型的用法与Java一样。
限制泛型类型使用extends:
class Foo<T extends SomeBaseClass> {
// Implementation goes here...
String toString() => "Instance of 'Foo<$T>'";
}
可以使用 SomeBaseClass 或其任意子类作为通用参数,指定任何非 SomeBaseClass 类型会导致错误。
10、异步支持
Dart 库中包含许多返回 Future 或 Stream 对象的函数. 这些函数在设置完耗时任务(例如 I/O 操作)后, 就立即返回了,不会等待耗任务完成。 使用 async
和 await
关键字实现异步编程。 可以让你像编写同步代码一样实现异步操作。
10.1、处理Future
获得Future执行完成的结果:
- 使用async和await
Future checkVersion() async {
var version = await lookUpVersion();
// Do something with version
}
使用await和async关键字的代码是异步的。虽然看起来有点像同步代码。
要使用await,代码必须在异步函数(使用async标记的函数)。
Future<String> lookUpVersion() async =>'1.0.0';
注意:函数体不需要使用Future Api。如有必要,Dart会自动创建Future对象的。如果函数没有返回值,需要设置返回类型为Future<void>
虽然异步函数可能会执行耗时的操作, 但它不会等待这些操作。 相反,异步函数只有在遇到第一个 await 表达式时才会执行。 也就是说,它返回一个 Future 对象, 仅在await表达式完成后才恢复执行。
使用try,catch,和finally来处理代码中使用await导致的错误。
try {
version = await lookUpVersion();
} catch (e) {
// React to inability to look up the version
}
一个异步函数中可以多次使用await。
var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);
在await表达式中,表达式的值通常是一个Future对象。如果不是,这时表达式的值会自动包装成一个Future对象。
Future对象指明返回一个对象的承诺。await表达式执行的结果为这个返回的对象。await表达式会阻塞代码的执行,直到需要的对象返回为止。
- 使用Future Api
future.then().error().catch()。。。。。
10.2、处理Stream
当需要从Stream中获取数据值时,可以通过以下两种方式:
- 使用async和一个异步循环(await for);
- 使用Stream API
注意:在使用 await for
前,确保代码清晰, 并且确实希望等待所有流的结果。 例如,通常不应该使用 await for
的UI事件侦听器, 因为UI框架会发送无穷无尽的事件流。
11、Isolates
大多数计算机中,甚至在移动平台上,都在使用多核CPU。 为了有效利用多核性能,开发者一般使用共享内存数据来保证多线程的正确执行。 然而, 多线程共享数据通常会导致很多潜在的问题,并导致代码运行出错。
所有 Dart 代码都在隔离区( isolates )内运行,而不是线程。 每个隔离区都有自己的内存堆,确保每个隔离区的状态都不会被其他隔离区访问。
12、Typedefs
在 Dart 中,函数也是对象,就想字符和数字对象一样。 使用 typedef ,或者 function-type alias 为函数起一个别名, 别名可以用来声明字段及返回值类型。 当函数类型分配给变量时,typedef会保留类型信息。
class SortedCollection {
Function compare;
SortedCollection(int f(Object a, Object b)) {
compare = f;
}
}
// Initial, broken implementation. // broken ?
int sort(Object a, Object b) => 0;
void main() {
SortedCollection coll = SortedCollection(sort);
// 虽然知道 compare 是函数,
// 但是函数是什么类型 ?
assert(coll.compare is Function);
}
当把 f
赋值给 compare
的时候,类型信息丢失了。 f
的类型是 (Object,
Object)
→ int
(这里 → 代表返回值类型), 但是 compare
得到的类型是 Function 。如果我们使用显式的名字并保留类型信息, 这样开发者和工具都可以使用这些信息:
typedef Compare = int Function(Object a, Object b);
class SortedCollection {
Compare compare;
SortedCollection(this.compare);
}
// Initial, broken implementation.
int sort(Object a, Object b) => 0;
void main() {
SortedCollection coll = SortedCollection(sort);
assert(coll.compare is Function);
assert(coll.compare is Compare);
}
提示: 目前,typedefs 只能使用在函数类型上.
由于 typedefs 只是别名, 他们还提供了一种方式来判断任意函数的类型。例如:
typedef Compare<T> = int Function(T a, T b);
int sort(int a, int b) => a - b;
void main() {
assert(sort is Compare<int>); // True!
}