With the release of Dart 2.12 & Flutter 2, sound null safety is finally here!
I haven’t had the time to check it out in more detail, but now that I’ve read about the subject, I decided to write a few blog posts about it. I also created a simple Flutter application, which has non-null-safe & null-safe versions, to support this blog post. The blog posts are also meant to function as a kind of cheat sheet that can be used by anyone.
Sound null safety
Sound null safety itself isn’t a new concept, but now we can finally use it in Flutter, as long as Dart SDK minimum version is set to 2.12!
I’ll go through the basics here, but you can find the complete documentation at https://dart.dev/null-safety.
First of all, what does “sound” or “soundness” mean in a null safety context? Well, Google’s definition is that “if an expression has a static type that does not permit null, then no possible execution of that expression can ever evaluate to null”.
The principles that guided Google when they had to make null safety choices were threefold;
- Code should be safe by default
- null safe code should be easy to write
- the resulting null safe code should be fully sound
A good statement can be found from the docs: “It is not null that is bad, it is having null go where you don’t expect it that causes problems”.
When you finally take the step to dive into sound null safety, types in your code are non-nullable by default. That means that variables can’t contain or be null unless you explicitly state so.
With null safety enabled, the ”runtime null-dereference errors turn into edit-time analysis errors”.
That means that we can avoid unexpected runtime errors if a null value was passed somewhere it shouldn’t have been.
As an example, the following variables are non-nullable when opted for null safety:
int amountChecked = 0;
double amountCheckedPercentage = 0.0;
String amountCheckedLabel = ”0.0%”;
int maxChallengesAmount = 0;
To make a variable nullable, we’ll simply insert the “?” keyword after type declaration;
String? title; // declare as nullable, as null indicates that something failed when setting title value
That might look familiar to many, as e.g. Swift, C#, and Kotlin use the exact same nullable declaration logic. It was done intentionally the same way in Dart too, to make it easier for developers to declare nullable types. Jumping between languages can sometimes be a hassle, so it’s convenient if they have as many similarities as possible.
Using “late” keyword
The “seasonJourneyModel” variable that contains all the necessary data for the application shouldn’t be null, otherwise, we can’t do anything with our Flutter app. If we don’t make it null, then the Dart analyzer will complain about possible null problems. We know the variable will be initialized later and used only after that. But how do we tell the analyzer that it won’t be null?
The “late” keyword comes to the rescue! It does just what we want; allows us to initialize our variable at a later point, and won’t complain about possible null problems.
class Controller extends GetxController {
// This variable contains all data related to current season journey
late SeasonJourneyModel seasonJourneyModel;
…
Future<void> _init() async {
…
String jsonString = await rootBundle.loadString(”assets/seasonJourney.json”);
seasonJourney = SeasonJourney.fromJson(json.decode(jsonString));
…
}
…
}
However, if we use the variable before it’s an initialization, then a “LateInitializationException” will be thrown during runtime, which ultimately crashes the app.
We can also use the final keyword late, and we don’t have to initialize that variable straight away. We can do that later at our choosing, but we can only assign it once, which is checked at runtime. Calling it twice will cause an exception to be thrown. Using final with late is a great way to model your state that gets initialized at some point and is immutable afterward.
late final SeasonJourneyModel seasonJourneyModel;
According to Google docs, the late keyword has two effects:
- The analyzer doesn’t require you to immediately initialize a late variable to a non-null value.
- The runtime lazily initializes the late variable. For example, if a non-nullable instance variable must be calculated, adding the late modifier delays the calculation until the first use of the instance variable.
Using variables and expressions
The Dart analyzer will generate errors when it discovers a nullable value somewhere where a non-null value is required (explicitly or implicitly). It can usually recognize nullable types in variables or expressions inside a function that can’t have a nullable value.
It is important to handle null values when using nullable variables or expressions. Some possible ways to handle null values can be e.g. if statement or for example the ??, ?. or ! operator;
Using ?? operator:
// value is set as object property if it’s not null, otherwise ”unknownSeason”
title = seasonJourneyModel.title ?? ”unknownSeason”;
Null check:
List<Widget> _getDrawerItems(Controller controller) {
List<Widget> items = [];
if (controller.seasonJourneyModel.chapters == null) {
return items;
}
…
return items; // Never null!
}
You should be a bit more careful with the ! operator, since it may throw an exception if the value is null, even the documentation says that ”If you aren’t positive that a value is non-null, don’t use the ! operator”, so use it with care!
// This will throw an exception if possibleNullValue is null
int? possibleNullValue = 1;
int value = possibleNullValue!;
If you need to change the type of a nullable variable, you can just use the as typecast operator.
This example contains as operator to convert a num? to an int?:
print(possibleNullValue()); // prints “null”
int? possibleNullValue() {
num? age;
return age as int?;
}
If you change the function to non-nullable and remove nullable declaration from as operator, then an error will be thrown:
// throws error
print(possibleNullValue());
int possibleNullValue() {
num? age;
return age as int;
}
When you’ve opted into null safety, you can’t use the member access operator (.) anymore, since the operand might be null. You can instead use the null-aware version of that operator, ?.:
selectedChapter?.challenges!.forEach(…
In the next part, I’ll be covering collections among other things. Stay tuned!