Flutter app development tutorials by Andrea Bizzotto

Migrating a Flutter & Firebase app to Null Safety: A Case Study

Null Safety helps us eliminate an entire class of bugs in our Flutter apps.

As Flutter 2 is here, we can migrate our projects to Null Safety and make the most of it.

To help us along, the Dart documentation comes with a handy migration guide. And we can use the dart migrate command to automate part of the migration process.

This all sounds nice in theory, but the process can be lengthy for big apps what have a lot of dependencies.

So in this article I'll show you how to migrate a non-trivial project built with Flutter & Firebase.

Migrating to Null Safety is optional. You can choose to upgrade your app and its dependencies to Flutter 2, without opting in to Null Safety.

As a reference we will migrate my Time Tracking app, which is the official project from my Flutter & Firebase course:

Time Tracker app screenshots

You can find this project on GitHub as well as the completed migration on this PR.

For reference, this app has roughly 2,500 lines of code:

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 -------------------------------------------------------------------------------

To make the process as painless as possible, we will go through the migration process one step at a time and ensure our app works after each step.

Ready? Let's go!

0. Create a new branch

As we're about to do a lot of changes, it's best to work on a new branch:

git checkout -b null-safety-migration

This way we can go back to a working state if we make any mistakes.

1. Check if all dependencies can be upgraded

As outlined in the official migration guide, it's best to update all the project dependencies first.

So let's check what needs to be updated by running this command:

dart pub outdated --mode=null-safety

This produces the following output:

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. Update to all the latest dependencies

The log above shows that all the dependencies can be upgraded. So let's run this:

dart pub upgrade --null-safety

After this process completes, the pubspec.yaml is updated with new versions and we get this output:

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. Install the updated dependencies and dealing with breaking changes

Next, we want to install all the updated packages:

flutter pub get

The installation completes successfully and all our dependencies now use Null Safety.

But before we migrate our code to Null Safety, we need to check that the app still compiles and runs:

flutter analyze

As it turns out, we have a few errors due to breaking changes in some packages:

error There isnt 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)

Remember that we want our app to be runnable at each step. So we should fix those errors before moving forward. Here's a commit showing all the required changes at this stage.

This is a good time to commit any changes to our temporary branch in Git:

git add . git commit -m "Upgrade dependencies + fixes for breaking changes"

4. Fixing the build on iOS

Next, we can try to run our app. The Android build seems to work fine, but on iOS we get a long error log. The actual error is:

[!] 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.

The error log suggests to run pod repo update to resolve this:

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.

But this is not the solution in this case. The real problem is that cloud_firestore: ^1.0.0 requires version 7.3.0 of the iOS Firestore SDK, but version 6.33.0 is currently installed.

Running pod repo update is not enough in this case because our ios/Podfile.lock file still points to version 6.33.0. Let's fix this:

cd ios rm Podfile.lock pod repo update

Next, we need to set a minimum deployment target of iOS 10.0 in our Xcode project:

Setting the deployment target to 10.0

And we also need to set this at the top of the Podfile:

platform :ios, '10.0'

This is required by newer versions of the iOS Firestore SDK.

If we compile and run the iOS app now, everything works correctly.

Firestore is a big library with a lot of dependencies and it takes a long time to compile it from scratch. See this tip for how to speed up the build.

This completes our preparation work:

  • We have migrated all our dependencies to Null Safe versions ✅
  • We have fixed breaking changes in our code ✅
  • Our app builds and runs correctly on iOS and Android ✅

We can commit our changes once again, and get started with the migration.

Make sure your app works correctly with all the latest packages before attempting to migrate to Null Safety. This will give you more confidence in your changes later on. You can always revert to a previous commit if something goes wrong.

5. Running the migration

Let's run this:

dart migrate

The output will look like this:

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... [--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/\]

Because of all the preparation steps we did above, this command succeeds and the console prints a URL that we can open to start the migration process:

View the migration suggestions by visiting: http://127.0.0.1:60278/Users/you/project/mypkg.console-simple?authToken=Xfz0jvpyeMI%3D

If we click on it, this page will open in our browser:

At this stage we can:

  • review all the proposed changes
  • (optionally) add hint markers
  • apply the migration to all files (or a subset if we want to do this in batches)

This project is not huge so we can apply the migration to all files, and this will apply all the proposed changes. It will also update the minimum Dart SDK version to 2.12.0 in the pubspec.yaml file:

environment: sdk: ">=2.12.0 <3.0.0"

The migration tool will do a reasonable job at making the right changes, but it is not perfect.

Chances are that the project won't compile at this stage. So we have to go back to our code, review all changes, and tweak as needed. This ensures that we don't introduce tech debt at this stage and we have a good foundation going forward.

This can be a lenghty process and you may uncover some interesting gotchas. You can review this commit to get an idea of all the changes I made.

This document is quite useful at this stage:

The Flutter team has also shared this helpful video that explains all the steps in detail:

6. Doing a smoke test

Once we are satisfied with all the changes, we should run the app and make sure everything still works.

A quick smoke test is a good way to check that all the main features still work.

7. Migrating all the tests

In addition to the code in lib, this project also has a number of unit and widget tests. These should also be updated as part of the migration.

Many of these tests rely on the mockito package.

Unfortunately mockito currently has some serious compatibility problems with Null Safety. As it stands, we can only create mock classes by introducing code generation steps to our build, which is not ideal.

I have followed the steps in this NULL_SAFETY_README to get my tests to work, but didn't manage to make them all green.

The mocktail package by Felix Angelov solves these problems while implementing a very similar API to mockito, though I'm waiting to see if the two package authors can agree on a solution that works for everyone.

So for the time being, I disabled the tests that I couldn't get to work (YOLO 😅).

Wrap Up

From beginning to end, the migration process took less than one day for this project.

Performing all the steps in the right order definitely helped, and I recommend doing the same in your own projects.

The biggest pain point was to update the tests dependent on mockito. I hope mockito will support Null Safety without code generation in the future as many projects depend on it. If not, I'll move over to mocktail.

To reiterate what I said at the beginning:

Migrating to Null Safety is optional. You can choose to upgrade your app and its dependencies to Flutter 2, without opting in to Null Safety.

When you get a chance, I'd still recommend migrating so that you can run your apps and all their dependencies with Sound Null Safety.

The Dart documentation covers everything you need to know, and you may also find my Null Safety guide useful.

How has your experience been with Null Safety? Let me know on Twitter.

Happy coding!

Want more?

Support my work and fast-track your Flutter learning with my in-depth courses.