Understanding null-safety in flutter
Tags: flutter, dart, programming
Published: 2022-01-12 02:54:18

## Introduction

Null-safety has caused a series of problems for developers, especially in dart, where it was introduced as an optional feature and developers have had to slowly migrate to it usually not fully understanding it. This post is here to change that.

## What exactly does null-safe mean?

To understand what null-safety is, first we need to understand what null is.

Null is a keyword in dart that means *nothing*, it is used to describe a variable with no value assigned. It should be used for variables that don't always have a value (for example a `currentUser` variable could be null if no user is signed in), a nullable variable is different from a late variable in that a late variable starts with no value but once it gets a value it can never go back to having no value at all.

Late variables are also different in that they are never null, an uninitialized null variable will not be null, and trying to check if it is will throw an error
```dart
class MyClass {
  late int myVar;

  void myMethod() {
    if (myVar != null) { // this if is reading an unasigned late variable
      print(myVar);
    }
  }
}

void main() {
  final myClass = MyClass();
  myClass.myMethod();
}
```
The above will throw an error while attempting to read `myVar` to see if it's null, you will also get a lint warning letting you know that `myVar == null` will never be true.

On the other hand, a nullable variable can be used when planning to read the value before assigning it or when planning to assign null to it more than once.

Null-safety then, makes sure we only ever add null checks when necesary. Please take a look at the following example:
```dart
void main() {
  int x = someFunction();
  if (x != null) {
    print(x);
  } else {
    print('x is null');
  }
}
```
In the example above, the code `if (x != null)` is redundant, because we declared `x` to be a null-safe variable (`int`, not `int?`), meaning that either `someFunction` will never return null, or we already got some sort of compilation error when assigning, either way, we don't need an if statement to know `x` will never be null.

In case we want to work with a nullable variable, we can make it so by adding a `?` after the type declaration:
```dart
void main() {
  int? x = someFunction(); // some function might return null
  if (x != null) {
    print(x);
  } else {
    print('x is null');
  }
}
```
Above `x` could be null assuming `someFunction` can return null, so the if statement is no longer redundant.

## Null-safety operators

Along with nullable variables, we have some useful operators, namely `?.`, `??` and `!`, (the last one was added with null-safety, but the rest already existed).

### The null aware operator

The first of the three operators I want us to take a look at is the `?.` operator, we'll call it the null-aware operator. The null aware operator can be used to access a nullable variable's instance fields, take a look at the following example:
```dart
String? myString = myFunction();
if (myString.isNotEmpty) {
  print("My string wasn't empty");
}
```
The above code will not compile. `myString` could be null, yet we are trying to get `isNotEmpty` but **if `myString` is null**, we will get an error, so we can't possibly compile this, this is where the `?.` operator comes into play, the `?.` operator will return null when used on a null value and it will read a property when used on a non-null value, take a look:
```dart
String? myString = myFunction();
if (myString?.isNotEmpty == true) {
  print("My string wasn't empty and it wasn't null either!");
}
``` 
In the new example, we used `?.`, which means `isNotEmpty` will be null if `myString` is also null. With that in mind, I added a `== true` after it to make sure that the if statement doesn't run if `myString` is null

### The if-null operator

The next operator is the `??` or if-null operator. It is quite a simple operator and as the name implies it can be used to provide a default value in case of null, take a look at this simplified example:
```dart
int? a = someNullableFunction();
int b = a ?? 0;
```
The above code can also be understood like this:
```dart
int? a = someNullableFunction();
int b;
if (a == null) {
  b = 0;
} else {
  b = a;
}
```
That is to say, the if-null operator will return the left operand if it isn't null and the right operand if the left is.
Of course, the operand can be linked together to form some confusing looking code:
```dart
int? a = someFunction();
int? b = someFunction();
int? c = someFunction();
int d = a ?? b ?? c ?? 0;
```
Above, `d` will first try to assign itself to `a`, but if `a` is null, it will attempt to use `b`, if `b` is null it will use `c` and if `c` is null it will use 0.

The if-null operator can be used in conjunction with the null-aware operator for some pretty nice results, take a look at this slightly more realistic example:
```dart
@override
Widget build(BuildContext context) {
  List<int>? myList = _generateList();
  return ListView.builder(
    itemCount: myList?.length ?? 0,
    itemBuilder: (context, index) { ... },
  );
}
```
Above, we don't know if our list of items will be null, but we can save ourselves a ternary by using null-awareness and if-null operators.

