Migrate Your Flutter App to Null Safety

Charles Chan
The Startup
Published in
6 min readJan 23, 2021

--

When I read about Flutter/Dart’s null safety beta, I was really excited to give it a try. This article talks about my experience migrating my Flutter Web application to fully embrace this exciting new feature.

Let’s get started.

Upgrade Dependencies

I started by building my old project “Carry On Baggage Allowance Calculator” from scratch (Old as in late 2020). Here’s a fragment of my pubspec.yaml :

environment:
sdk: ">=2.7.0 <3.0.0"

dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
json_annotation: ^3.1.0
intl: ^0.16.1
yaml: ^2.2.1
...

With a fresh install, I did a % flutter pub get and was immediately greeted with an error:

Because luggage_finder_app depends on flutter_localizations any from sdk which depends on intl 0.17.0-nullsafety.2, intl 0.17.0-nullsafety.2 is required.
So, because luggage_finder_app depends on intl ^0.16.1, version solving failed.
Running "flutter pub get" in luggage_finder_app...
pub get failed (1; So, because luggage_finder_app depends on intl ^0.16.1, version solving failed.)

Although it was claimed that null safety is an opt-in feature, a fresh install still somehow forced me to upgrade my dependency. Anyway, I was glad that the error message was very useful and the change seemed easy. So, I replaced the line:

intl: ^0.16.1

with

intl: ^0.17.0-nullsafety.2

Now my % flutter pub get was clean and the app worked fine without any changes. That’s great! For any kind of migration exercise, it is always a good idea to start with a working baseline.

But hey, you asked, we haven’t turned on null safety yet! Yes, you’re right. Don’t celebrate too early. Change the line:

environment:
sdk: ">=2.7.0 <3.0.0"

to

environment:
sdk: ">=2.12.0-0 <3.0.0"

We were officially in the null safety mode now. But we were not done with dependency upgrade yet. Looking through ALL of our dependencies, we noted a few of them have already published their null safety versions. Let us upgrade them as well. For example, I also upgraded:

json_annotation: ^4.0.0-nullsafety.0
yaml: ^3.0.0-nullsafety.0

If your dependency provides a builder, don’t forget to run them again (and after every code change that may affect the result):

% flutter packages pub run build_runner build

As soon as you changed the environment to >=2.12.0 , your IDE should immediately flag null safety issues. Let’s tackle them one by one here:

Constructor Changes

Let’s start with fixing the constructors:

Kudos to the Dart/Flutter team. The error message I saw above was concise and easy to understand.

The first parameter Key is a parameter to the parent StatefulWidget class, let’s take a look at what it expects:

abstract class StatefulWidget extends Widget {
/// Initializes [key] for subclasses.
const StatefulWidget({ Key? key }) : super(key: key);

Do you notice the ? next to Key ? It means that the parameter is an nullable field. So, we can just denote the same in our own parameter in our constructor.

For the next parameter appDataUrl , it must not be null because that is where the application retrieves its data to function. To enforce non-null parameter, we changed the @required annotation to the required keyword. So, the modified constructor looked like this:

class HomePage extends StatefulWidget {
HomePage({Key? key, required this.appDataUrl})
: assert(appDataUrl != null),
super(key: key);

Map Access

The next error I saw was related accessing elements inside a map.

Although it wasn’t mentioned in the here or there, it makes sense that a map may not contain the element you need with the given key and must therefore return you a null object. Since I knew an element will always be found using the key, I simply denoted the non-nullable result by adding ! at the end:

airlines.forEach((airline) {
selectedAirlineLuggageConstraints[airline] =
appData.airlineLuggageConstraints[airline]!;
});

True Nullable

Ideally, we don’t ever want to deal with nulls. However, there are cases where having a null object makes sense. Case in point: Element retrieval from a map as seen above. So, how do we denote that it’s okay to return null? Here’s my next error to fix:

Again, kudo to the Dart/Flutter team, the error message is easy to understand. Assuming you are not going to change the logic of your original program, the change is easy. Btw, it is important to NOT change any program logic during migration. This helps you identify issues due to migration, not your logic change.

Simply add a ? to tell the compiler that a null value can be returned from the method (I also took the opportunity to mark my function variables as nullable):

DimensionConstraint? _findSmallestFitting(
List<DimensionConstraint> dimensionConstraints) {
double? width, height, depth, weight;

if (dimensionConstraints.isEmpty) {
return null;
}

Changing a function signature almost always have a cascade effect to the call centres. In my case, it generated the following errors.

Simply append ? to the variable to denote a nullable value:

final DimensionConstraint? maxPersonalItem =
_findSmallestFitting(personalItemConstraints);

Callback Function

A side effect to null safety is that the function signature is also controlled more tightly. For example, here’s my next error:

It was compiling and working before, but now, it’s complaining not just about null safety but about the type entirely. To fix this, we can follow the recommendation of the error message and change the type:

final void Function()? onPressed;

@override
Widget build(BuildContext context) {
return OutlineButton.icon(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
onPressed: this.onPressed,

Operator Overloading

Most of our changes so far are straightforward and can be understood very easily. However, don’t get carried away. Especially when you are dealing with null aware operators and operator overloading. Look at my following error as an example:

The original code above clearly indicates that the variable name can be null, otherwise, what is the point of using name? to access the map function. However, the Dart compiler did not flag the variable name as nullable. Let’s take a look at why.

It turns out that the Yaml 3.0.0-nullsafety.0 version has this line:

@override
dynamic operator [](key) => nodes[key]?.value;

Hmm.. So, if the nodes[key] is null, it should return a null value. But the Dart compiler let it pass as if it will always return a non-null value. Is it because we cannot express nullable result in an overloaded operator? If you know the answer, let me know.

Anyway, the correct fix should be a combination of ? to denote all nullable variables.

final YamlMap? name = item['name'];
final Map<String, String>? nameMap =
name?.map((locale, value) => MapEntry(locale, value));

Clean Up

After you are done with the conversion, there are cases where null check is no longer necessary. So, let’s do some clean up. For example:

Let’s change this to:

Dimension(this.width, this.height, this.depth, this.weight);

and my weight variable (which is nullable) to:

final double? weight;

Wow! Good riddance. This is what we want to see. Clean and concise code without worrying about runtime null safety.

Conclusion

In this article, I demonstrated how to migrate my pet project to use the null safety feature. Kudos to the Dart/Flutter team, the error messages are concise and easy to follow.

As a programmer, switching to null safety gives you extra confidence about your variables like type safety does. As you migrate, you might also uncover potential bugs that didn’t catch your eyes before.

I was very excited to see null safety landed in the Dart/Flutter land. As this article said, this is a major milestone for the language and the community.

--

--

Charles Chan
The Startup

A seasoned consultant specialized in Software Development and Architecture. Charles also loves to tavel, follow him on https://www.gorestrepeat.com/