How to convert the Flutter project to null safety without hassle

Nihad Delic
6 min readApr 4, 2022

It's a really old Flutter topic, but I believe that there are still some devs that are confused about some of, let's call them “situations”, with null safety. This article will give you a strategy on how to convert your existing project to null safety, along with some tips and tricks on how to improve checks on nullable instances.

I would say that's really easy to convert some simple Flutter projects and startups that you finished in 2–3 months, but what to do with bigger projects that are in development for 2–3 years? The Flutter team has given us great tools for conversion, but sometimes that is just not enough. It’s simply not safe to convert all files to null safety in one day. It might be easy to convert, but testing every module, feature, and environment robustly enough to release the next day with confidence is not. You definitely need some sort of strategy, and that strategy has a name. TESTING!

The only way to go fast, is to go well
— Robert C. Martin — “Clean Architecture”

Testing strategy

Unit-widget-golden-integration

Of course, as always, I would say: “write tests!”. This is the first and most important rule in every refactoring, modification, or implementation of new code. It would be perfect if you already implemented all your tests by test-driven development.

QA testing

Even if we are using TDD on our project, I wouldn’t have complete confidence in our tests. QA testing is really important in detecting regressions, but we all know that people who work in the QA team are humans, just like us, and humans make mistakes. At this stage, I would expect our regression-free level of confidence to be 99.99%, but just in case there is the next phase of testing.

Releases — User testing

Who better to test than the real users? What I mean by this is releasing conversion to null-safety in small chunks. Ideally, we would convert one bigger module, complete the testing on our end and release it to production. It’s slow but much safer than doing it all in one go, and it will allow you to roll back only one directory/module and detect mistakes more easily.

These three phases of testing will not guarantee a bug-free product, but they will definitely reduce the chance of something unpredictable happening.

Let’s code, enough talking

There are definitely a few tips and tricks on how to avoid ! inside your code. Why avoid it? To be honest I don’t like it at all as it seems somehow wrong to force something that has value even if it's possible that doesn’t have it. Another issue is the sheer number of them, which makes us destroy our clean code with one character.

Option type instead of a question mark

This plugin has some superpowers when it comes to null safety. Instead of defining the type as nullable, you would define it as optional. I will use this plugin in my snippets, so if you want to follow up on some of my code examples you would need to add one more library to your project.

Optional child widget-option

One issue with this approach is that whenever you add a new if or if-else statement, you are increasing the chance to produce a new bug. If you create a new widget that will handle null values, you will not need anymore to use if conditions inside your UI code, it's also not needed anymore to repeat your unit testing, and you will always be able to change your code in one place.

A widget that uses the nullable value

If the child widget depends on a value you need to check with if/else and decide which flow to use based on the if statement, it would look something like this:

String? value = 'someNullableValue';

if (value != null) {
return Text(value);
} else {
return const Placeholder();
}

The above approach leads to possible dirty code, more tests, additional checks, and a lot more complexity. Instead, generalizing the null check will reduce your coding time, and you won’t need to wonder whether you need to check for nulls or not.

class OptionWidget<T> extends StatelessWidget {
const OptionWidget({
required this.option,
required this.builder,
this.emptyChild = const SizedBox(),
Key? key,
}) : super(key: key);

final Option<T> option;

final Widget Function(BuildContext context, T value) builder;

final Widget emptyChild;

@override
Widget build(BuildContext context) {
return option.isPresent ? builder(context, option.value) : emptyChild;
}
}

Calling this newly created wrapper widget will look something like this:

String? value = 'someNullableValue';OptionWidget<String>(
option: Option.of(value),
emptyChild: const Placeholder(),
builder: (_, val) {
return Text(val);
},
)

Values that have already been checked for null

I would call this a Flutter SDK limitation, but hopefully, in the future, the Dart compiler will be better suited to handle this case. For example, please have a look at this code snippet::

void main() {
const instance = ExampleClass();

late String variable;

if (instance.description != null) {
variable = instance.description;
// error from compailer:
// A value of type 'String?' can't be assigned to a variable // of // type 'String'
}
}

class ExampleClass {
const ExampleClass({this.description});

final String? description;
}

There are two different solutions for this case:

a) Create a new finallocal variable. In this way the compiler knows that the value cannot be changed again to a null value

void main() {
const instance = ExampleClass();
final description = instance.description;

late String variable;

if (description != null) {
variable = description;
}
}

b) Use lifting 🏋️‍♀️

Create one extension (it can be of course a separated method), like this one:

extension LiftGenericExtensions<T extends Object> on T? {
T get liftNonNull {
try {
assert(this != null);
return this!;
} on AssertionError catch (e) {
throw Exception(
'It was not possible to lift this object ${e.toString()}',
);
}
}
}

This getter will simply do an assertion check, and if it passes, it will force the value from the object to be a non-nullable value. For sure this getter will remove all exclamation marks inside your code.

Bellow, you will find an example of a calling getter from an extension

void main() {
const instance = ExampleClass();

late String variable;

if (instance.description != null) {
variable = instance.description.liftNonNull;
}

// consider try-catching ;]
}

Note: As stated inside the comment, please consider wrapping this code inside try-catch, as it can happen that you forgot to add check for null value before calling a getter from the extension.

The reason why the Dart compiler cannot recognize already checked values is described here.

A value that should be converted from null to some value

In some cases you may want to convert a property that has a null value to some other value, for example, if the variable is null, convert it to a N/A string value. For doing so, we would just define one simple extension on our nullable String type, as described in the following example:

void main() {
const instance = ExampleClass();

print('value: ${instance.description.valueOrEmpty}');
}

extension Extension on String? {
String get valueOrEmpty => this ?? '';
}

Calling functions on the nullable instance

It’s not possible to call a function that has a nullable type without asserting its value. For example:

void main() {
const instance = ExampleClass();

instance.callback();
// The function can't be unconditionally
// invoked because it can be 'null'
}


class ExampleClass {
const ExampleClass({this.callback});

final VoidCallback? callback;
}

To resolve this, you can do a “traditional” way of checking:

void main() {
const instance = ExampleClass();

final fun = instance.callback;

if (fun != null) {
fun();
}
}

Or, just simply, do the following:

void main() {
const instance = ExampleClass();

instance.callback?.call();
}

Abstraction — NoOp and Implementation

When methods are nullable, it's relatively easy as NoOp class methods will always return null .

abstract class Feature {
bool? boolMethod();
}

class FeatureImplementation extends Feature {
@override
bool? boolMethod() => true;
}

class FeatureNoOp extends Feature {
@override
bool? boolMethod() => null;
}

Conclusion

I would love to hear from you in the comment section about what is giving you a headache regarding this topic. Maybe we can resolve it together and help others. Also, if you have any advice or experience on how to handle this issue in a different way, please let me and others know.

--

--