Last but not least, this operator can be used in conjunction with the `=` operator to asign if null:
```dart
int? a = someFunction();
a ??= 0;
```
Above, `a` will be 0 if it was null, but it will keep its old value otherwise, as with other assignment operators (like `+=`), `??=` is just a synonym of:
```dart
int? a = someFunction();
a = a ?? 0;
```
Also, I do want to note that in this example we should just do this:
```dart
int a = someFunction() ?? 0;
```
which is more concise.

### The null check operator

Finally, the `!` or null check operator (not to be confused with the not operator, as that one is used before a boolean expression and the null check operator is used **after** a nullable expression).

It was introduced along with null-safety to dart and it has quite a simple function, it can be used to turn a nullable value into a non-null one:
```dart
int? a = myFunction();
int b = a!;
```
Using this operator we can easily assign `a` to `b` even if `a` is nullable and `b` is not.

Now I hear you asking what's the catch? What happens if `a` is null? And I hear you!

The answer is very simple, you get a null-check error, meaning the program will crash if you use a `!` operator on a null value, you should not use this operator on a value that might be null, it is meant to be used only if you **know** a nullable value isn't null, here is a very simple example:
```dart
class MyClass {
  int? _value;
  
  int? get value => _value;
  
  void setValueIfNull(int val) {
    _value ??= val;
  }
}

void main() {
  MyClass c = MyClass();
  c.setValueIfNull(1);
  int x = c.value!;
}
```
Above, we as the programmers know that `MyClass.value` will not be null right after a call to `MyClass.setValueIfNull()`, because said method will assign some value, however dart has no way of knowing this, so we can help dart out by using the `!` operator on `MyClass.value` when reading it. Here is another, simpler example:
```dart
int? a = someFunction();
if (a != null) {
  int b = a!;
}
```
Now because we are inside an if statement that made sure a isn't null, we can use the `!` operator freely, in reality, this code will work, but we will get a warning letting us know that using `!` is not necesary because even dart is smart enough to know that `a` is not null right after checking it, we will talk more on this topic in a second.

I think it is also worth mentioning that the null check operator can be used like this `!.` to access a nullable variable's fields. Here is a slightly more realistic example:
```dart
@override
Widget build(BuildContext context) {
  return FutureBuilder<List<int>>(
    future: _getNumbers(),
    builder: (context, snapshot) {
      if (snapshot.hasData) {
        return ListView.builder(
          itemCount: shapshot.data!.length,
          itemBuilder: ...
        );
      }
      return CircularProgressIndicator();
    }
  );
}
```
As you can see, because we knew `snapshot.data` wouldn't be null, we can use `!` like this `snapshot.data!.length`

## Type escalation

As I mentioned before if you put this code here: 
```dart
int? a = someFunction();
if (a != null) {
  int b = a!;
}
```
into the dart compiler, you will get a lint warning letting you know that there is no need to use `!` because `a` will never be null. This is what we refer to as type escalation, in this context, we have already made sure that `a` will never be null, so for dart, the type of the variable `a` has escalated from `int?` to `int`.
Keep in mind the real type of the variable is still `int?` so we can still assign null, in this case, the type escalation will only work inside the if statement and last until we assign `a` again.

Type escalation works outside of null-safety, but I believe it is especially useful when working with nullable values.
```dart
dynamic myVariable = someDynamicFunction();
if (myVariable is String) {
  print(myVariable.substring(1)); // you should get intellisense here
}
```

It is also worth mentioning type escalation only works on local variables, the reason being it is hard to predict what getters will return, take a look at this example:
```dart
class MyClass {
  void myMethod() {
    if (myVar != null) {
      int currentVar = myVar;
    }
  }

  int? get myVar {
    int myNum = someRandomNumberGenerator(min: 0, max: 100);
    return myNum > 50 ? myNum : null;
  }
}
```
As you can see above the global getter `myVar` will return `null` 50% of the times it is called, meaning it won't necessarily be safe after making a check, so type escalation can't happen **and the above code will give an error**.

That's it! You are done reading this article, if you are interested in continuing to read about null-safety, I recommend you go to [dart's official null-safety page](https://dart.dev/null-safety) which also contains tons of awesome resources to level up your null-safety skills!