前言
目前項目已經(jīng)在2.x的版本上運(yùn)行一段時間了,截止到目前Flutter穩(wěn)定版本已經(jīng)到3.7.0了,
但是Flutter3.0.x是最后支持iOS9、iOS10以及32位系統(tǒng)的版本,所以基于各方面考慮,決定把Flutter升級到3.0.5版本。
同時,因?yàn)榭瞻踩惨呀?jīng)出來很久了,且在dart 2.19版本后,可能不支持空安全遷移工具了,所以決定把項目也遷移到空安全。
主要兩步:
準(zhǔn)備工作
- 使用命令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
//如果你想指定版本,則在升級后可以進(jìn)入到flutter的安裝目錄進(jìn)行重置
//HEAD^就是你想要的版本的commit id
git reset --hard HEAD^
升級依賴三方庫
- 使用命令
dart pub outdated --mode=null-safety查看當(dāng)前依賴的pakeage是否支持空安全。
項目中所依賴的三方庫,有一部分沒有支持空安全,如那些版本前面有x的三方庫,而那些打√的就是已經(jīng)支持的空安全版本。
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
如果你的依賴庫全部支持空安全,這里會將所有依賴升級到空安全中,如果不是全部支持,命令行中會打印很多支持依賴的三方庫,只需要將建議運(yùn)行的命令拷貝并在命令行中運(yùn)行即可。至于那些一直不支持空安全的三方庫,需要考慮更換別的庫進(jìn)行代替了。
啟動遷移
執(zhí)行啟動遷移命令, 如果在使用命令過程遇見了錯誤,可根據(jù)提示,參考下面的命令
//直接遷移
dart migrate
//跳過依賴的三方庫是否支持空安全
dart migrate --skip-import-check
//跳過依賴的三方庫是否支持空安全且忽略異常情況
dart migrate --skip-import-check --ignore-exceptions
執(zhí)行命令,開始分析需要進(jìn)行遷移的代碼
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.
- 左側(cè)部分:是建議支持空安全的文件,選中每個文件會在中間展示這個文件的所有變化后的代碼,如果你不想遷移某個文件,可以取消勾選,具體請看后面的文章,有介紹
- 中間部分:是讓我們看所有變化后的代碼,我們可以在上面進(jìn)行確認(rèn)修改
- 右側(cè)部分:是相關(guān)個改變的結(jié)果詳細(xì)注解,點(diǎn)擊
linexxx,下面的Edit Details可以看具體的原因

從上圖我們可以看到,高亮部分,如late、?、!之類的都是遷移工具經(jīng)過分析之后,給我們自動添加上去的,但是通過工具推導(dǎo)出來的類型也可能是錯誤的,我們就需要對遷移結(jié)果進(jìn)行改進(jìn)。下面,通過一個例子來說明。
遷移順序
遵循一個原則,從依賴最少的文件開始遷移。
如:A依賴B,B依賴C,C不依賴別的文件,那么遷移順序就是C、B、A。
改進(jìn)遷移建議
- 原來非空安全的代碼
var intList = const <int>[0, null];
var zero = intList[0];
var one = zero + 1;
var zeroOne = <int>[zero, one];
- 遷移工具遷移為空安全的結(jié)果
var intList = const <int?>[0, null];
var zero = intList[0];
var one = zero! + 1;
var zeroOne = <int?>[zero, one];
從上面的結(jié)果,我們可以看到 ,遷移工具認(rèn)為zero變量的類型為int?,但是其實(shí)我們知道,這種情況下,zero不可能為null,且zeroOne中的元素也不可能為null,所以我們需要對遷移結(jié)果進(jìn)行改進(jìn)。此時我們還沒有應(yīng)用遷移結(jié)果,所以我們?nèi)匀豢梢栽?code>IDE中修改我們的代碼,然后點(diǎn)擊遷移界面右上角RERUN FROM SOURCES按鈕進(jìn)行預(yù)估遷移結(jié)果刷新。
因?yàn)樵诖藭r我們還沒有完全支持空安全,所以在代碼中,無法使用late、?、!這些關(guān)鍵字,如果你使用了,那么開發(fā)工具爆紅,如下圖

