DEV Community

Cover image for Understanding null-safety in flutter
h8moss
h8moss

Posted on

Understanding null-safety in flutter

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

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();
}
Enter fullscreen mode Exit fullscreen mode

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:

void main() {
  int x = someFunction();
  if (x != null) {
    print(x);
  } else {
    print('x is null');
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

void main() {
  int? x = someFunction(); // some function might return null
  if (x != null) {
    print(x);
  } else {
    print('x is null');
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

String? myString = myFunction();
if (myString.isNotEmpty) {
  print("My string wasn't empty");
}
Enter fullscreen mode Exit fullscreen mode

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:

String? myString = myFunction();
if (myString?.isNotEmpty == true) {
  print("My string wasn't empty and it wasn't null either!");
}
Enter fullscreen mode Exit fullscreen mode

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:

int? a = someNullableFunction();
int b = a ?? 0;
Enter fullscreen mode Exit fullscreen mode

The above code can also be understood like this:

int? a = someNullableFunction();
int b;
if (a == null) {
  b = 0;
} else {
  b = a;
}
Enter fullscreen mode Exit fullscreen mode

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:

int? a = someFunction();
int? b = someFunction();
int? c = someFunction();
int d = a ?? b ?? c ?? 0;
Enter fullscreen mode Exit fullscreen mode

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:

@override
Widget build(BuildContext context) {
  List<int>? myList = _generateList();
  return ListView.builder(
    itemCount: myList?.length ?? 0,
    itemBuilder: (context, index) { ... },
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

int? a = someFunction();
a ??= 0;
Enter fullscreen mode Exit fullscreen mode

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:

int? a = someFunction();
a = a ?? 0;
Enter fullscreen mode Exit fullscreen mode

Also, I do want to note that in this example we should just do this:

int a = someFunction() ?? 0;
Enter fullscreen mode Exit fullscreen mode

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:

int? a = myFunction();
int b = a!;
Enter fullscreen mode Exit fullscreen mode

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:

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!;
}
Enter fullscreen mode Exit fullscreen mode

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:

int? a = someFunction();
if (a != null) {
  int b = a!;
}
Enter fullscreen mode Exit fullscreen mode

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:

@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();
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

int? a = someFunction();
if (a != null) {
  int b = a!;
}
Enter fullscreen mode Exit fullscreen mode

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.

dynamic myVariable = someDynamicFunction();
if (myVariable is String) {
  print(myVariable.substring(1)); // you should get intellisense here
}
Enter fullscreen mode Exit fullscreen mode

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:

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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 which also contains tons of awesome resources to level up your null-safety skills!

Top comments (0)