观书有会意处,题其衣裳,以记其事~
总体介绍
1.1 Flutter框架结构
无论学什么技术,都要现有一张清晰的“地图”,而我们的学习过程就是“按图索骥”,这样我们才不会陷于细节而“目无全牛”。言归正传,我们看一下Flutter官方提供的Flutter框架图,如图1-1所示:
Flutter Framework
这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上,我们来简单介绍一下:
- 底下两层(Foundation和Animation、Painting、Gestures)在Google的一些视频中被合并为一个dart UI层,对应的是Flutter中的
dart:ui
包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。 - Rendering层,这一层是一个抽象的布局层,它依赖于dart UI层,Rendering层会构建一个UI树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM。Rendering层可以说是Flutter UI框架最核心的部分,它除了确定每个UI元素的位置、大小之外还要进行坐标变换、绘制(调用底层dart:ui)。
- Widgets层是Flutter提供的的一套基础组件库,在基础组件库之上,Flutter还提供了 Material 和Cupertino两种视觉风格的组件库。而我们Flutter开发的大多数场景,只是和这两层打交道。
Flutter Engine
这是一个纯 C++实现的 SDK,其中包括了 Skia引擎、Dart运行时、文字排版引擎等。在代码调用 dart:ui
库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。
1.2 AOT & JIT
程序主要有两种运行方式:静态编译与动态解释。静态编译的程序在执行前全部被翻译为机器码,通常将这种类型称为AOT (Ahead of time)即 “提前编译”;而解释执行的则是一句一句边翻译边运行,通常将这种类型称为JIT(Just-in-time)即“即时编译”。AOT程序的典型代表是用C/C++开发的应用,它们必须在执行前编译成机器码,而JIT的代表则非常多,如JavaScript、python等,事实上,所有脚本语言都支持JIT模式。但需要注意的是JIT和AOT指的是程序运行方式,和编程语言并非强关联的,有些语言既可以以JIT方式运行也可以以AOT方式运行,如Java、Python,它们可以在第一次执行时编译成中间字节码、然后在之后执行时可以直接执行字节码,也许有人会说,中间字节码并非机器码,在程序执行时仍然需要动态将字节码转为机器码,是的,这没有错,不过通常我们区分是否为AOT的标准就是看代码在执行之前是否需要编译,只要需要编译,无论其编译产物是字节码还是机器码,都属于AOT。在此,读者不必纠结于概念,概念就是为了传达精神而发明的,只要读者能够理解其原理即可,得其神忘其形。
Dart
1.1 内建类型
Dart 语言支持以下内建类型:
- Number(包含int、double)
- String
- Boolean
- List (也被称为 Array)
- Map
- Set
- Rune (用于在字符串中表示 Unicode 字符)
- Symbol
这些类型都可以被初始化为字面量。 例如, 'this is a string'
是一个字符串的字面量, true
是一个布尔的字面量。
因为在 Dart 所有的变量终究是一个对象(一个类的实例), 所以变量可以使用 构造涵数 进行初始化。 一些内建类型拥有自己的构造函数。 例如, 通过 Map()
来构造一个 map 变量。
1.2 函数
1.2.1 可选参数
- 命名可选参数
- 位置可选参数
- 默认参数
1.2.2 函数是一等对象
一个函数可以作为另一个函数的参数。 例如:
1 | void printElement(int element) { |
同样可以将一个函数赋值给一个变量,例如:
1 | var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!'; |
1.2.3 匿名函数
1 | ([[*Type*] *param1*[, …]]) { *codeBlock*;}; |
下面例子中定义了一个包含一个无类型参数 item
的匿名函数。 list 中的每个元素都会调用这个函数,打印元素位置和值的字符串。
1 | var list = ['apples', 'bananas', 'oranges']; |
如果函数只有一条语句, 可以使用箭头简写。1
2list.forEach(
(item) => print('${list.indexOf(item)}: $item'));
1.2.4 返回值
所有函数都会返回一个值。 如果没有明确指定返回值, 函数体会被隐式的添加 return null;
语句。
1.3、运算符
1.3.1 赋值运算符
使用 =
为变量赋值。 使用 ??=
运算符时,只有当被赋值的变量为 null 时才会赋值给它。
1 | // 将值赋值给变量a |
复合赋值运算符:
Compound assignment Equivalent expression For an operator *op*: a *op*= b
a = a *op* b
Example: a += b
a = a + b
1.3.2 条件表达式
Dart有两个运算符,有时可以替换 if-else 表达式, 让表达式更简洁:
*condition* ? *expr1* : *expr2*
如果条件为 true, 执行 expr1 (并返回它的值): 否则, 执行并返回 expr2 的值。
*expr1* ?? *expr2*
如果 expr1 是 non-null, 返回 expr1 的值; 否则, 执行并返回 expr2 的值。
如果赋值是根据布尔值, 考虑使用 ?:
。
1 | var visibility = isPublic ? 'public' : 'private'; |
如果赋值是基于判定是否为 null, 考虑使用 ??
。
1 | String playerName(String name) => name ?? 'Guest'; |
1.3.3 级联运算符 (..)
级联运算符 (..
) 可以实现对同一个对像进行一系列的操作。 除了调用函数, 还可以访问同一对象上的字段属性。 这通常可以节省创建临时变量的步骤, 同时编写出更流畅的代码。
考虑一下代码:
1 | querySelector('#confirm') // 获取对象。 |
第一句调用函数 querySelector()
, 返回获取到的对象。 获取的对象依次执行级联运算符后面的代码, 代码执行后的返回值会被忽略。
上面的代码等价于:
1 | var button = querySelector('#confirm'); |
级联运算符可以嵌套,例如:
1 | final addressBook = (AddressBookBuilder() |
在返回对象的函数中谨慎使用级联操作符。 例如,下面的代码是错误的:
1 | var sb = StringBuffer(); |
sb.write()
函数调用返回 void, 不能在 void
对象上创建级联操作。
1.4 控制流程语句
1.4.1 for 循环
如果要迭代一个实现了 Iterable 接口的对象, 可以使用 forEach() 方法, 如果不需要使用当前计数值, 使用 forEach()
是非常棒的选择;
1 | candidates.forEach((candidate) => candidate.interview()); |
实现了 Iterable 的类(比如, List 和 Set)同样也支持使用 for-in
进行迭代操作 iteration :
1 | var collection = [0, 1, 2]; |
1.5 类
1.5.1 获取对象的类型
使用对象的 runtimeType
属性, 可以在运行时获取对象的类型, runtimeType
属性回返回一个 Type 对象。
1 | print('The type of a is ${a.runtimeType}'); |
1.5.2 构造函数
1.5.2.1 构造函数语法糖
1 | class Point { |
1 | class Point { |
1.5.2.2 命名构造函数
使用命名构造函数可为一个类实现多个构造函数, 也可以使用命名构造函数来更清晰的表明函数意图:
1 | class Point { |
1.5.2.3 重定向构造函数
1 | class Rect{ |
1.5.2.4 常量构造函数
如果该类生成的对象是固定不变的, 那么就可以把这些对象定义为编译时常量。 为此,需要定义一个 const
构造函数, 并且声明所有实例变量为 final
。
1 | class ImmutablePoint { |
1.5.2.5 工厂构造函数
当执行构造函数并不总是创建这个类的一个新实例时,则使用 factory
关键字。 例如,一个工厂构造函数可能会返回一个 cache 中的实例, 或者可能返回一个子类的实例。
以下示例演示了从缓存中返回对象的工厂构造函数:
1 | class Logger { |
1.5.2.6 noSuchMethod()
当代码尝试使用不存在的方法或实例变量时, 通过重写 noSuchMethod()
方法,来实现检测和应对处理:
1 | class A { |
1.6 泛型
1.6.1 为什么使用泛型
1.6.1.1 正确指定泛型类型可以提高代码质量
如果想让 List 仅仅支持字符串类型, 可以将其声明为 List
(读作“字符串类型的 list ”)。 那么,当一个非字符串被赋值给了这个 list 时,开发工具就能够检测到这样的做法可能存在错误。 例如:
1 | var names = List<String>(); |
1.6.1.2 使用泛型可以减少重复的代码
1 | abstract class ObjectCache { |
1 | abstract class StringCache { |
1 | abstract class Cache<T> { |
1.6.2 限制泛型类型
使用泛型类型的时候, 可以使用 extends
实现参数类型的限制。
1 | class Foo<T extends SomeBaseClass> { |
1.7 库和可见性
1.7.1 使用库
import
参数只需要一个指向库的 URI。 对于内置库,URI 拥有自己特殊的dart:
方案。 对于其他的库,使用系统文件路径或者 package:
方案
1.7.2 指定库前缀
如果导入两个存在冲突标识符的库, 则可以为这两个库,或者其中一个指定前缀。 例如,如果 library1 和 library2 都有一个 Element 类, 那么可以通过下面的方式处理:
1 | import 'package:lib1/lib1.dart'; |
1.7.3 导入库的一部分
如果你只使用库的一部分功能,则可以选择需要导入的 内容。例如:
1 | // Import only foo. |
延迟加载库
Deferred loading (也称之为 lazy loading) 可以让应用在需要的时候再加载库。 下面是一些使用延迟加载库的场景:
- 减少 APP 的启动时间。
- 执行 A/B 测试,例如 尝试各种算法的 不同实现。
- 加载很少使用的功能,例如可选的屏幕和对话框。
要延迟加载一个库,需要先使用 deferred as
来导入:
1 | import 'package:greetings/hello.dart' deferred as hello; |
当需要使用的时候,使用库标识符调用 loadLibrary()
函数来加载库:
1 | Future greet() async { |
在前面的代码,使用 await
关键字暂停代码执行一直到库加载完成。 关于 async
和 await
的更多信息请参考 异步支持。
在一个库上你可以多次调用 loadLibrary()
函数。但是该库只是载入一次。
使用延迟加载库的时候,请注意一下问题:
- 延迟加载库的常量在导入的时候是不可用的。 只有当库加载完毕的时候,库中常量才可以使用。
- 在导入文件的时候无法使用延迟库中的类型。 如果你需要使用类型,则考虑把接口类型移动到另外一个库中, 让两个库都分别导入这个接口库。
- Dart 隐含的把
loadLibrary()
函数导入到使用deferred as *的命名空间*
中。loadLibrary()
方法返回一个 Future。
1.8 异步支持
1.8.1 Future
await
关键字必须在async
函数内部使用- 调用
async
函数必须使用await
关键字
1 | import 'dart:async'; |