Deep Dive Into Null and Null Safety in Dart
Idris Idris
Posted on August 25, 2022
Introduction
You are in a class and a teacher walks in, asks for a student to assign a task but the student is absent. The teacher can either call another student for the task or ask someone to relay the task's message to the student whenever he/she gets back. This is a model for handling a request with absent recipient.
This analogy can also be modelled in our codes where a variable could either be absent at a point of call or assigned to a value at that point. The ability to handle such case effectively is paramount for every developer.
In this article, we'll be looking at Null and Null safety in Dart.
We will specifically cover the following:
- Introduction to Null and Null safety in Dart
- Nullable and Non-nullable Dart datatypes
- Null-aware operators
- The
late
keyword
Having mentioned that, let's start:
Null and Null safety in Dart
Null, as a programming concept, simply mean “absence of value”. Having a data type to denote this absence is useful. Let's take a scenario where people are asked to fill their full names in a form. It is possible for some of these people not to have middle names. In a case like this, we want to make an expression that this field could contain a value of String, or it may have no data at all.
The Null class in Dart provides this null value. Imagine there’s no option for null values, we could simply assign an empty string ( “” ) to represent the absence of a middle name. But then, we would need to specify this with a comment for our colleagues to understand.
String middleName = “”;
Using an empty String such as this works but we would have to tell other colleagues about it. What if we can actually set middleName
to the null that it is?
String middleName = null;
This is very clear to everyone that there is no value for middleName
, and based on this, we can easily decide what should happen when the value of the middleName
is null.
The code above will work just fine in versions of Dart prior to 2.12. However for versions above this, an error will be thrown (at compile time).
A value of type ‘Null’ can’t be assigned to a variable of type ‘String’
Before the introduction of Null safety, Dart allows us to pass in values that could be null, calling a method on it on compile time (i.e while typing the code) without throwing an error. This could be dangerous at some point because if that value is eventually null, then our code breaks at runtime. We don’t want our users to see our flaws, right?
Let’s take an example:
Assuming we wish to check the length of our middleName
variable, we can define a function:
int capitalize(String middleName){
return middleName.toUpperCase();
}
Let’s try this code in our main.dart file, in the main function.
void main(){
print (capitalize(‘Adeyemi’)); // ‘ADEYEMI’
print(capitalize(‘learning flutter’)); // 'LEARNING FLUTTER’
}
This works just fine as long as we do not pass null as a value in the capitalize function. Once we pass null as a parameter to capitalize, in Dart <2.12 (before null safety), the code doesn’t throw an error on compile time but if we run it, the code breaks with an error.
NoSuchMethodError: The getter ‘length’ was called on null.
As much as possible we do not want our users to see this, and we might not be able to catch it since it doesn’t appear before running the code. This is why we need null safety. If the same code as above is written in Dart version >2.12 (i.e null safe version), an error is thrown instantly when null is passed as an argument to capitalize.
print(capitalize(null));
Error shown:
The argument type ‘Null’ can’t be assigned to the parameter type ‘String’
This change eliminates the possibility of being surprised by null, hence prevents a number of errors. In short, Null safety help keep us safe from the problems of forgotten null occurrence.
Nullable and Non-nullable Data Types
With the advent of Dart null safety, Dart guarantees us that all its data types are non-null by default. This means that all the data types must have a value, unless we want it to be otherwise (i.e we want it to be null).
Non-nullable types are the common easy-to-understand types that we know, they are declared without using the ?
(question mark) at the end.
Examples of non-nullable types:
- String: “Flutter”, “Dart”, ‘Google’
- int: 2, 35, 7, 22, 100
- double: 0.003, 22.7, 3.142
- bool: true, false
- Sport: basketball, football
This means that all these values are acceptable. For example, we can write in our editor.
int myAge = 20;
bool userRegistered = true;
String myWord = “I love Flutter”;
Sport football = Sport(name: “Football”, players: 11);
All these would work fine as they are not null. But when we try:
String myName = null; //A red underline shows on compile time
A compile-time error is shown immediately.
A value of type 'Null' can’t be assigned to a variable of type ‘String’.
But how then do we assign null to a variable?
Picture Reference: express.co.uk
This is where Dart nullable types comes in.
To make a type nullable, all we have to do is to add a ‘?’ in front of the type.
int? myAge = null; //This works
A nullable type allows null value in addition to its original type. This means that the type ‘double?’ can take values such as 1.42, 23.5, null, 0.005 amongst others.
The question mark (?) in front of the type is not an operation on the data type, rather it indicates a whole new datatype.
String? is a whole different datatype from String.
Reference: Dart doc
Every non-nullable Dart type has its corresponding nullable type
Also, we can say the non-nullable type is a subtype of its corresponding nullable type. That is, int is a subtype of int? since int? can take a value of int.
Finally, if a nullable variable is not assigned initially, it is given a type of Null.
int? year;
String? middleName;
If this is printed on console:
print(‘Year is $year’);
print(‘Middle name is $middleName’);
We have:
Year is null
Middle name is null
Null Aware Operators
One of the major issues before the introduction of null safety is how easy it is for developers to forget to handle values that might turnout to be null. With null safety now, Dart ensures that developers cannot perform specific operations on nullable values unless they’ve handled the possibility of null.
For example:
int? age;
print(age.isEven);
Instantly, on compile-time, Dart throws an unchecked_use_of_nullable_value error stating:
The property ‘isEven’ can’t be unconditionally accessed because the receiver can be ‘null’
Then it goes ahead to suggest:
Try making the access conditional (using ‘?.’) or adding a null check to the target (‘!’).
We can, with this improvement, easily handle the compile-time error instead of waiting until this crashes our app.
Dart provides a set of operators to help handle the possibility of null values, these are known as the null-aware operators.
The null-aware operators are as follows:
- If-null operator (??)
- Null-aware access operator (?.)
- Null assertion operator(!)
- Null-aware assignment operator (??=)
- Null-aware cascade operator(?..)
- Null-aware index operator (?[ ])
Now let’s see how these operators work
If-null operator (??)
If we have:
int? numberOfYears;
final age = numberOfYears ?? 0;
This is simply read as “If numberOfYears is not null, then set age equals numberOfYears, if it is null, set age equals 0”
It should be noted that using that using ?? ensures that the value of age can never be null. Hence, Dart infers the variable as int, not int?
From above, using the ?? operator is same as writing:
int? numberOfYears;
int? age;
if(numberOfYears != null){
age = numberOfYears;
}else{
age = 0;
}
That is about 6 additional lines of code instead of the one-liner using the ?? operator.
Null-aware access operator (?.)
Earlier, we tried age.isEven, and we saw that Dart give us an error at compile-time. To by-pass this, one of the suggested way is to use the ?. operator on age. That is, age?.isEven
.
This can be explained simply as “If age is null, return null, otherwise if age has a value, return true/false if it’s even or not”.
The operator ?. should be used when we’re not totally sure if the variable will be null or not. Hence, if it turns out to be null, we can easily handle it, if it isn’t null too, we handle it accordingly.
Null assertion operator (!)
Unlike the Null aware access operator (?.), whenever we’re so sure that the variable we’re handling is definitely not null, we could use the ! operator (bang operator).
age!.isEven
In this case, we’re saying that “ Yes, I am sure age is not going to be null, so just check if it’s even”. This comes at a cost though, because if age eventually turns out to be null, our app will crash at runtime.
The ! operator should be treated as a dangerous operator and only used when we’re very sure our variable will not be null. By using this operator, we simply tell Dart that “look, I am going out of null safety on this variable because I’m sure it’s null, so let me handle it myself”.
Null- aware assignment operator (??=)
This works like the if-null operator (??), but we use the ??= when updating the same value.
For example:
String? country;
When it’s time to use the address variable, it will be wise to check if an address has been assigned earlier. This can be done using the ?? operator as:
country = country ?? “Nigeria”;
But since it’s the same variable we want to update, we can simply write:
country ??= “Nigeria”;
In simple terms, the ??= operator simply says “If country is null, set country to “Nigeria”, otherwise, retain the current value of country”.
Null-aware cascade operator (?..)
In Dart, the cascade operator (..) allows us to call multiple method on the same object at a go OR help set multiple properties on the same object.
For example, given a class Sport like this:
class Sport{
String? name;
int? players;
}
If Sport class isn’t nullable, we can us the cascade operator like this:
Sport sport = Sport()
..name = "Football"
..players = 11;
A common use case of the null-aware cascade operator is on the Path object, during painting in Flutter.
Path? path;
Path
?..moveTo(0, 0)
..lineTo(0, 4)
..lineTo(4, 4)
..lineTo(4, 0)
..lineTo(0,0);
The null-aware cascade operator can be short-circuited, you only need to use it for the first time in the chain and if the variable (path) is null, all operations in the chain do not get called.
Null-aware Index Operator (?[])
In Dart, indexing is used to access an element of a List
List<String> myColors = [“Blue”, “Green”, “Red”];
The indexes begin at 0, hence to call out “Green”, we do this:
var green = myColors[1];
Now with null-safety, we know that a List might be null, hence, to index an item of that List we have our helper ?[] operator.
Let’s see an example:
List<String>? myColors = [“Blue”, “Green”, “Red”];
Even though, the List is nullable but since we’ve assigned a value to it, it has been promoted.
Setting myColors to null.
myColors = null;
Now, when we try to get the “Green” value again, we use the null-aware index operator as thus:
String? green = myColors?[1];
This, in literal term means “if myColors is null, assign null value to green. If otherwise, assign the 1th index of myColors to green”.
If we try to access a value from a null list before null-safety, it would have resulted in our app crashing. But again, thanks to null-safety.
The late
keyword
Sometimes, we want to make our variable non-nullable but we do not have the value to initialize it. The late keyword helps us to promise Dart that a value will be green to the variable before its usage, but that will be later. Using the late keyword means that the variable’s value won’t be calculated until we access the variable for the first time. This is called lazy initialization. There are different ways by which we could later assign a value to that variable before using it like using the initState (for a StatefulWidget) or calling a function that returns that value.
Whenever we try to access a variable that we specific with the late keyword without later assigning a value to it, we broke our promise, hence we get a LateInitializationError
Example:
class Sport{
late String name;
}
And then we have something like this somewhere else.
Sport sport = Sport();
print(sport.name) //an error would be thrown since we have not initialized name.
LateInitializationError: Field ‘name’ has not been initialized
Conclusion
In this article, we have looked at the concept of Null and the beauty of Null safety. If used appropriately, Null safety can help you in writing better and less error-prone codes.
Additional Resources
Posted on August 25, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.