Dart's null safety feature was released a while back (3rd march, 2021) in v2.12.0 and for some (like people who previously used languages with null safety e.g. kotlin and swift) it seemed like a long overdue update and it only made sense to adopt it immediately.
For me that wasn't the case at all and it has taken a while for me to get myself familiar with it. If you're still yet to embrace null safe dart then you should try to stick to the end and hopefully it wouldn't seem so strange anymore.
The following topics would be covered in this article:
- What is Null Safety
- How It Works
- Nullable vs Non-Nullable Types
- Working With Null Safety
- Other Important things to Note
- Conclusion
What is Null Safety
First thing to know is that dart is a statistically typed language.
With statistically typed languages, you are required to declare the data types of your variables unlike dynamically typed languages like python.
It also means that the dart compiler is able to detect problems related to data types in your code without having to run it first (i.e. at compile time). While for dynamically typed languages the data type is checked when the app is running (i.e. at runtime).
// Dart example
int number = 5;
// Python example
number = 5
Let's take a look at a function that takes a number and returns true if the number is even and false if it isn't. And don't worry I haven't forgotten this is about null safety.
// Dart example
isEven(int number) {
return number % 2 == 0;
}
void main() {
print(isEven(8)); // true
print(isEven(9)); // false
print(isEven('hey')); // doesn't compile until you fix it by passing an int value
}
// Python example
def isEven(number):
return number % 2 == 0
print(isEven(8)) // true
print(isEven(9)) // false
print(isEven('hello')) // compiles but crashes at runtime
You now see how defining data types in a statistically typed language like dart helps the compiler to detect and prevent errors and the earlier we find these errors the earlier we can fix them which is a good thing. Right?
Now take a look at the same example running on dart without null safety.
// Dart example
isEven(int number) {
return number % 2 == 0;
}
void main() {
print(isEven(8)); // true
print(isEven(null)); // compiles but crashes at runtime
}
The compiler doesn't detect anything wrong with this but it crashes at runtime because The method '%' was called on null
.
Wouldn't it be nice if the compiler also lets you know that something is not right beforehand? The same way it did when we tried using isEven('hello'). That would have prevented this issue and the code would have run safely ๐.
That's null safety, its able to prevent null from going places its not wanted, but that doesn't mean you wouldn't be able to use null still.
The above code wouldn't compile in null safe dart because The argument type 'Null' can't be assigned to the parameter type 'int'.
. Cool huh?
How it works
So what changed exactly?
Before null safety dart treated Null as a subtype of every other dart type. So basically Strings, int, List, Map etc. could all be assigned null as a value. Which is not a bad thing as null is very important and used to define an empty state.
And also, every variable you define without assigning a value has a default value of null. Same way every function with a return type but no return statement get evaluated to null.
// Dart without null safety
List items = null; // null
List items; // has a default value of null
bool isEven(int number) {
number % 2 == 0;
} // evaluates to null, though a warning is given at compile time it still compiles.
Like I said earlier having null isn't bad. The problem happens when we try to access the properties and methods of the defined type. So basically a List can have null as it's value but null doesn't have what it takes to be a List because with List we can do things like .length, .add(), .remove(), .map() and so much more but null doesn't have any of these.
// Dart without null safety
void main() {
List items;
print(items.length); โ
}
The above code would compile but crashes at runtime because The getter 'length' was called on null.
There's an easy fix for this which is the null aware operator ?.
. So we can do this instead
void main() {
List items;
print(items?.length); โ
}
And instead of crashing, null gets printed on the console. What happens is, at runtime when dart encounters the ?.
it CHECKS to see if items has been given a value (i.e. a list) before calling length on it. In our case the value of list is null so dart automatically assigns null to the evaluation on the right side of ?.
Now for a statistically typed language, checks like this should happen at compile time not at runtime.
With null safety, null is no longer treated as a subtype of all other dart types. So if you tell dart a variable is going to be a String you MUST keep your word.
// Dart with null safety
int number = null; โ
int number; โ
List items = null; โ
List items; โ
bool isEven(int number) {
number % 2 == 0;
} โ
Nullable vs Nun-Nullable Types
Null is still very important for dart programs, especially for things like optional parameters so even with dart null safety you can still work with null. But to do this you have to be explicit with dart. You have to let it know you want to work with null types.
To let dart know you want to be able to work with null types you add ?
to the type definition making it a nullable type. This lets dart know It would either be null OR the underlying type, nothing else.
int? number; // This works because it is nullable
int? number = 0; // This also works because it is of type int
Without the ?
dart expects to find a value that matches the type (Non-nullable)
int number = 0; // This works because it is has a value of type int
int number; // This wouldn't work because it expects an int value and nothing else
The important thing to note is that a nullable type WOULD NOT be accepted where a non-nullable type is expected, but a non-nullable type WOULD be accepted where a nullable type is expected. ๐ฅด
It sounds better in code form so take a look ๐
int first = 1; // non-nullable, expects an int at all times
int? second = 2; // nullable, expects an int OR null
first = second; // compile error, because second COULD be null
first = null; // definite NO, because first CANNOT be null
second = first; // this works, because first is an int
second = null; // this also works because it accepts null
There are ways for nullable and non-nullable types to interact peacefully so lets look at that next.
Working With Null Safety
There are a few functionalities dart provides for us to easily work with null safety.
Null Aware Operator
The good old ?
.
We talked about this already How It Works.
Assertion Operator
Take for instance we have a Person
class.
class Person {
String? name;
String? title;
void setName(String val) {
name = val;
}
}
void main() {
Person person = Person();
person.setName('ifeanyi');
print(person.name.length); โ
}
If you look at the program above you can see that we would definitely set the name variable before we get to the print statement, but yet, we cant access the .length because name
COULD be null and dart's static analysis cant tell name has been set because its more concerned with the name type (String?), which actually says name
could be null. But in this case we are 100% sure it's not.
For something Like this we can let dart know, that we know something it doesn't by using the assertion operator !.
print(person.name!.length); โ
Think of the assertion operator as a shorthand for type casting. Actually that is what it is. So what that !
does is same as casting a nullable type (String?) to the underlying nun-nullable type (String) like so:
print((person.name as String).length);
Late Variables
In a case where we know before hand that a variable would get assigned a value before it is used like in the example above we can use the late
keyword. So the person class becomes
class Person {
late String name;
// ...
}
void main() {
Person person = Person();
person.setName('ifeanyi');
print(person.name.length); โ
}
So we made the name
a non-nullable String and used the late keyword to tell dart not to worry about where it's value is for now, instead it should worry about it when it actually needs it.
Therefore it compiles safely and if for some reason name
isn't assigned before we use it then an error is thrown at runtime.
LateInitializationError: Field 'name' has not been initialized.
Required Parameters
If you use flutter you probably know how required named parameters work.
With dart's null safety the type checker requires all optional parameters to either have a nullable type or a non-nullable type with a default value.
void introduce({String name = 'john', String? hobby}) {
if (hobby != null) {
print('I am $name. I love $hobby');
return;
}
print('I am $name');
}
void main() {
introduce(); // I am john
introduce(hobby: 'coding'); // I am john. I love coding
introduce(name: 'ifeanyi', hobby: 'coding'); // I am ifeanyi. I love coding
}
In the above example name
must have a default value because it is nun-nullable.
if you want to have a named parameter with a non-nullable type and no default value you would have to use the required keyword which would require the caller of the method to always pass the parameter.
void introduce({required String name, String? hobby}) {
if (hobby != null) {
// ...
}
void main() {
introduce(hobby: 'coding'); โ // name must be passed in
introduce(name: 'ifeanyi'); // I am ifeanyi
introduce(name: 'ifeanyi', hobby: 'coding'); // I am ifeanyi. I love coding
}
Other Important things to Note
So while doing research for this article I came across some cool things I've been using but didn't actually take note of what was going on.
Definite assignment analysis
Dart null safety is more flexible for local variables. So we can define a non-nullable variable without giving it an initial value and as long as the variable gets assigned before it is used, there's no problem.
Take for instance:
String wordValue(int number) {
String result; // No problem here โ
switch (number) {
case 0:
result = 'zero';
break;
case 1:
result = 'one';
break;
case 2:
result = 'two';
break;
default:
result = 'no idea';
}
print(result.length); โ
return result; โ
}
Because the value of result would definitely get assigned before it is used we don't have an issue.
This is because darts static analyzer is able to track the assignment of local variables through all control flow paths. And as long as the variable is assigned on every path before the variable is used, the variable is considered initialized.
String wordValue(int number) {
String result; // No problem here โ
switch (number) {
case 0:
result = 'zero';
break;
default:
// no default assignment
}
print(result.length); โ
return result; โ
}
void main() {
String five = wordValue(5); // This wouldn't have a result without the default block
}
Without the default assignment of the switch statement It wouldn't compile because The non-nullable local variable 'result' must be assigned before it can be used.
.
Type Promotions
Dart's null safety also supports type promotions on null checks.
Let's first understand what type promotion in dart is.
In dart there is the dynamic
key word which can hold any value, be it int, String, List etc.
Imagine we have a function that takes a dynamic parameter and returns an int
int length(dynamic value) {}
We can check value
type at runtime and determine how to handle it. For example:
int length(dynamic value) {
if (value is List) {
return value.length;
}
// ...
}
When value is List
evaluates to true, dart automatically assigns the type to be a List which is why we can access the properties and methods of a list like .length.
In other words dart promoted value's type from type dynamic to type List.
Same thing happens with null checks.
int length([List? value]) {
if (value != null) {
return value.length; โ
}
return 0;
}
When the null check evaluates to true, value
gets promoted from nullable List? to non-nullable List.
Thanks for reading
That's it for now. Congratulations if you made it this far ๐๐
If you liked this article you should definitely check out This Article by Bob Nystrom because this is me explaining what I learnt from it in my own words.
Dart's null safety is supported from SDK version 2.12.0 and above.
Happy coding.