Home Flutter Books Dart Apprentice

7
Nullability Written by Jonathan Sande

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

You know that game where you try to find the item that doesn’t belong in a list? Here’s one for you:

horse, camel, pig, cow, sheep, goat

Which one doesn’t belong?

It’s the third one, of course! The other animals are raised by nomadic peoples, but a pig is a farmer’s animal — it doesn’t do so well trekking across the steppe. About now you’re probably muttering to yourself why your answer was just as good — like, a sheep is the only animal with wool, or something similar. If you got an answer that works, good job. Here’s another one:

196, 144, 169, 182, 121

Did you get it? The answer is one hundred and eighty-two. All the other numbers are squares of integers.

One more:

3, null, 1, 7, 4, 5

And the answer is . . . null! All of the other items in the list are integers, but null isn’t an integer.

What? Was that too easy?

Null overview

As out of place as null looks in that list of integers, many computer languages actually include it. In the past Dart did, too, but as of version 2.12, Dart decided to take null out of the list and only put it back if you allow Dart to do so. This feature is called sound null safety, but to find out what was so dangerous about null in the first place, you’ll have to keep reading.

What null means

Null means “no value” or “absence of a value”. It’s quite useful to have such a concept. Imagine not having null at all. Say you ask a user for their postal code so that you can save it as an integer in your program:

int postalCode = 12345;
int postalCode = -1;
// Hey everybody, -1 means that the user
// doesn't have a postal code. Don't forget!
int postalCode = -1;
int postalCode = null;
A value of type 'Null' can't be assigned to a variable of type 'int'.

The problem with null

As useful as null is for indicating the absence of a value, developers do have a problem with it. The problem is that they tend to forget that it exists. And when developers forget about null, they don’t handle it in their code. Those nulls are like little ticking time bombs ready to explode the code.

environment:
  sdk: '>=2.10.0 <3.0.0'
void main() {
  print(isPositive(3));  // true
  print(isPositive(-1)); // false
}

bool isPositive(int anInteger) {
  return !anInteger.isNegative;
}
print(isPositive(null));
NoSuchMethodError: The getter 'isNegative' was called on null.
environment:
  sdk: '>=2.12.0 <3.0.0'

Nullable vs. non-nullable types

Starting with version 2.12, Dart separated its types into nullable and non-nullable. Nullable types end with a question mark (?) while non-nullable types do not.

Non-nullable types

Dart types are non-nullable by default. That means they’re guaranteed to never contain the value null, which is the essence of the meaning of sound in the phrase “sound null safety”. These types are easy to recognize because, unlike nullable types, they don’t have a question mark at the end.

int myInt = 1;
double myDouble = 3.14159265;
bool myBool = true;
String myString = 'Hello, Dart!';
User myUser = User(id: 42, name: 'Ray');
int postalCode = null; // error

Nullable types

A nullable type can contain the null value in addition to its own data type. You can easily tell the type is nullable because it ends with a question mark (?), which is like saying, “Maybe you’ve got the data you want or maybe you’ve got null. That’s the question.” Here are some example values that nullable types could contain:

int? myInt = null;
double? myDouble = null;
bool? myBool = null;
String? myString = null;
User? myUser = null;
int? age;
double? height;
String? message;
print(age);
print(height);
print(message);

Mini-exercises

  1. Create a String? variable called profession, but don’t give it a value. Then you’ll have profession null. :]
  2. Give profession a value of “basketball player”.
  3. Write the following line and then hover your cursor over the variable name. What type does Dart infer iLove to be?
const iLove = 'Dart';

Handling nullable types

The big problem with the old nullable types in the past was how easy it was to forget to add code to handle null values. That’s no longer true. Dart now makes it impossible to forget because you really can’t do much at all with a nullable value until you’ve dealt with the possibility of null.

String? name;
print(name.length);
The property 'length' can't be unconditionally accessed because the receiver can be 'null'.

Type promotion

The Dart analyzer, which is the tool that tells you what the compile-time errors and warning are, is smart enough to tell in a wide range of situations if a nullable variable is guaranteed to contain a non-null value or not.

String? name;
name = 'Ray';
print(name.length);

Flow analysis

Type promotion works for more than just the trivial example above. Dart uses sophisticated flow analysis to check every possible route the code could take. As long as none of the routes come up with the possibility of null, it’s promotion time!

bool isPositive(int? anInteger) {
  if (anInteger == null) {
    return false;
  }
  return !anInteger.isNegative;
}

Null-aware operators

In addition to flow analysis, Dart also gives you a whole set of tools called null-aware operators that can help you handle potentially null values. Here they are in brief:

If-null operator (??)

One very convenient way to handle null values is to use the double question mark (??), also known as the if-null operator. This operator says, “If the value on the left isn’t null, then use it; otherwise, go with the value on the right.”

String? message;
final text = message ?? 'Error';
String text;
if (message == null) {
  text = 'Error';
} else {
  text = message;
}

Null-aware assignment operator (??=)

In the example above, you had two variables: message and text. However, another common situation is when you have a single variable that you want to update if its value is null.

double? fontSize;
fontSize = fontSize ?? 20.0;
x = x + 1;
x += 1;
fontSize ??= 20.0;

Null-aware access operator (?.)

Earlier with anInteger.isNegative, you saw that trying to access the isNegative property when anInteger was null caused a NoSuchMethodError. There’s also an operator for null safety when accessing object members. The null-aware access operator returns null if the left-hand side is null. Otherwise, it returns the property on the right-hand side.

int? age;
print(age?.isNegative);
null
print(age?.toDouble());

Null assertion operator (!)

