前言
目前项目已经在2.x的版本上运行一段时间了,截止到目前Flutter稳定版本已经到3.7.0了,
但是Flutter3.0.x是最后支持iOS9、iOS10以及32位系统的版本,所以基于各方面考虑,决定把Flutter升级到3.0.5版本。
同时,因为空安全也已经出来很久了,且在dart 2.19版本后,可能不支持空安全迁移工具了,所以决定把项目也迁移到空安全。
主要两步:
准备工作
- 使用命令dart --version查看dart版本
kaye@KKdeMacBook-Pro app-flutter % dart --version
Dart SDK version: 2.12.2 (stable) (Wed Mar 17 10:30:20 2021 +0100) on "macos_x64"
- 使用命令
flutter doctor
查看flutter
版本
kaye@KKdeMacBook-Pro app-flutter % flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.0.4, on macOS 11.5.1 20G80 darwin-x64, locale zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio (version 2020.3)
[✓] VS Code (version 1.62.2)
[✓] Connected device (1 available)
• No issues found!
升级Flutter版本
//升级到支持的最新版本
flutter upgrade
//如果你想指定版本,则在升级后可以进入到flutter的安装目录进行重置
//HEAD^就是你想要的版本的commit id
git reset --hard HEAD^
升级依赖三方库
- 使用命令
dart pub outdated --mode=null-safety
查看当前依赖的pakeage
是否支持空安全。
项目中所依赖的三方库,有一部分没有支持空安全,如那些版本前面有x
的三方库,而那些打√
的就是已经支持的空安全版本。
kaye@KKdeMacBook-Pro app-flutter % dart pub outdated --mode=null-safety
Showing dependencies that are currently not opted in to null-safety.
[✗] indicates versions without null safety support.
[✓] indicates versions opting in to null safety.
Package Name Current Upgradable Resolvable Latest
direct dependencies:
charts_flutter ✗0.9.0 ✗0.9.0 ✓0.11.0 ✓0.12.0
flutter_swiper ✗1.1.6 ✗1.1.6 ✗1.1.6 ✗1.1.6
keyboard_visibility ✗0.5.6 ✗0.5.6 ✗0.5.6 ✗0.5.6
1 dependency is constrained to a version that is older than a resolvable version.
To update it, edit pubspec.yaml, or run `dart pub upgrade --null-safety`.
- 升级依赖库
dart pub upgrade --null-safety
如果你的依赖库全部支持空安全,这里会将所有依赖升级到空安全中,如果不是全部支持,命令行中会打印很多支持依赖的三方库,只需要将建议运行的命令拷贝并在命令行中运行即可。至于那些一直不支持空安全的三方库,需要考虑更换别的库进行代替了。
启动迁移
执行启动迁移命令, 如果在使用命令过程遇见了错误,可根据提示,参考下面的命令
//直接迁移
dart migrate
//跳过依赖的三方库是否支持空安全
dart migrate --skip-import-check
//跳过依赖的三方库是否支持空安全且忽略异常情况
dart migrate --skip-import-check --ignore-exceptions
执行命令,开始分析需要进行迁移的代码
kaye@KKdeMacBook-Pro app-flutter % dart migrate
Migrating /Users/xxxx/app-flutter
See https://dart.dev/go/null-safety-migration for a migration guide.
Analyzing project...
[--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------]No analysis issues found.
Generating migration suggestions...
[--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------]
Compiling instrumentation information...
[--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------]
View the migration suggestions by visiting:
http://127.0.0.1:61098/xxxx/app-flutter?authToken=S12X5Ez93Iw%3D
Use this interactive web view to review, improve, or apply the results.
When finished with the preview, hit ctrl-c to terminate this process.
If you make edits outside of the web view (in your IDE), use the 'Rerun from
sources' action.
- 左侧部分:是建议支持空安全的文件,选中每个文件会在中间展示这个文件的所有变化后的代码,如果你不想迁移某个文件,可以取消勾选,具体请看后面的文章,有介绍
- 中间部分:是让我们看所有变化后的代码,我们可以在上面进行确认修改
- 右侧部分:是相关个改变的结果详细注解,点击
linexxx
,下面的Edit Details
可以看具体的原因
从上图我们可以看到,高亮部分,如late、?、!
之类的都是迁移工具经过分析之后,给我们自动添加上去的,但是通过工具推导出来的类型也可能是错误的,我们就需要对迁移结果进行改进。下面,通过一个例子来说明。
迁移顺序
遵循一个原则,从依赖最少的文件开始迁移。
如:A依赖B,B依赖C,C不依赖别的文件,那么迁移顺序就是C、B、A。
改进迁移建议
- 原来非空安全的代码
var intList = const <int>[0, null];
var zero = intList[0];
var one = zero + 1;
var zeroOne = <int>[zero, one];
- 迁移工具迁移为空安全的结果
var intList = const <int?>[0, null];
var zero = intList[0];
var one = zero! + 1;
var zeroOne = <int?>[zero, one];
从上面的结果,我们可以看到 ,迁移工具认为zero
变量的类型为int?
,但是其实我们知道,这种情况下,zero
不可能为null
,且zeroOn
e中的元素也不可能为null
,所以我们需要对迁移结果进行改进。此时我们还没有应用迁移结果,所以我们仍然可以在IDE
中修改我们的代码,然后点击迁移界面右上角RERUN FROM SOURCES
按钮进行预估迁移结果刷新。
因为在此时我们还没有完全支持空安全,所以在代码中,无法使用late、?、!
这些关键字,如果你使用了,那么开发工具爆红,如下图
那么我们怎么在非完全空安全的情况下,进行标记呢?官方给我们提供了一些表达式,供我们使用。
表达式 | 说明 |
---|---|
expression /*!*/ |
添加 ! 至代码中,将 表达式 转换为其基础类型对应的不可空的类型。 |
type /*!*/ |
将 类型 标记为非空。 |
/*?*/ |
将前面的类型标记为可空。 |
/*late*/ |
将变量声明标记为 late ,表示其不会第一时间进行初始化。 |
/*late final*/ |
将变量声明标记为 late final ,表示其不会第一时间进行初始化,且初始化后不可改变。 |
/*required*/ |
将参数标记为required
|
所以,我们可以在我们的代码中使用/*?*/
或者/*!*/
等进行标记。
var intList = const <int>[0, null];
var zero = intList[0]/*!*/;
var one = zero + 1;
var zeroOne = <int>[zero, one];
点击迁移界面右上角RERUN FROM SOURCES按钮进行预估迁移结果刷新,结果如下:
可以看到,因为在第2行末尾添加了/!/,导致迁移工具给出的迁移结果也是不一样的。
//原始代码迁移结果
var intList = const <int?>[0, null];
var zero = intList[0];
var one = zero! + 1;
var zeroOne = <int?>[zero, one];
//添加标记改进后迁移结果
var intList = const <int?>[0, null];
var zero = intList[0]/*!*/;
var one = zero + 1;
var zeroOne = <int >[zero, one];
应用迁移
如果我们觉得迁移结果没有什么问题,那么点击浏览器中迁移界面右上角APPLY MIGRATION就可以应用迁移结果,这个操作是不可逆的,所以我们需要确保完全认同迁移结果了,才可以点击。
注意:应用迁移后会修改pubspec.yaml文件中的environment sdk版本
点击确定,再看我们的代码,此时已经应用了空安全。
控制台输出哪些文件迁移到了空安全,哪些不是空安全。
Applying migration suggestions to disk...
Migrated 8 files:
test/widget_test.dart
lib/turn_box.dart
lib/main.dart
lib/my_process_bar.dart
lib/my_painter.dart
lib/demo.dart
pubspec.yaml
.dart_tool/package_config.json
Opted 1 file out of null safety with a new Dart language version comment:
lib/demo2.dart
pubspec.yaml中修改结果
//迁移前的版本
environment:
sdk: '>=2.10.0 <3.0.0'
//迁移后的版本
environment:
sdk: '>=2.12.0 <3.0.0'
代码分析
迁移到空安全后,我们使用代码分析器,对代码进行分析,帮助我们进一步修改代码,比如有些变量从可空变成了非空,如果我们在代码中又判断了是否为空,就显得有些不必要。
使用dart analyze
命令进行代码分析,控制台也给出了相应的提示,逐一修改即可。
当然,在Flutter
开发中,如果我们使用了Android Studio
的话,就可以直接使用可视化工具进行dart analyze
如下图所示,双击就可以定位到相应的位置,按需修复即可。
如果你不想迁移某些包
在使用迁移工具迁移项目时,有可能因为项目巨大,一次性无法完全迁移,那么可以直接取消勾选,这样这些文件就不会被迁移,同时会在你的文件中头部插入一行类似下面的注释,这样你的文件就不会应用空安全。后续如果想迁移到空安全,就再次执行命令dart migrate
即可。
// @dart=2.9
// 如果不想应用空安全,可以添加上面的代码
var intList = const <int>[0, null];
var zero = intList[0];
var one = zero + 1;
var zeroOne = <int>[zero, one];
过程中遇见的问题
Q1. 第三方库不支持空安全,导致dart migrate
命令执行错误
解决方案,执行命令 dart migrate --skip-import-check`
Q2. 代码异常Null check operator used on a null value at offset
解决方案:找出相应的错误代码所在文件,删除
?
或者执行dart migrate --skip-import-check --ignore-exceptions
kk@dabaodeMacBook-Pro app-flutter % dart migrate --skip-import-check
Migrating /Users/qgg/workspace/app-flutter
See https://dart.dev/go/null-safety-migration for a migration guide.
Analyzing project...
[-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|]Warning: package has unmigrated dependencies.
Continuing due to the presence of `--skip-import-check`. To see a complete
list of the unmigrated dependencies, re-run without the `--skip-import-check`
flag.
No analysis issues found.
Generating migration suggestions...
[-------------------------| ]Aborting migration due to an exception. This most likely is due to a
bug in the migration tool. Please consider filing a bug report at:
https://github.com/dart-lang/sdk/issues/new
Please include the SDK version (2.17.6) in your bug report.
To attempt to perform migration anyway, you may re-run with
--ignore-exceptions.
Exception details:
Null check operator used on a null value at offset 4675 in /Users/qgg/workspace/app-flutter/lib/widget/trade/aip/aip_trade_target_rate_widget.dart (Offset?.zero)
#0 EdgeBuilder._handlePropertyAccessGeneralized (package:nnbd_migration/src/edge_builder.dart:3186:46)
#1 ....
......
Q2. FlutterBoost 4.x返回值的问题
在使用FlutterBoost
进行push
页面的时候,如果使用await
或者then
获取pop
的返回值时,FlutterBoost
对返回值进行了调整,4.x
版本目前返回的结果如果没有指定的话,返回的是一个空Map
,在接收的时候要格外小心,除非返回的是Map
类型,否则不要写明类型,不然会抛出一个类型错误。具体在FlutterBoostApp._completePendingResultIfNeeded
中体现,boost
官方推荐返回值使用Map
case 1: BoostNavigator打开,result是个空的Map,如果指定了类型接收result,非Map类型可能会抛出类型错误的异常
A 页面:
var result = await BoostNavigator.instance.push(xxx, withContainer: true);
B 页面:
BoostNavigator.instance.pop(); //这里没有传入pop的值
case 2: BoostNavigator打开,正常
A 页面:
bool result = await BoostNavigator.instance.push(xxx, withContainer: true);
B 页面:
BoostNavigator.instance.pop(true);
case 3:系统 Navigator打开,result可以是任何类型,包括null
A 页面:
var result = await Navigator.push(context, xxx);
B 页面:
Navigator.of(context).pop((bool/int/xxx); 或者
BoostNavigator.instance.pop(bool/int/xxx);非null值时,是可以指定具体的类型