前言
目前項目已經在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值時,是可以指定具體的類型