Notes by Lesnitsky Subscribe

#flutter ยท 2020-06-10 02:36 PM Twitter Logo

Fighting null in your Dart and Flutter applications

I found myself in a need of handling null again and covered many ways to do this in this article. This is an entry-level topic, but important though, since accidentally trying to access properties and methods of null will cause an error (often unexpected, thus โ€“ unhandled)

Plain old if

Of course, the simplest way to handle null: just check whether something is null with the equality operator

if (someVar == null) {
doSomething();
}

dart-basics

There is a package which provides isNull/isNotNull getters to many built-in dart types https://pub.dev/packages/basics, example above will look like this

if (someVar.isNull) {
doSomething();
}

Null-aware operator ??

Dart has built-in operators which make your code more verbose and clean. Often your business logic will have some sort of default behavior when dealing with null, and your code will look something like this:

int c;
int a = 42;
int b;

if (b == null) {
c = a; // default value
} else {
c = b;
}

This code could be simplified with null-aware ?? operator.

Operator ?? returns the expression on its right if the left side is null, so the example above will turn into this:

int a = 42;
int b;

int c = b ?? a; // b is null, c is 42

Related operator ??= assigns a value to a variable in case it is currently uninitialized (equals null), so if you ever have a code like this:

int a = 42;
int b;
int c = 15;
int d = 34;
int e = 13;

if (a == null) {
a = c;
}

if (b == null) {
b = d;
}

if (b == null) {
b = e;
}

you can rewrite it in something much cleaner:

int a = 42;
int b;
int c = 15;
int d = 34;
int e = 13;

a ??= c; // a is still 42
b ??= d; // b was null, assigned 34
b ??= e; // b is not null, still 34

I often use ??= operator to generate global keys for widgets based on some entity ids, here's an example:

class MyWidget extends StatefulWidget {
static Map<String, GlobalKey> keys = {};
final SomeEntity entity;
const MyWidget({Key key, this.entity}) : super(key: key);

@override
_MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return OtherWidget(
key: MyWidget.keys[widget.entity.id] ??= new GlobalKey(
debugLabel: widget.entity.id,
),
);
}
}

Null-aware property access

Another case where you need to handle null is class method/property access

The following example will throw an exception

class MyClass {
String id;
}

void main() {
MyClass a;

print(a.id); // throws
}

Of course, "if" works

if (a != null) {
print(a.id);
}

but you can use ?. operator instead. It returns an accessed value or null if the object is not initialized (equals null)

print(a?.id);

Since this operator may return null, it could be combined with null-aware ?? operator

print(a?.id ?? 'a is null');

Null-aware spread operator

Another case where we might need to handle nulls is while constructing collections from other collections using the spread operator.

List a;
final b = [1, 2, 3, if (a != null) ...a];

This example could be rewritten a bit more elegant using ?? operator

List a;
final b = [1, 2, 3, ...(a ?? [])];

or even better, using null-aware spread

List a;
final b = [1, 2, 3, ...?a];

Null safety on type level

Dart has a non-nullable by default experiment (NNBD), which makes it even easier to deal with nulls, since you'll be warned about something being potentially null at compilation time. Read these awesome explanations:

and relevant discussion https://github.com/dart-lang/language/issues/376

Nulls in Flutter widget tree

Many widgets don't like nulls as well. For example, the following widget will fail to build since footer is null:

class SomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget footer;

return Column(
children: [
Text('42'),
Text('15'),
footer,
],
);
}
}

Another scenario where you might have troubles with null is whenever you map a collection into widgets::

class SomeWidget extends StatelessWidget {
final List<String> items;

const SomeWidget({Key key, this.items}) : super(key: key);

@override
Widget build(BuildContext context) {
return Column(
children: items.map((item) {
if (item != null) {
return Text(item);
}
}).toList(),
);
}
}

Else branch isn't covered, so you'll end up with an error

One way to fix it is to filter out all nulls:

Column(
children: items
.map((item) {
if (item != null) {
return Text(item);
}
})
.where((element) => element != null)
.toList(),
);

This is not the best approach in terms of performance, so I often use some "placeholder" widget (empty container)

Column(
children: items.map((item) {
return item != null ? Text(item) : Container();
}).toList(),
);

Container, on the other hand, is not the cheapest widget as well, since it is more expensive to construct, the framework needs to perform a layout of the container, check whether it needs to be painted, etc., so I've built almost-zero-cost widget which is good to be used in such cases: https://pub.dev/packages/null_widget

It could be also used together with https://pub.dev/packages/guard

Please ping me on twitter if I forgot to include other good examples or ways to handle nulls

๐Ÿ’™