Sometimes Dart isn’t sure whether a nullable variable is null or not, but you know it’s not. Dart is smart and all, but machines don’t rule the world yet.

String nonNullableString = myNullableString!;
bool? isBeautiful(String? item) {
  if (item == 'flower') {
    return true;
  } else if (item == 'garbage') {
    return false;
  }
  return null;
}
bool flowerIsBeautiful = isBeautiful('flower');
A value of type 'bool?' can't be assigned to a variable of type bool
bool flowerIsBeautiful = isBeautiful('flower')!;
bool flowerIsBeautiful = isBeautiful('flower') as bool;
bool flowerIsBeautiful = isBeautiful('flower') ?? true;

Null-aware cascade operator (?..)

In Chapter 6 you learned about the cascade operator (..), which allows you to call multiple methods or set multiple properties on the same object.

class User {
  String? name;
  int? id;
}
User user = User()
  ..name = 'Ray'
  ..id = 42;
User? user;
user
  ?..name = 'Ray'
  ..id = 42;
String? lengthString = user?.name?.length.toString();

Null-aware index operator (?[])

The null-aware index operator (?[]) is used for accessing the elements of a list when the list itself might be null. You’ve used lists already a couple of times in this book, but since you won’t cover them in depth until Chapter 8, this section will just give a simple explanation of how the ?[] operator is used.

List<int>? myList = [1, 2, 3];
myList = null;
int? myItem = myList?[2];

Initializing non-nullable fields

When you create an object from a class, Dart requires you to initialize any non-nullable member variables before you use them.

class User {
  String name;
}

Using initializers

One way to initialize a property is to use an initializer value:

class User {
  String name = 'anonymous';
}

Using initializing formals

Another way to initialize a property is to use an initializing formal, that is, by using this in front of the field name:

class User {
  User(this.name);
  String name;
}

Using an initializer list

You can also use an initializer list to set a field variable:

class User {
  User(String name)
    : _name = name;
  String _name;
}

Using default parameter values

Optional parameters default to null if you don’t set them, so for non-nullable types, that means you must provide a default value.

class User {
  User([this.name = 'anonymous']);
  String name;
}
class User {
  User({this.name = 'anonymous'});
  String name;
}

Required named parameters

As you learned in Chapter 5, if you want to make a named parameter required, use the required keyword.

class User {
  User({required this.name});
  String name;
}

Nullable instance variables

All of the methods above guaranteed that the class field will be initialized, and not only initialized, but initialized with a non-null value. Since the field is non-nullable, it’s not even possible to make the following mistake:

final user = User(name: null);
The argument type 'Null' can't be assigned to the parameter type 'String'
class User {
  User({this.name});
  String? name;
}

No promotion for non-local variables

One topic that people often get confused about is the lack of type promotion for nullable instance variables.

bool isLong(String? text) {
  if (text == null) {
    return false;
  }
  return text.length > 100;
}
class TextWidget {
  String? text;

  bool isLong() {
    if (text == null) {
      return false;
    }
    return text.length > 100; // error
  }
}
The property 'length' can't be unconditionally accessed because the receiver can be 'null'.
bool isLong() {
  if (text == null) {
    return false;
  }
  return text!.length > 100;
}
class TextWidget {
  String? text;

  bool isLong() {
    final text = this.text; // shadowing
    if (text == null) {
      return false;
    }
    return text.length > 100;
  }
}

The late keyword

Sometimes you want to use a non-nullable type, but you can’t initialize it in any of the ways you learned above.

class User {
  User(this.name);

  final String name;
  final int _secretNumber = _calculateSecret();

  int _calculateSecret() {
    return name.length + 42;
  }
}
The instance member '_calculateSecret' can't be accessed in an initializer.
late final int _secretNumber = _calculateSecret();
class User {
  User(this.name) {
    _secretNumber = _calculateSecret();
  }
  late final int _secretNumber;
  // ...
}

Dangers of being late

The example above was for initializing a final variable, but you can also use late with non-final variables. You have to be careful with this, though. Take a look at the following example:

class User {
  late String name;
}
final user = User();
print(user.name);
LateInitializationError: Field 'name' has not been initialized.

Benefits of being lazy

Who knew that it pays to be lazy sometimes? Dart knows this, though, and uses it to great advantage.

class SomeClass {
  late String? value = doHeavyCalculation();
  String? doHeavyCalculation() {
    // do heavy calculation
  }
}

Challenges

Before moving on, here are some challenges to test your knowledge of nullability. It’s best if you try to solve them yourself, but solutions are available with the supplementary materials for this book if you get stuck.

Challenge 1: Random nothings

Write a function that randomly returns 42 or null. Assign the return value of the function to a variable named result that will never be null. Give result a default of 0 if the function returns null.

Challenge 2: Naming customs

People around the world have different customs for giving names to children. It would be difficult to create a data class to accurately represent them all, but try it like this:

Key points

  • Null means “no value.”
  • A common cause of errors for programming languages in general comes from not properly handling null.
  • Dart 2.12 introduced sound null safety to the language.
  • Sound null safety distinguishes nullable and non-nullable types.
  • A non-nullable type is guaranteed to never be null.
  • Null aware operators help developers to gracefully handle null.
??    if-null operator
??=   null-aware assignment operator
?.    null-aware access operator
?.    null-aware method invocation operator
!     null assertion operator
?..   null-aware cascade operator
?[]   null-aware index operator
...?  null-aware spread operator

Where to go from here?

Dart is an evolving and ever improving language. Since development and discussions about new features all happen out in the open, you can watch and even participate. Go to dart.dev/community to learn more.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.