Understanding null safety in Dart
What is sound null safety and how to achieve it in Dart.
Ever wondered what is all this fuss about null safety? In this article, I'll provide a brief introduction to null safety in Dart and share code examples for a better understanding.
Introduction
Null safety helps you solve one of the most common errors in software development: NullPointerException.
The main issue with the NullPointerException
is that it can happen in almost any part of the app if not well handled. Checking for nulls every now and then throughout your app isn’t the best solution either. As your app grows, it can become even harder to handle the null values. For these reasons and more, handling nulls at the language level is probably the best way to avoid the infamous NullPointerEception
.
To build on this idea, the Dart team introduced sound null safety. This means that variables are considered non-nullable by default. If you don’t give an object null support when you create it, it will never store a null
. As a result, you avoid null reference errors in your code.
Activate Null Safety
To activate null safety, you need to go to your pubspec.yaml file and ensure the SDK version is set to:
environment:
sdk: ">=2.12.0 <3.0.0"
Make sure to run flutter pub get
after updating the pubspec.yaml.
The Dart Type System
Before we move on to creating null safe apps, let's discuss the type system implemented by Dart.
Dart divides types into two groups:
- Nullable types - can contain either a value or null
- Non-Nullable types - can only contain values. Nulls are not permitted
Let's have a look at these types in the next section.
Understanding Sound Null Safety
Now that we have null-safety activated, all our types are by default non-nullable, which means they all require some value.
String name = 'Aditya Sharma'; // This works ✔
String name = null; // Will throw error ❌
Adding a ?
makes the Type nullable and allows us to pass a null value to our name
field.
String? name = 'Aditya Sharma'; // This works ✔
String? name = null; // This works too ✔
Let's now have a look at some real-world scenarios:
1. Null Checks
Null check is a Dart feature that helps you to avoid null related errors.
Let's take an example:
void setName(String name) {
name.toUpperCase();
print(name);
}
void main() {
setName(null); // Throws error ❌
}
In this case, type String is not nullable and thus we cannot store a null inside it.
If we want to pass a null value to setName()
, we change the parameter type to String?
:
void setName(String? name) {
name.toUpperCase(); // Warning here ❌
print(name);
}
void main() {
setName(null);
}
Now, the compiler warns us that we cannot call the upperCase()
on name
as it can also store a null value.
We have 2 ways to fix it:
The null aware accessor
?.
:void setName(String? name) { name?.toUpperCase(); print(name); } // void main() { setName(null); }
Check for null before calling any method on
name
:void setName(String? name) { if (name == null) return; name.toUpperCase(); print(name); } // void main() { setName(null); }
2. Non-nullable Type to Nullable Type Conversion
When converting from Type
to Type?
, there is no problem as Type?
accepts both real value and null value.
void main() {
String name = 'Aditya';
String? newName = name; // Works fine ✔
}
3. Nullable Type to Non-nullable Type Conversion
When converting from Type?
to Type
, the compiler gives us a warning.
void main() {
String? name = null;
String newName = name; // Warning ❌
}
We have 3 ways to fix it:
Check for null before conversion:
void main() { String? name = null; if (name != null) { String newName = name; } }
Using the if-null
??
operator for providing a default value:void main() { String? name = null; String newName = name ?? 'Aditya'; }
Using the Bang operator
!
(also called the null assertion operator):void main() { String? name = null; String newName = name!; }
PS: If you’re sure that an expression with a nullable type isn’t null, you can use a null assertion operator
!
to make Dart treat it as non-nullable. By adding!
just after the expression, you tell Dart that the value won’t benull
, and that it’s safe to assign it to a non-nullable variable.
⚠️: If you’re wrong, Dart throws an exception at run-time. This makes the!
operator unsafe, so don’t use it unless you’re very sure that the expression isn’t null.
4. Late Init
Sometimes, you can’t initialize properties in the constructor, but you’ll define them in other methods of your class. In that case, you mark those properties with late
.
Another advantage of late is lazy initialization. Dart will not initialize late properties until they’re used for the first time. This can be useful during app initialization when an expression is costly or might not be needed.
late String name;
@Override
void initState() {
super.initState();
name = 'Aditya Sharma';
}
Sound Null Safety In a Nutshell
Dart has three main operators to work with null:
- Null-aware operators: The null aware accessor
?.
, which accesses properties of its operand, is an example. If the operand isnull
, then it will not throw an exception. Instead, it shows null text. - Bang operator: Use
!
to cast away nullability. It tries to cast a nullable type to a non-nullable type, throwing an exception if the operand isnull
. - If-null operator:
??
is shorthand for an if-else condition. If the left side of the??
operator is null, then it will use its right side. - Late init: Use
late
on variables when you’re sure you’ll initialize them before using them. Uselate
with class properties.
References
That's all folks! Do let me know what you liked and what can be improved in the comments below! Also, feel free to drop me a line if you don't understand something. ✌
Thanks for reading!