您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
京东快递APP对Flutter 2.0空安全的适配
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
京东快递APP对Flutter 2.0空安全的适配
自猿其说Tech
2021-10-18
IP归属:未知
57080浏览
Flutter
计算机编程
2021年3月4日凌晨,Flutter 2.0 正式对外发布,除了增强了对于多平台的适配(包括:web、windows、Linux、macOS)外,其中很重要的一点变化就是Flutter 2.0中使用的编程语言Dart的版本更新为 2.12,并支持健全的空安全。京东快递APP在1.0.4版本就采用了Flutter 2.0,并且整个APP完成了对空安全的适配。本文在这里希望和大家分享下空安全适配的实践过程和踩坑,欢迎一起交流探讨。 ### 1 理解空安全 空安全(Sound null safety)并不是 Dart 独有的,Kotlin, Rust, C#, Swift 等语言都支持此特性。空安全特性默认代码中的所有类型都是非空的,并且使用了特定的静态检查和编译优化,使得在编译时,就能提前发现空指针引用和解引用(null-dereference)错误并进行解决,从而保证在运行时不出出现空指针异常,从而增加了代码的健壮性和APP的稳定性。 让我们来看下面这个未使用空安全的Dart程序的例子: ```javascript // 未使用空安全: bool isEmpty(String string) => string.length == 0; main() { isEmpty(null); } ``` 因为上面的例子没有使用空安全,因此在运行时,string在调用 length 方法时会抛出 NoSuchMethodError 异常。 不过,如果可以让静态类型检查器,对于在 null 的值上调用 length 这样的错误在变异时,能提前被检测到,而不是在运行的时候才能发现,增加代码的健壮性和APP的稳定性。而空安全所做的工作就是如此。 空安全的目标并不是消除 null。相反,null可以表示一个空缺的值,并且是十分有用的。 null 并不糟糕,糟糕的是它在你意想不到的地方出现,并最终引发问题。 因此,对于空安全而言,我们的目标是让你对代码中的 null 可见且可控,并且确保它不会传递至某些位置从而引发崩溃。 #### 1.1 非空和可空类型 在空安全推出之前,静态类型系统允许所有类型的表达式中的每一处都可以有 null。从类型理论的角度来说,Null 类型可以被看作是所有类型的子类 但是 null 值并没有它们定义很多方法和属性供开发者使用,所以当 null 传递至其他类型的表达式时,对它的任何操作都有可能失败。这就是空引用的症结所在——所有错误都来源于尝试在 null 上查找一个不存在的方法或属性。 空安全通过修改了类型的层级结构,从根源上解决了这个问题。 Null 类型仍然存在,但它不再是所有类型的子类。 ![](//img1.jcloudcs.com/developer.jdcloud.com/c178fb18-3d6d-4ee9-8219-a6fe80dea70420211018141439.png) 引入空安全后,所有的类被分成两部分,一部分为可空类型,一种就是非可空类型。我们将所有的可空类型作为它对应的非可空基础类型的超类。你也可以将 null 传递给一个可空的类型,即现在 Null 从原来的可以是任何类型的子类,变成为只是任何可空类型的子类。 ![](//img1.jcloudcs.com/developer.jdcloud.com/6a7248a0-5b1f-4bc0-a606-7529deffbd2420211018141457.png) 进行了这些改造以后,非空类型区域中的类型能访问到你想要的该类的所有方法,但不能包含 null。对应的平行的可空类型家族,它们允许出现 null,但他们只能访问同时在原有非可空类型及 Null 类型下同时定义的方法和属性,也就是说对于可空类型来说只有 toString()、== 和 hashCode 是可以访问。因此,可空类型在转换到非空类型前基本没有实际作用。但是让值从可空的一侧走向非空的一侧是非安全的,但反之则是安全的。 我们可以通过!语法将可空类型转换为非空类型,同时静态检查工具也能帮我们自动的将可空类型进行类型提升,使得我们不需要手动的来通过!来进行类型转换。如果你判断了一个可空的变量是否不为 null,进行到下一步后 Dart 就会将这个变量的类型提升至非空的对应类型: ```javascript // Using null safety: String makeCommand(String executable, [List<String>? arguments]) { var result = executable; if (arguments != null) { result += ' ' + arguments.join(' '); } return result; } ``` 但是,在使用!进行类型转换的时候要特别注意,除非确认当前的可空类型的值,不可能为空,否则使用!同样会出现空指针的引用的错误。空安全并不是在你每次出海前给你一件救生衣,提醒你记得穿戴。相反,它提供给我们一艘不会沉的小船,只要你不跳下水里(在不恰当的时候使用!),就无事发生。 ### 2 Flutter APP对空安全的适配步骤 #### 2.1 健全的空安全 官方文档中对于APP的迁移到空安全的过程有如下的步骤建议: 1. 等待你依赖的 package 迁移完成。 1. 迁移你的 package 的代码,最好使用交互式的迁移工具。 1. 静态分析 package 的代码。 1. 测试你的代码,确保可用。 1. 如果你已经在 pub.flutter-io.cn 发布了你的 package,可以将迁移完成的空安全版本以预发布版本进行发布。 但是有时候并非你使用的所有的package都能及时完成迁移,并且随着开发的不断进行,你迁移的越晚,所需要的成本越大。而且很多package的新版都必须要求你的工程中使用空安全,因此,我们就必须采用非健全的空安全来作为过渡,先实现部分代码或者package的迁移,然后随着所有的package和模块完成迁移,你就可以从非健全的空安全转换为健全的空安全。 因此,在京东快递APP中,我们实际采用的步骤是: 1. 将项目改造为支持非健全的空安全。 1. 迁移工程中你想要迁移的部分代码,最好使用交互式的迁移工具。 1. 静态分析 package 的代码。 1. 测试你的代码,确保可用。 1. 等待你依赖的 package 迁移完成,更新package依赖,并将项目改回健全的空安全。 这样改造的好处就是,在改造的过程中,工程一直是可以编译通过,你可以采用渐进的方式来迁移代码,包括只迁移部分package或者部分代码文件。不过,因为是非健全的空安全,实际上,空安全的很多特性(包括安全性)只能在完成迁移的那部分代码中可以使用,这也是它的名字中的“非健全”的含义。 #### 2.2 非健全的空安全 一个 Flutter应用可以同时包含已经迁移至空安全和未迁移至空安全的库,同时也可以部分代码使用空安全,部分代码不使用空安全。这些混合模式的程序会运行在非健全的空安全下。 非健全的空安全是通过使用 "// @dart=2.9" 这样的一个Dart语言的语法糖来实现的。开发者可以将它放到任何一个Dart文件的第一行,这就代表着这个Dart文件指定使用 Dart 2.9 的语言版本(不支持空安全)进行静态分析,也就是提前声明该文件不支持空安全,保证你的代码在非健全的空安全模式下可以编译通过。 我们将Flutter应用的入口文件main.dart中加入"// @dart=2.9"就可以支持整个Flutter应用的非健全的空安全。 首先,编辑 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(); } ``` 经过以上改造,我们就可以在我们工程中的部分代码使用空安全,部分代码不使用空安全,同时也可以的工程中引用支持空安全的package也同时可以引用一些暂不支持空安全的库。这对于我们分模块逐步迁移工程来说很重要。 ### 3 代码迁移 经过了对代码的非健全空安全的支持,我们需要对于工程中的代码进行迁移。 针对迁移,你有两个选项可以选择: - 使用迁移工具,它可以帮你处理大多数可推导的变更。 - 完全自己动手进行迁移。 #### 3.1 使用迁移工具 迁移工具会将一个非空安全的 package 转换至空安全。你可以先在代码中添加提示标记来引导迁移工具的转换。 开始转换前,你需要运行 flutter pub outdated --mode=null-safety 以查看所有依赖库是否为最新且支持空安全的情况,并将相应的依赖库转换为空安全的版本。 ![](//img1.jcloudcs.com/developer.jdcloud.com/7d2f3220-bbd2-4630-8df7-9d3deb6f1e6420211018141755.png) 其次,在工程中包含 pubspec.yaml 的目录下,执行 dart migrate 命令,启动迁移工具。但是当你所依赖的库中有一些还没完成空安全的迁移的话,需要加入--skip-import-check来阻止命令对于依赖库是否已经完成空安全适配的检查。 ``` $ dart migrate --skip-import-check ``` 如果你当前的 package 可以进行迁移,迁移工具会输出类似以下的内容: ```bash View the migration suggestions by visiting: http://127.0.0.1:60278/Users/you/project/mypkg.console-simple?authToken=Xfz0jvpyeMI%3D ``` 使用 Chrome 浏览器访问 URL,你可以看到一个交互式的界面,引导你进行迁移: ![](//img1.jcloudcs.com/developer.jdcloud.com/efd9a576-eb8c-4936-a229-a48904ce3f9120211018142205.png) 在工具中看到其推断的所有变量和类型注解。例如,在上面的截图中,工具推断第一行的 ints 列表元素可能为空,所以应该变为 int?(先前为 int)。我们可以手动对它的迁移方案进行修改,修改完成后通过点击APPLY MIGRATION来最终将修改生效。 #### 3.2 手动迁移 如果你不想使用迁移工具,你也可以手动进行迁移。 我们应当优先迁移最下层的库 —— 也就是哪些没有导入其他 package 的库,接着迁移直接依赖于这个库的依赖库,最后再迁移依赖项最多的库。 例如,假设你的 lib/src/util.dart 导入了其他(空安全)的 package 和核心库,但它没有包含任何 import '<本地路径>' 的引用。那么你应当优先考虑迁移 util.dart,然后迁移依赖了 util.dart 的文件。如果有一些循环引用的库(例如 A 引用了 B,B 引用了 C,C 引用了 A),建议同时对它们进行迁移。 手动对 package 进行迁移时,请参考以下步骤: 1)编辑 package 的 pubspec.yaml 文件,将最低 SDK 版本设置到 2.12.0: ``` environment: sdk: '>=2.12.0 <3.0.0' ``` 2)重新生成 package 的配置文件: ``` $ flutter pub get ``` 在版本最低是 2.12.0-0 的 SDK 上运行 dart pub get 时,会将每个 package 的默认 SDK 版本设定为 2.12,并且默认它们已经迁移至空安全。 3)在你的 IDE 上打开package 。 你也许会看到很多错误,没关系,让我们继续。 4)利用分析器来辨析静态错误,逐个迁移 Dart 文件。按需添加 ?、!、required 以及 late 来消除静态错误。 #### 3.3 迁移后的代码调整 自动迁移虽然可以批量帮助你做迁移,但是为了保险起见,所有自动调整的代码是需要开发者再重新进行调整的。而且从我们的迁移经验来说,自动迁移过程中,确实有些地方并不是特别合理,需要我们手动来调整。 在京东快递APP的迁移过程中,我们主要进行了下面几方面的调整: 1)类中未初始化的属性被默认设置为可空类型 在一些情况下,虽然属性在定义的时候未被初始化,但是在该属性使用之前,我们会在代码中对其进行赋值,并且该属性的值为非空,这时候就应该将该属性声明为late。 late 关键字主要用于延迟初始化。初始化主要分为两种:声明处默认值初始化和延迟初始化。但是并不是所有场景都合适使用声明处默认值初始化,这时候就需要使用late,但是当使用late变量时,一定要确保在使用之前要初始化,否则抛出异常。 2)删除对数据的空判断 以前很多代码的空判断逻辑可能需要我们手动删除,如果很多类型已经设置为非空,就不需要判断,而一些可能为空的判断也需要随着修改。比如: ``` //未迁移至空安全 bool login if(login){ xxx } //迁移至空安全 bool? login if(login == true){ xxx } ``` 3)在迁移后,可能会出现的报错 type 'Null' is not a subtype of type 'String' in type cast 这是因为你转换为空安全之后,默认的非可空类型 (String, int, bool, double, etc) 是不能设置为nil的。你需要重新修改你的代码看是否将一个可空的类型的变量或者nil直接赋值给了一个非可空类型的变量,然后在赋值前增加非空判断。如果这个被赋值的变量确实可以是可空的,也可以直接加一个 ?将他的类型变成可空的。 ```javascript String? myNullableString; ``` 4)有时候某些 await 语法会被自动迁移系统强行增加 as FutureOr ,而通常情况下,这是没必要的,因此如果你不需要改为原来的声明就可以。 5)有时候一些方法定义也会被强行修改,比如 redux 相关的这些修改可能也会影响运行问题,所以只需要把 as 部分去除就可以了。 6)除非你可以明确的确认非空类的数据不可能为空,不要轻易使用!来获取可空类型的值。 7)加入flutter_lint库对修改后的代码进行代码规范的检查,否则很容易在迁移后出现一些不易发现的问题。 #### 3.4 健全的空安全 随着所有的package和模块完成迁移,你就可以从非健全的空安全转换为健全的空安全。这时,就需要查看哪些库没有适配空安全。 ``` flutter pub outdated --mode=null-safety ``` 会列出所有的未支持的库,这时候你有三种选择: 1. 等待代码库的开发发布新版本 1. 如果有可以替代的库,替换已经适配空安全的库 1. 如果是开源库,clone库,自己进行空安全的适配 当所有的库,都修改完之后,就可以将我们的app.dart文件复原为原来的文件,并删除app_main.dart文件。就完成了整个的迁移过程。 ### 4 总结 使用Flutter 2.0并不意味着必须适配空安全。空安全是Dart 2.12增加的新功能,你可以使用Flutter 2.0,但是使用Dart 2.9的语法规则和特性。但是空安全带来了代码的健壮性和程序的稳定性,而且由于Flutter的开发很依赖于各种第三方库,随着很多第三方库在完成适配空安全之后,就不再维护非空安全的版本,因此尽早的迁移到空安全,除了提升APP的稳定性有好处外,也可以依赖于最新的第三方库进行开发,包括第三方库的bug修复和一些新功能的支持。 对于空安全的适配是一个系统性的工程,Flutter工程中的绝大多数代码可能都需要进行修改。因此除了开发的工作量,对于测试也是一个不小的挑战,需要大家的通力配合才能最终实现平稳的迁移。在京东快递APP的迁移过程中也是如此,任辉同学对于网络组件库的升级、耿晓晗同学对所有功能的细心测试和回归还有其他同学的支持,都保证了整个迁移的顺利落地,在此再次对大家的配合和努力表示感谢。 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:快递快运技术部 郝宏伟
原创文章,需联系作者,授权转载
上一篇:AspectJ浅析系列(三)自定义注解
下一篇:WebScoket简介与使用
相关文章
【技术干货】企业级扫描平台EOS关于JS扫描落地与实践!
Flutter异步编程中Completer的使用
聊一聊多线程不得不知的Future(一)
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号