那么我們怎么在非完全空安全的情況下,進(jìn)行標(biāo)記呢?官方給我們提供了一些表達(dá)式,供我們使用。
| 表達(dá)式 | 說明 |
|---|---|
expression /*!*/ |
添加 ! 至代碼中,將 表達(dá)式 轉(zhuǎn)換為其基礎(chǔ)類型對應(yīng)的不可空的類型。 |
type /*!*/ |
將 類型 標(biāo)記為非空。 |
/*?*/ |
將前面的類型標(biāo)記為可空。 |
/*late*/ |
將變量聲明標(biāo)記為 late,表示其不會第一時間進(jìn)行初始化。 |
/*late final*/ |
將變量聲明標(biāo)記為 late final,表示其不會第一時間進(jìn)行初始化,且初始化后不可改變。 |
/*required*/ |
將參數(shù)標(biāo)記為required
|
所以,我們可以在我們的代碼中使用/*?*/或者/*!*/等進(jìn)行標(biāo)記。
var intList = const <int>[0, null];
var zero = intList[0]/*!*/;
var one = zero + 1;
var zeroOne = <int>[zero, one];
點(diǎn)擊遷移界面右上角RERUN FROM SOURCES按鈕進(jìn)行預(yù)估遷移結(jié)果刷新,結(jié)果如下:

可以看到,因?yàn)樵诘?行末尾添加了/!/,導(dǎo)致遷移工具給出的遷移結(jié)果也是不一樣的。
//原始代碼遷移結(jié)果
var intList = const <int?>[0, null];
var zero = intList[0];
var one = zero! + 1;
var zeroOne = <int?>[zero, one];
//添加標(biāo)記改進(jìn)后遷移結(jié)果
var intList = const <int?>[0, null];
var zero = intList[0]/*!*/;
var one = zero + 1;
var zeroOne = <int >[zero, one];
應(yīng)用遷移
如果我們覺得遷移結(jié)果沒有什么問題,那么點(diǎn)擊瀏覽器中遷移界面右上角APPLY MIGRATION就可以應(yīng)用遷移結(jié)果,這個操作是不可逆的,所以我們需要確保完全認(rèn)同遷移結(jié)果了,才可以點(diǎn)擊。
注意:應(yīng)用遷移后會修改pubspec.yaml文件中的environment sdk版本

點(diǎn)擊確定,再看我們的代碼,此時已經(jīng)應(yīng)用了空安全。
控制臺輸出哪些文件遷移到了空安全,哪些不是空安全。
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中修改結(jié)果
//遷移前的版本
environment:
sdk: '>=2.10.0 <3.0.0'
//遷移后的版本
environment:
sdk: '>=2.12.0 <3.0.0'
代碼分析
遷移到空安全后,我們使用代碼分析器,對代碼進(jìn)行分析,幫助我們進(jìn)一步修改代碼,比如有些變量從可空變成了非空,如果我們在代碼中又判斷了是否為空,就顯得有些不必要。
使用dart analyze命令進(jìn)行代碼分析,控制臺也給出了相應(yīng)的提示,逐一修改即可。
當(dāng)然,在Flutter開發(fā)中,如果我們使用了Android Studio的話,就可以直接使用可視化工具進(jìn)行dart analyze如下圖所示,雙擊就可以定位到相應(yīng)的位置,按需修復(fù)即可。

如果你不想遷移某些包
在使用遷移工具遷移項目時,有可能因?yàn)轫椖烤薮?,一次性無法完全遷移,那么可以直接取消勾選,這樣這些文件就不會被遷移,同時會在你的文件中頭部插入一行類似下面的注釋,這樣你的文件就不會應(yīng)用空安全。后續(xù)如果想遷移到空安全,就再次執(zhí)行命令dart migrate即可。
// @dart=2.9
// 如果不想應(yīng)用空安全,可以添加上面的代碼
var intList = const <int>[0, null];
var zero = intList[0];
var one = zero + 1;
var zeroOne = <int>[zero, one];

過程中遇見的問題
Q1. 第三方庫不支持空安全,導(dǎo)致dart migrate命令執(zhí)行錯誤
解決方案,執(zhí)行命令 dart migrate --skip-import-check`

Q2. 代碼異常Null check operator used on a null value at offset
解決方案:找出相應(yīng)的錯誤代碼所在文件,刪除
?
或者執(zhí)行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進(jìn)行push頁面的時候,如果使用await或者then獲取pop的返回值時,FlutterBoost對返回值進(jìn)行了調(diào)整,4.x版本目前返回的結(jié)果如果沒有指定的話,返回的是一個空Map,在接收的時候要格外小心,除非返回的是Map類型,否則不要寫明類型,不然會拋出一個類型錯誤。具體在FlutterBoostApp._completePendingResultIfNeeded中體現(xiàn),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:系統(tǒng) 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值時,是可以指定具體的類型