2021年3月4日凌晨,Flutter 2 正式发布,除了增强了对于多平台的适配(包括:web、windows、Linux)外,很重要的一点就是Flutter 2.0中使用的编程语言Dart的版本更新为 2.12,并支持健全的空安全。我们APP在1.0.4版本就升级了Flutter 2.0,并完成了对空安全的适配,本文在这里希望和大家分享下空安全适配的实践过程和踩坑,欢迎一起交流探讨。
理解空安全
空安全(Sound null safety)特性并不是 Dart 独有的,Kotlin, Rust, C#, Swift 等语言都有此特性。空安全默认代码中的所有的类型都是非空的,并且使用了特定的静态检查和编译优化,使得在运行时,可以保证不会发生空指针引用和解引用(null-dereference)错误,因为这会在编译时就会发现并解决,增加了代码的健壮性。
让我们来看下面这个例子:
// Without null safety:
bool isEmpty(String string) => string.length == 0;
main() {
isEmpty(null);
}
如果你的Dart 程序时并未使用空安全,它将在调用 .length 时抛出 NoSuchMethodError 异常。 如果可以让静态类型检查器,使得诸如在可能为 null 的值上调用 .length 这样的错误能提前能被检测到,就可以提升APP的稳定性和用户的体验。
值得注意的是,我们的目标并不是 消除 null
。相反,可以表示一个 空缺 的值是十分有用的。 null
并不糟糕,糟糕的是 它在你意想不到的地方出现,最终引发问题。
因此,对于空安全而言,我们的目标是让你对代码中的 null
可见且可控,并且确保它不会传递至某些位置从而引发崩溃。
非空和可空类型
在空安全推出之前,静态类型系统允许所有类型的表达式中的每一处都可以有 null。从类型理论的角度来说,Null 类型被看作是所有类型的子类
但是 null 值并没有它们定义的任何一个方法和属性。所以当 null 传递至其他类型的表达式时,任何操作都有可能失败。这就是空引用的症结所在——所有错误都来源于尝试在 null 上查找一个不存在的方法或属性。
空安全通过修改了类型的层级结构,从根源上解决了这个问题。 Null
类型仍然存在,但它不再是所有类型的子类。
这样,所有的类都分成两部分,一部分为可空类型,一种就是非可空类型,Null仍然是任何可空类型的子类。
使用可空类型
我们将所有的可空类型作为基础类型的超类。你也可以将 null 传递给一个可空的类型,即 Null 也是任何可空类型的子类:
Flutter APP对空安全的适配步骤
官方文档中对于APP的迁移过程有如下的步骤建议:
- 等待 你依赖的 package 迁移完成。
- 迁移 你的 package 的代码,最好使用交互式的迁移工具。
- 静态分析 package 的代码。
- 测试 你的代码,确保可用。
- 如果你已经在 pub.flutter-io.cn 发布了你的 package,可以将迁移完成的空安全版本以 预发布 版本进行 发布。
但是有时候并非你使用的所有的package都能及时完成迁移,而且你迁移的越晚,需要的成本越大。而且很多package的新版都必须要求你的工程中使用空安全,因此,我们就必须采用一种方式,来实现非健全的空安全。然后随着所有的package完成迁移,你就可以从非健全的空安全转换为健全的空安全。
- 非健全的空安全
// @dart=2.9 是Dart语言的一个语法糖,将它放到任何一个Dart文件的第一行就代表着,这个Dart文件指定 Dart 2.9 的语言版本进行静态分析,可以减少未迁移的分析错误,保证你的代码可以编译通过。
首先,编辑 package 的 pubspec.yaml 文件,将最低 SDK 版本设置到 2.12.0:
environment:
sdk: '>=2.12.0 <3.0.0'
其次,新建一个app_main.dart文件,将你原来在main.dart文件中的代码转移到这个文件,并修改代码中的main方法,名称为app_main.
最后,在原来的main.dart文件中,加入如下代码:
// @dart=2.9
import 'app_main.dart';
void main(){
appmain();
}
这就保证了你的代码可以在部分支持空安全的情况下仍然可以顺利的运行。
2. 迁移
针对迁移,你有两个选项可以选择:
· 使用迁移工具,它可以帮你处理大多数可推导的变更。
使用迁移工具
迁移工具会带上一个非空安全的 package ,将它转换至空安全。你可以先在代码中添加 提示标记 来引导迁移工具的转换。
开始转换前,请做好如下的准备:
· 使用 Dart SDK 的最新版本。
· 运行 dart pub outdated --mode=null-safety
以确保所有依赖为最新且空安全。
在包含 pubspec.yaml
的目录下,执行 dart migrate
命令,启动迁移工具。
$ dart migrate
如果你的 package 可以进行迁移,工具会输出类似以下的内容:
View the migration suggestions by visiting:
http://127.0.0.1:60278/Users/you/project/mypkg.console-simple?authToken=Xfz0jvpyeMI%3D
使用 Chrome 浏览器访问 URL,你可以看到一个交互式的界面,引导你进行迁移:
你可以在工具中看到其推断的所有变量和类型注解。例如,在上面的截图中,工具推断第一行的 ints
列表元素可能为空,所以应该变为 int?
(先前为 int
)。
手动迁移
如果你不想使用迁移工具,你也可以手动进行迁移。
我们推荐你 优先迁移最下层的库 —— 指的是没有导入其他 package 的库。接着迁移直接依赖了下层库的依赖库。最后再迁移依赖项最多的库。
举个例子,假设你的 lib/src/util.dart
导入了其他(空安全)的 package 和核心库,但它没有包含任何 import <本地路径>
的引用。那么你应当优先考虑迁移 util.dart
,然后迁移依赖了 util.dart
的文件。如果有一些循环引用的库(例如 A 引用了 B,B 引用了 C,C 引用了 A),建议同时对它们进行迁移。
手动对 package 进行迁移时,请参考以下步骤:
- 编辑 package 的
pubspec.yaml
文件,将最低 SDK 版本设置到2.12.0
:
environment:
sdk: '>=2.12.0 <3.0.0'
2.重新生成 package 的配置文件:
$ dart pub get
在版本最低是 2.12.0-0
的 SDK 上运行 dart pub get
时,会将每个 package 的默认 SDK 版本设定为 2.12,并且默认它们已经迁移至空安全。
在你的 IDE 上打开package 。 你也许会看到很多错误,没关系,让我们继续。
利用分析器来辨析静态错误,逐个迁移 Dart 文件。按需添加
?
、!
、required
以及late
来消除静态错误。迁移后的代码调整
自动迁移虽然可以批量帮助你做迁移,但是其中很多自动调整的代码是需要你自己根据业务来进行调整的。
- 类中未初始化的属性被默认设置为可空类型
但是有时候,我们可能会在后面的代码中,进行赋值,并且为非空,这时候就应该将该属性声明为late。
late 关键字主要用于延迟初始化。初始化主要分为两种:声明处默认值初始化和延迟初始化。但是并不是所有场景都合适使用声明处默认值初始化,这时候就需要使用late,但是当使用late变量时,一定要确保在使用之前要初始化,否则抛出异常。
- 删除对数据的空判断
以前很多代码的空判断可能就需要删除了,因为很多类型设置为非空,就不需要判断。一些可能为空的判断就需要随着修改。
比如:对于bool? login类型的代码,我们以前可能需要设置为
if(login),那就需要修改为,if(login == true)
有时候某些 await 语法会被强行增加 as FutureOr ,如果你不需要改为原来的声明就可以。
有时候一些方法定义也会被强行修改,比如 redux 相关的这些修改可能也会影响运行问题,所以只需要把 as 部分去除就可以了。
而比如这类方法报错,一般就是提供的参数和使用参数对应不上,只需要添加上 ? 即可修复
完全的空安全
当完成你的工程代码的空安全的迁移以后,就需要查看哪些库没有适配空安全。
dart pub outdated
会列出所有的未支持的库,这时候你有三种选择:
等待代码库的开发发布新版本
如果有可以替代的库,替换已经适配空安全的库
如果是开源库,clone库,自己进行空安全的适配
当所有的库,都修改完之后,就可以将我们的app.dart文件复原为原来的文件,并删除app_main.dart文件。就完成了整个的迁移过程。
-
总结
使用Flutter 2.0并不意味着必须适配空安全。空安全是Dart 2.12增加的新功能,你可以使用Flutter 2.0,但是使用Dart 2.9的语法规则和特性。但是由于Flutter的开发很依赖于第三方库,而第三方库在适配空安全之后,就不再维护非空安全的版本,因此尽早的迁移到空安全,除了提升APP的稳定性有好多,也有利于使用新的第三方库进行开发,包括第三方库的bug修复和新功能的支持。