This codelab teaches you about Dart’s null-safe type system,
which was introduced in Dart 2.12. When you opt into null safety,
types in your code are non-nullable by default,
meaning that values can’t be null
unless you say they can be.
This codelab covers the following material:
- Nullable and non-nullable types.
- When to add
?
or!
to indicate nullability or non-nullability. - Flow analysis and type promotion.
- How and when to use null-aware operators.
- How the
late
keyword affects variables and initialization.
Using embedded DartPad editors, you can test your knowledge by completing and running exercises. To get the most out of this codelab, you should have some knowledge of basic Dart syntax.
Nullable and non-nullable types
When you opt in to null safety, all types are
non-nullable by default. For example, if you have a variable of
type String
, it will always contain a string.
If you want a variable of type String
to accept any string
or the value null
, give the variable a nullable type by adding a
question mark (?
) after the type name. For example, a variable of
type String?
can contain a string, or it can be null.
Exercise: Non-nullable types
The variable a
below is declared as an int
. Try changing the value in the
assignment to 3 or 145. Anything but null!
void main() {
int a;
a = null;
print('a is $a.');
}
Exercise: Nullable types
What if you need a variable that can hold a null value? Try changing the
type of a
so that a
can be either null or an int:
void main() {
int a;
a = null;
print('a is $a.');
}
Exercise: Nullable type parameters for generics
Type parameters for generics can also be nullable or non-nullable. Try using
question marks to correct the type declarations of aNullableListOfStrings
and
aListOfNullableStrings
:
void main() {
List<String> aListOfStrings = ['one', 'two', 'three'];
List<String> aNullableListOfStrings;
List<String> aListOfNullableStrings = ['one', null, 'three'];
print('aListOfStrings is $aListOfStrings.');
print('aNullableListOfStrings is $aNullableListOfStrings.');
print('aListOfNullableStrings is $aListOfNullableStrings.');
}
The null assertion operator (!)
If you’re sure that an expression with a nullable type isn’t null, you can use a
null assertion operator
(!
) to make Dart treat it as non-nullable. By adding !
just after the
expression, you tell Dart that the value won’t be null, and
that it’s safe to assign it to a non-nullable variable.
Exercise: Null assertion
In the code below, try adding exclamation points to correct the broken assignments:
int? couldReturnNullButDoesnt() => -3;
void main() {
int? couldBeNullButIsnt = 1;
List<int?> listThatCouldHoldNulls = [2, null, 4];
int a = couldBeNullButIsnt;
int b = listThatCouldHoldNulls.first; // first item in the list
int c = couldReturnNullButDoesnt().abs(); // absolute value
print('a is $a.');
print('b is $b.');
print('c is $c.');
}
Null-aware operators
If a variable or expression is nullable, you can use type promotion to access the type’s members, or you can use null-aware operators to handle nullable values.
If based on the flow of the program,
you know the value of an expression isn’t null
,
you can use the null assertion operator (!
),
but that will throw an exception if the value is null
.
To handle potentially null
values you can instead use
the conditional property access operator (?.
)
or null-coalescing operators (??
)
to conditionally access a property or
provide a default value if null
respectively.
Exercise: Conditional property access
If you’re unsure that an expression with a nullable type is null
or not,
you can use the conditional member access operator (?.
)
to conditional execute the remainder of the expression.
// The following calls the 'action' method only if nullableObject is not null
nullableObject?.action();
In the code below, try using conditional property access
in the stringLength
method to fix the error and
to return the length of the string or null
if it is null
:
{$ begin main.dart $}
int? stringLength(String? nullableString) {
return nullableString.length;
}
{$ end main.dart $}
{$ begin solution.dart $}
int? stringLength(String? nullableString) {
return nullableString?.length;
}
{$ end solution.dart $}
{$ begin test.dart $}
void main() {
const nonNullString = 'testing';
try {
final nonNullResult = stringLength(nonNullString);
if (nonNullResult != nonNullString.length) {
_result(false, [
'Tried calling `stringLength`, with the string \'testing\' but '
'received $nonNullResult instead of the expected ${nonNullString.length}.'
]);
return;
}
final nullableResult = stringLength(null);
if (nullableResult != null) {
_result(false, [
'Tried calling `stringLength`, with a `null` value but '
'received $nullableResult instead of the expected `null`.'
]);
return;
}
_result(true);
} on UnimplementedError {
_result(false, [
'Tried running `stringLength`, but received an error. Did you implement the method?'
]);
return;
} catch (e) {
_result(
false, ['Tried calling `stringLength`, but received an exception: $e']);
}
}
{$ end test.dart $}
{$ begin hint.txt $}
You can use the conditional property access operator (?.)
to only access a property of if expression is not null otherwise return null.
{$ end hint.txt $}
Exercise: Null-coalescing operators
If you want to provide an alternative value
when the expression evaluates to null
,
you can specify another expression to evaluate and return instead
with the null-coalescing operator (??
).
// Both of the following print out 'alternate' if nullableString is null
print(nullableString ?? 'alternate');
print(nullableString != null ? nullableString : 'alternate');
You can also use the null-coalescing assignment operator (??=
)
to evaluate and assign an expression result to a variable
only if that variable is currently null
.
// Both of the following set nullableString to 'alternate' if it is null
nullableString ??= 'alternate';
nullableString = nullableString != null ? nullableString : 'alternate';
In the code below, try using these operators
to implement updateStoredValue
following the logic outlined in its documentation comment:
{$ begin main.dart $}
abstract class Store {
int? storedNullableValue;
/// If [storedNullableValue] is currently `null`,
/// set it to the result of [calculateValue]
/// or `0` if [calculateValue] returns `null`.
void updateStoredValue() {
TODO('Implement following documentation comment');
}
/// Calculates a value to be used,
/// potentially `null`.
int? calculateValue();
}
{$ end main.dart $}
{$ begin solution.dart $}
abstract class Store {
int? storedNullableValue;
/// If [storedNullableValue] is currently `null`,
/// set it to the result of [calculateValue]
/// or `0` if [calculateValue] returns `null`.
void updateStoredValue() {
storedNullableValue ??= calculateValue() ?? 0;
}
/// Calculates a value to be used,
/// potentially `null`.
int? calculateValue();
}
{$ end solution.dart $}
{$ begin test.dart $}
class NullStore extends Store {
@override
int? calculateValue() {
return null;
}
}
class FiveStore extends Store {
@override
int? calculateValue() {
return 5;
}
}
void main() {
try {
final nullStore = NullStore();
if (nullStore.storedNullableValue != null) {
_result(false,
['The `storedNullableValue` field should be `null` at first.']);
return;
}
nullStore.updateStoredValue();
if (nullStore.storedNullableValue != 0) {
_result(false, [
'Tried calling `updateStoredValue`, when `calculateValue` returned `null` '
'but `storedNullableValue` was ${nullStore.storedNullableValue} '
'instead of the expected 0.'
]);
return;
}
final fiveStore = FiveStore();
fiveStore.updateStoredValue();
if (fiveStore.storedNullableValue != 5) {
_result(false, [
'Tried calling `updateStoredValue`, when `calculateValue` returned `5`'
'but `storedNullableValue` was ${fiveStore.storedNullableValue} '
'instead of the expected 5.'
]);
return;
}
fiveStore.storedNullableValue = 3;
if (fiveStore.storedNullableValue != 3) {
_result(false, [
'Tried calling `updateStoredValue`, when `storedNullableValue` '
'was already not `null`'
'but `storedNullableValue` was still updated when it shouldn\'t be.'
]);
return;
}
_result(true);
} on UnimplementedError {
_result(false, [
'Tried running `updateStoredValue`, but received an error. Did you implement the method?'
]);
return;
} catch (e) {
_result(false,
['Tried calling `updateStoredValue`, but received an exception: $e']);
}
}
{$ end test.dart $}
{$ begin hint.txt $}
You can think of the null-coalescing operators
as providing an alternative value if the left-hand side is `null`.
{$ end hint.txt $}
Type promotion
With sound null safety, Dart’s flow analysis has been extended to take nullability into account. Nullable variables that can’t possibly contain null values are treated like non-nullable variables. This behavior is called type promotion.
Exercise: Definite assignment
Dart’s type system can track where variables are assigned and read, and can verify that non-nullable variables are given values before any code tries to read from them. This process is called definite assignment.
Try uncommenting the if
-else
statement in the code below, and
watch the analyzer errors disappear:
void main() {
String text;
//if (DateTime.now().hour < 12) {
// text = "It's morning! Let's make aloo paratha!";
//} else {
// text = "It's afternoon! Let's make biryani!";
//}
print(text);
print(text.length);
}
Exercise: Null checking
In the code below, add an if
statement to the beginning of getLength
that
returns zero if str
is null:
int getLength(String? str) {
// Add null check here
return str.length;
}
void main() {
print(getLength('This is a string!'));
}
Exercise: Promotion with exceptions
Promotion works with exceptions as well as return statements. Try a null check
that throws an Exception
instead of returning zero.
int getLength(String? str) {
// Try throwing an exception here if `str` is null.
return str.length;
}
void main() {
print(getLength(null));
}
The late keyword
Sometimes variables—fields in a class, or top-level variables—should
be non-nullable, but they can’t be
assigned a value immediately.
For cases like that, use the
late
keyword.
When you put late
in front of a variable declaration,
that tells Dart the following:
- Don’t assign that variable a value yet.
- You will assign it a value later.
- You’ll make sure that the variable has a value before the variable is used.
If you declare a variable late
and the variable is read before it’s assigned a
value, an error is thrown.
Exercise: Using late
Try using the late
keyword to correct the following code. For a little extra
fun afterward, try commenting out the line that sets description
!
class Meal {
String _description;
set description(String desc) {
_description = 'Meal description: $desc';
}
String get description => _description;
}
void main() {
final myMeal = Meal();
myMeal.description = 'Feijoada!';
print(myMeal.description);
}
Exercise: Late circular references
The late
keyword is helpful for tricky patterns like circular references.
The following code has two objects that need to maintain non-nullable references
to each other. Try using the late
keyword to fix this code.
Note that you don’t need to remove final
. You can create
late final
variables:
you set their values once, and after that they’re read-only.
class Team {
final Coach coach;
}
class Coach {
final Team team;
}
void main() {
final myTeam = Team();
final myCoach = Coach();
myTeam.coach = myCoach;
myCoach.team = myTeam;
print('All done!');
}
Exercise: Late and lazy
Here’s another pattern that late
can help with:
lazy initialization
for expensive non-nullable fields.
Try this:
- Run this code without changing it, and note the output.
- Think: What will change if
you make
_cache
alate
field? - Make
_cache
alate
field, and run the code. Was your prediction correct?
int _computeValue() {
print('In _computeValue...');
return 3;
}
class CachedValueProvider {
final _cache = _computeValue();
int get value => _cache;
}
void main() {
print('Calling constructor...');
var provider = CachedValueProvider();
print('Getting value...');
print('The value is ${provider.value}!');
}
What’s next?
Congratulations, you’ve finished the codelab! If you’d like to learn more, here are some suggestions for where to go next:
- Learn more about null safety:
- Overview: Sound null safety.
- Deep dive: Understanding null safety.
- Try another codelab.
- Get the Dart SDK.
If you’re interested in using embedded DartPads, like this codelab does, see best practices for using DartPad in tutorials. If you’re interested in improving this codelab, see issue #3093.