Dart Null Safety

Dart Null Safety

What you need to know to get started

ยท

11 min read

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

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.

ย