空值安全(Null Safety)通过在开发过程中而不是在运行时捕获空值错误,帮助我们避免Flutter应用程序中的整类错误。
随着Flutter 2的到来,我们可以将我们的项目迁移到Null Safety并充分利用它。
为了帮助我们,Dart文档中提供了一个方便的迁移指南。而且我们可以使用dart migrate 命令来自动完成部分迁移过程。
这在理论上听起来很不错,但对于有很多依赖关系的大型应用来说,这个过程可能会很漫长。
所以在这篇文章中,我将向你展示如何迁移一个用Flutter和Firebase构建的非简单的项目。
在我们讨论这个问题之前,让我们先把一件事弄清楚。
如何检查一个项目是否在使用Null Safety?
打开pubspec.yaml 文件,在environment 部分中检查Dart SDK的版本。
environment:
sdk: ">=2.10.0 <3.0.0"
- 你是否看到SDK:2.10.0或以下?那么你运行的是没有Null Safety的项目
- 你看到sdk:2.12.0或以上?那么你运行的是Null Safety
请牢记这一点。
迁移到Null Safety是可选的。您可以选择将您的应用程序及其依赖关系升级到 Flutter 2**,而不选择 Null Safety**,方法是在您的
pubspec.yaml中保持 Dart SDK 为 2.10.0 或以下版本。
将Flutter和Firebase应用迁移至Null Safety
作为参考,我们将迁移我的时间跟踪应用程序,它是我的Flutter & Firebase课程的官方项目。
时间追踪应用程序截图
你可以在GitHub上找到这个项目,也可以在这个PR上找到已经完成的迁移。
作为参考,这个应用大约有2500行代码。
45 text files.
45 unique files.
0 files ignored.
github.com/AlDanial/cloc v 1.84 T=0.03 s (1311.3 files/s, 82058.4 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Dart 45 265 13 2538
-------------------------------------------------------------------------------
SUM: 45 265 13 2538
-------------------------------------------------------------------------------
为了使这一过程尽可能地无痛,我们将一步一步地完成迁移过程,并确保我们的应用程序在每一步之后都能正常工作。
准备好了吗?让我们开始吧!
0.创建一个新的分支
由于我们要做大量的修改,最好在一个新的分支上工作。
git checkout -b null-safety-migration
这样一来,如果我们犯了什么错误,就可以回到工作状态了。
1.检查所有的依赖关系是否可以被升级
正如官方迁移指南中所述,最好先更新项目的所有依赖关系。
因此,让我们通过运行这个命令来检查哪些需要更新。
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:
cloud_firestore ✗0.14.3 ✗0.14.3 ✓1.0.0 ✓1.0.0
cupertino_icons ✗1.0.0 ✓1.0.2 ✓1.0.2 ✓1.0.2
firebase_auth ✗0.18.2 ✗0.18.2 ✓1.0.0 ✓1.0.0
firebase_core ✗0.5.2 ✗0.5.2 ✓1.0.0 ✓1.0.0
flutter_login_facebook ✗0.4.0+1 ✗0.4.0+1 ✓1.0.0-nullsafety.1 ✓1.0.0-nullsafety.1
google_sign_in ✗4.5.6 ✗4.5.6 ✓5.0.0 ✓5.0.0
intl ✗0.16.1 ✗0.16.1 ✓0.17.0 ✓0.17.0
provider ✗4.3.2+2 ✗4.3.2+2 ✓5.0.0 ✓5.0.0
rxdart ✗0.24.1 ✗0.24.1 ✓0.26.0 ✓0.26.0
dev_dependencies:
mockito ✗4.1.3 ✗4.1.3 ✓5.0.0 ✓5.0.0
1 upgradable dependency is locked (in pubspec.lock) to an older version.
To update it, use `dart pub upgrade`.
9 dependencies are constrained to versions that are older than a resolvable version.
To update these dependencies, edit pubspec.yaml, or run `dart pub upgrade --null-safety`.
2.更新到所有最新的依赖项
上面的日志显示,所有的依赖关系都可以被升级。所以让我们运行这个。
dart pub upgrade --null-safety
这个过程完成后,pubspec.yaml ,用新的版本进行更新,我们得到这个输出。
cupertino_icons: ^1.0.0 -> ^1.0.2
firebase_core: 0.5.2 -> ^1.0.0
firebase_auth: 0.18.2 -> ^1.0.0
google_sign_in: 4.5.6 -> ^5.0.0
flutter_login_facebook: 0.4.0+1 -> ^1.0.0-nullsafety.1
provider: 4.3.2+2 -> ^5.0.0
cloud_firestore: 0.14.3 -> ^1.0.0
intl: 0.16.1 -> ^0.17.0
rxdart: 0.24.1 -> ^0.26.0
mockito: 4.1.3 -> ^5.0.0
3.安装更新后的依赖关系并处理破坏性变化
接下来,我们要安装所有的更新包。
flutter pub get
安装成功后,我们所有的依赖项现在都使用Null Safety。
但在我们将代码迁移到Null Safety之前,我们需要检查应用程序是否仍在编译和运行。
flutter analyze
事实证明,由于一些软件包的破坏性变化,我们有一些错误。
error • There isn’t a setter named 'value' in class 'ValueStreamExtensions' at lib/app/sign_in/email_sign_in_bloc.dart:60:19 • (assignment_to_final_no_setter)
error • Case expressions must be constant at lib/services/auth.dart:91:12 • (non_constant_case_expression)
error • There's no constant named 'Success' in 'FacebookLoginStatus' at lib/services/auth.dart:91:32 • (undefined_enum_constant)
error • Case expressions must be constant at lib/services/auth.dart:97:12 • (non_constant_case_expression)
error • There's no constant named 'Cancel' in 'FacebookLoginStatus' at lib/services/auth.dart:97:32 • (undefined_enum_constant)
error • Case expressions must be constant at lib/services/auth.dart:102:12 • (non_constant_case_expression)
error • There's no constant named 'Error' in 'FacebookLoginStatus' at lib/services/auth.dart:102:32 • (undefined_enum_constant)
请记住,我们希望我们的应用程序在每一步都是可运行的。所以我们应该在继续前进之前修复这些错误。下面是一个提交,显示了这个阶段所有需要的修改。
这是一个很好的时机,可以将任何修改提交到我们在Git中的临时分支。
git add .
git commit -m "Upgrade dependencies + fixes for breaking changes"
4.修复iOS上的构建
接下来,我们可以试着运行我们的应用程序。安卓系统的构建似乎可以正常工作,但在iOS上,我们得到了一个很长的错误日志。实际的错误是。
[!] CocoaPods could not find compatible versions for pod "Firebase/Firestore":
In snapshot (Podfile.lock):
Firebase/Firestore (= 6.33.0, ~> 6.33.0)
In Podfile:
cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) was resolved to 1.0.0, which depends on
Firebase/Firestore (= 7.3.0)
You have either:
* out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.
* changed the constraints of dependency `Firebase/Firestore` inside your development pod `cloud_firestore`.
You should run `pod update Firebase/Firestore` to apply changes you've made.
错误日志建议运行pod repo update 来解决这个问题。
Error: CocoaPods's specs repository is too out-of-date to satisfy dependencies.
To update the CocoaPods specs, run:
pod repo update
Error running pod install
Error launching application on iPhone 12 Pro Max.
但在这种情况下,这不是解决方案。真正的问题是,cloud_firestore: ^1.0.0 需要iOS Firestore SDK的版本7.3.0 ,但目前安装的是版本6.33.0 。
在这种情况下,运行pod repo update 是不够的,因为我们的ios/Podfile.lock 文件仍然指向版本6.33.0 。让我们来解决这个问题。
cd ios
rm Podfile.lock
pod repo update
接下来,我们需要在我们的Xcode项目中设置iOS 10.0的最低部署目标。
设置部署目标为10.0
而且我们还需要在Podfile的顶部设置这个。
platform :ios, '10.0'
这是新版本的iOS Firestore SDK的要求。
如果我们现在编译并运行iOS应用,一切都能正常工作。
Firestore是一个很大的库,有很多依赖关系,从头编译需要很长时间。关于如何加快构建速度,请看这个提示。
这就完成了我们的准备工作。
- 我们已经将所有的依赖关系迁移到Null Safe版本 ✅
- 我们已经修复了代码中的破坏性变化 ✅
- 我们的应用程序在iOS和Android上正确构建和运行 ✅
我们可以再次提交我们的修改,并开始进行迁移。
在尝试迁移到Null Safety之前,请确保你的应用程序能够在所有最新的软件包中正常运行。这将使你在以后的修改中更有信心。如果出了问题,你可以随时恢复到之前的提交。
5.运行迁移
让我们来运行这个。
dart migrate
输出结果会是这样的。
Migrating /Users/andrea/work/codewithandrea/github/flutter-course-apps/time_tracker_flutter_course
See https://dart.dev/go/null-safety-migration for a migration guide.
Analyzing project...
[--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/\]
由于我们在上面做了所有的准备步骤,这个命令成功了,控制台打印出一个URL,我们可以打开这个URL来开始迁移过程。
View the migration suggestions by visiting:
http://127.0.0.1:60278/Users/you/project/mypkg.console-simple?authToken=Xfz0jvpyeMI%3D
如果我们点击它,这个页面将在我们的浏览器中打开。
在这个阶段,我们可以。
- 查看所有建议的修改
- (可以选择)添加提示标记
- 对所有的文件进行迁移(如果我们想分批进行,也可以选择一个子集)。
这个项目不是很大,所以我们可以将迁移应用到所有的文件,这将应用所有建议的修改。它还会在pubspec.yaml 文件中把Dart SDK的最低版本更新为2.12.0。
environment:
sdk: ">=2.12.0 <3.0.0"
迁移工具会合理地进行正确的修改,但它并不完美。
在这个阶段,项目有可能无法编译。所以我们必须回到我们的代码中,审查所有的变化,并根据需要进行调整。这可以确保我们在这个阶段不会引入技术债务,并且我们有一个良好的基础来继续前进。
这可能是一个漫长的过程,你可能会发现一些有趣的问题。你可以查看这个提交,了解我所做的所有改动。
在这个阶段,这份文件是非常有用的。
Flutter团队还分享了这个有用的视频,详细解释了所有的步骤。
6.做一个烟雾测试
一旦我们对所有的变化感到满意,我们应该运行应用程序,并确保一切都能正常工作。
快速的烟雾测试是检查所有主要功能是否仍然工作的一个好方法。
7.移植所有的测试
除了lib 中的代码,这个项目还有一些单元和部件测试。这些也应该作为迁移的一部分被更新。
这些测试中有许多都依赖于mockito包。
不幸的是,mockito目前与Null Safety有一些严重的兼容性问题。目前,我们只能通过在构建中引入代码生成步骤来创建模拟类,这并不理想。
我已经按照这个NULL_SAFETY_README中的步骤让我的测试工作起来,但没有设法让它们都变成绿色。
Felix Angelov的mocktail包解决了这些问题,同时实现了与mockito非常相似的API,尽管我在等待,看这两个包的作者是否能达成一个适合所有人的解决方案。
因此,我暂时禁用了那些我无法工作的测试(YOLO 😅)。
总结
从开始到结束,这个项目的迁移过程花了不到一天时间。
按照正确的顺序执行所有的步骤绝对有帮助,我建议在你自己的项目中也这样做。
最大的痛点是要更新依赖mockito的测试。我希望mockito将来能支持Null Safety而不需要生成代码,因为很多项目都依赖于它。如果没有,我就转而使用mocktail。
重申我在开始时所说的。
迁移到Null Safety是可选的。你可以选择将你的应用程序和它的依赖关系升级到Flutter 2,而不选择加入Null Safety。
当你有机会的时候,我还是建议你进行迁移,这样你就可以用Sound Null Safety来运行你的应用程序和它们的所有依赖关系。
Dart文档涵盖了你需要知道的一切,你也可能发现我的Null Safety指南很有用。