Implementation guidelines of sound null safety using the freezed

keisuke ishikura
5 min readOct 3, 2021

--

I have implemented sound null safety in the application using the freezed that is flutter package. I will share implementation guidelines at that time.

Implementation guidelines of sound null safety using the freezed

Introduction

I think the main purpose of null safety support is to prevent unexpected crashes caused by null. If you try to support null safety, you may meet resistance from some programmers. Their reason for resisting is that there are times when null is necessary. For example, in the case of the bool type, true or false would mean something, so they want to use null to express that they are unselected or unexecuted. And if they don’t put null in the number type, the UI might always show some number. In addition, the amount of code tends to increase because the null-aware code is added to the null-unaware code. I’m not saying don’t use null when dealing with null safety, I’m saying handle null properly. I think it’s especially bad to crush in mobile apps. For me, it’s almost as bad as a silly release mistake. I feel that many programmers don’t care about this these days. I think it’s because the user side understands how difficult it is to build a system, and they are more tolerant of programmer’s mistakes. As a programmer, I think this is not a happy situation. But I digress. The null safety support prevents unexpected crashes. So, null safety support should always be done. Never put null in a variable where null is not required by the specification. With this assumption, as an exception, if null is required by the specification, then null is allowed. However, be sure to implement null handling. The code may be long and cumbersome, but that is our problem, not the user’s. In order to provide the best experience to users, null safety should be supported.

In this article, I would like to focus on freezed models and present a guideline for dealing with null safety, assuming that there are many programmers who use freezed to create their models. The idea is to prevent the influx of nulls when retrieving data from other systems.

String type

Let’s set a space as the default value.

@Default('') String strValue,

int type

It is difficult to decide what to put as the default value, but I set it to 0. If I don’t want to include 0 in the specification, I set it to -1, because I want to distinguish between 0 and no data when displaying numeric information. I have not encountered this problem so far, but if there is a problem with -1 in the specification, you can allow null.

@Default(0) int intValue,

double type

For the same reason as for the int type, the default value is set to 0.0.

@Default(0.0) double doubleValue,

num type

For the same reason as for the int and double types, the default value is set to 0.

@Default(0) num numValue,

bool type

For the bool type, true and false are often important in the specification, so I set null. I sometimes set it to false when it is good to have false as the default value.

bool? boolValue,

DateTime type

As it is very difficult to determine the default value for the DateTime type, null is allowed.

DateTime? datetimeValue,

List type

Let’s set [] as the default value.

@Default([]) List<String> listValue,

GeoPoint class

GeoPoint is a firestore class, so it is not recognized by freezed. It is possible to use dynamic, but the compiler’s null check does not work for dynamic variables. So, set the type of the variable to GeoPoint. Then, create a GeoPointConverter class to avoid the freezed error.

@JsonKey(name: 'location', fromJson: GeoPointConverter.fromJson, toJson: GeoPointConverter.toJson)
GeoPoint? location

The GeoPointConverter class is a class that just returns null if the value is null.

class GeoPointConverter {
static GeoPoint? fromJson(GeoPoint? value) => value == null ? null : value;

static GeoPoint? toJson(GeoPoint? value) => throw UnsupportedError('');
}

some class

If the default value is set to an instance of class in freezed, build_runner will fail, so null is allowed.

SomeClass? someClassValue,

To summarize the above, the model definition of freezed with null safety support is as follows.

@freezed
class SampleFreezedModel with _$SampleFreezedModel {
const factory SampleFreezedModel({
@Default('') String strValue,
@Default(0) int intValue,
@Default(0.0) double doubleValue,
@Default(0) num numValue,
bool? boolValue,
DateTime? datetimeValue,
@Default([]) List<String> listValue,
SomeClass? someClassValue,
@JsonKey(name: 'location', fromJson: GeoPointConverter.fromJson, toJson: GeoPointConverter.toJson)
GeoPoint? location,
}) = _SampleFreezedModel;
}

Tips

For beautiful code, how to avoid ??

As the implementation becomes more complex, you may come across variables that are never null in the implementation, but are allowed to be null. In such cases, it is necessary to add ??. However, I don’t like to do that, so I defined an extension, and by using it, I can avoid using ??.

For example, if it is declared as DateTime? but always contains a value, define the following extension.

extension DateTimeNullConverter on DateTime? {
DateTime notNull() {
if (this == null) {
throw UnsupportedError('That's impossible.');
}
return this ?? DateTime.now().add(Duration(days: -100));
}
}

The usage is as follows.

class SomeClass {
DateTime? dateTimeValue;
}
final _someClass = SomeClass();
_someClass.dateTimeValue = DateTime.now();
DateTime _dateTimeValue2 = DateTime.now().add(Duration(days: -100));
final Duration _duration =
_someClass.dateTimeValue.notNull().difference(_dateTimeValue2);

This is what happens if you don’t use this extension.

class SomeClass {
DateTime? dateTimeValue;
}
final _someClass = SomeClass();
_someClass.dateTimeValue = DateTime.now();
DateTime _dateTimeValue2 = DateTime.now().add(Duration(days: -100));
final Duration _duration =
(_someClass.dateTimeValue ?? DateTime.now()).difference(_dateTimeValue2);

Please look at the bold text. The effect can be clearly seen by comparison.

How to avoid returning undefined values in a function that returns an emun value

The getter, sampleEnumValue, returns the value of one of the enums, SampleEnum. If an impossible value is set in the conditional statement, you may not want to return any value of SampleEnum. This often happens in the early stages of development when the API I/F definition and DB definition are uncertain. You don’t want to allow null in the return value, so you may wonder what to make the return value. In such a case, let’s use throw to return UnsupportedError. There is no need to return the value of SampleEnum, which is an enum, and it will also let you know if there is a problem with the data. This is a great way to kill two birds with one stone.

enum SampleEnum {
one,
two
}

final int _condition = 0;

SampleEnum get sampleEnumValue {
if(_condition == SampleEnum.one.index) {
return SampleEnum.one;
} else if (_condition == SampleEnum.two.index) {
return SampleEnum.two;
} else {
throw UnsupportedError('未実装');
}
}

Conclusion

For code that uses freezed, I proposed a model implementation that supports sound null safety. I have created four applications using this method, and so far no problems have occurred. I hope this will be a guideline for you when you think about sound null safety support. In addition, the code that supports sound null safety may seem redundant. I have introduced two tips to make it a little better. I hope this will be useful to you as well.

--

--

keisuke ishikura
keisuke ishikura

Written by keisuke ishikura

Mypace Co.,Ltd. CEO/ Mobile Engineer

Responses (1)