Cinnamon logo
Cinnamon logo
Cinnamon logo
Close

Home

Projects

Services

About Us

Careers

Blog

Let’s collaborate

Close

Null safety and late initializers in Flutter

Josip Kilić

2022-06-06

5min

Development

Complex applications with many users leave little space for error. Learn null safety to reduce app crashes and gain loyal customers.

placeholderLate_initializers.png

Share this blog:

twitter logo
facebook logo
linkedin logo
link logo

Late Initializers

Late initializers should only be used in specific circumstances:

  1. If we can guarantee that the variable will definitely be initialized. Late” means “this variable will be initialized late”, it does not mean “this variable might be initialized late”. If the Late is not initialized, then we get the red screen of death. This is something that should never happen.

  2. If initializing the variable is expensive and we want to be more efficient.

A late variable is only actually created when we attempt to use it, as opposed to when we define/initialize it. For example, if we initialize it in the constructor, but we never actually use it, then no work is done. It’s like it was never created. So, if initializing the variable is very expensive and requires a lot of processing, but we only use it sometimes, then this can make the app more efficient.

You are breaking both of these rules if you’re doing this:

  1. We are using late variables that are never initialized, because the initialization is based on the outcome of logic, such as “if”, or is based on the response from a server, which we cannot guarantee. This leads to the red screen of death.

  2. The late variables are not expensive in our app, so late is overkill.

Null Safety/Nullable Types

We have two types of types in Flutter:

  1. Non-nullable: a standard type, the only type that was allowed prior to Flutter adding null safety. For example, int, bool. Under no circumstances can these variables ever be null.

  2. Nullable: new standard types that can also be null like int?, bool?. For example, a bool can be true or false, a bool? can be true, false, or null.

If we are using non-nullable types, then we do not need to think about it or do anything special. The compiler forces us to follow the rules and behave responsibly. If we try to make a bool = null, the app simply will not compile. Easy.

If we are using nullable types, then we need to work with the compiler to create a safe scenario. If we get it wrong, then we will get errors when the app runs, or the red screen of death. It is our responsibility to safely handle nullable types.

To do this, we need to follow some rules:

  1. No assumptions: if a variable is nullable, then it might be null. End of story. No ifs, buts, or maybes. It could be null, so we need to handle this. We cannot assume it will not be null. Especially if the setting of this variable depends on any kind of logic or even worse, a server response.

  2. Always check: before using a nullable variable, we must check if it is null or not. Again, no ifs, buts or maybes. It could be null, we cannot assume, so we must check.

  3. Handle if null: if the variable is null, deal with it. We cannot ignore it because this would be the same as assuming it will never be null. Handling it can be as simple as showing an error message or providing a default value. However, we cannot just throw in any value to please the compiler, again, this would be working under the assumption it will never be null. We must assume the default value might be used at some point, and so we need to provide a real, usable value, and we must test it.

  4. Never, ever use the ‘bang’ operator.

int? testValueCouldBeNull = null testValueCouldBeNull = 2 logger.v(testValueCouldBeNull!) 

In this particular, basic example, this would be fine, this would print out “2”. We can use our brains to see that this value will not be null and the compiler will trust our judgment, however, when we move beyond basic examples, we simply cannot keep track of all the code, and everything that can happen in real time, and so we cannot make this assumption. The following code would crash:

int? testValueCouldBeNull = null logger.v(testValueCouldBeNull!)

Again, this is obvious, but what about the following code:

int? testValueCouldBeNull = null testValueCouldBeNull = defineValueOnlyIfUserHasInternetAccess() logger.v(testValueCouldBeNull!)

We have no idea if the user has internet access at that moment, 99% of the time they will, but we will have 1% of the time with crashes. We are making an assumption that the user has internet at that moment. We cannot do this.

We need to do something like this:

int? testValueCouldBeNull = null testValueCouldBeNull = defineValueIfUserHasInternetAccess() If (testValueCouldBeNull != null) { logger.v(testValueCouldBeNull!) } else { logger.v(‘Error, no value’) }

Or

int? testValueCouldBeNull = null testValueCouldBeNull = defineValueIfUserHasInternetAccess() ?? -1 logger.v(testValueCouldBeNull!)

These are all trivial examples, but if we don’t follow these rules, then we add very difficult to track bugs into the app, that will happen at very random times and can cause a lot of problems. I will give you a real world example that caused a lot of problems over the holidays, this became the number one bug over Christmas and New Year’s…

I released updates to the app I was working on just before Christmas, and almost all bugs were solved, but we still had users emailing every day who could not use the app at all.

I checked and double checked everything and could see no problems at all. Thankfully, because of the new logging system we created, I was able to get diagnostic data and discovered the following:

kKeyStorageSystemServerLocalTimestampDifference: -1640791890842 correctedTimestamp: 1970-01-01 01:05:57.898

The correctedTimestamp code is setting their timestamp to 1970 which means all their attempts to contact the server fail as only recent timestamps are accepted, to help defeat spambots.

Why is this happening?

final apiTimestamp = await _generalNetworkController.getTimestamp() ?? 0;

We try to get the timestamp from the server, if this fails, the timestamp is set to 0. 0 is a nonsensical timestamp. It means 1970. We asked the server to tell us the current time, and if it cannot, then we say “ok, the current time is 1970”

So, in some respects, this code is good i.e. a) we don’t make any assumptions about the value never being null, which is great b) we provide a default/fallback, however, the fallback is nonsense, an assumption is being made that it will never ever happen and so any value is being given to the compiler just to make the compiler happy, but no real consideration has been given to what will actually happen to the logic if it does.

This tiny little assumption has become the number one bug and completely broke the app for many people. Imagine when we get to the stage where we have hundreds of these assumptions?

Better code would have been:

final apiTimestamp = await _generalNetworkController.getTimestamp() ?? DateTime.now().millisecondsSinceEpoch;

Or to treat a failure as a complete failure, and therefore, do not attempt to correct the timestamp, try again later, etc.

Also, doing a quick scan of the code, I can see bang operators that could cause random problems:

File getVideoFile({required Message conversation}) => File(conversation.localVideoPath!); Image.file(File(filepath!), height: 150, width: 200, fit: BoxFit.cover)

It is most likely that we cannot guarantee that these variables will not be null, we are making an assumption that they will never be null, not handling if they are null, not providing a fallback value, and so on. Using this shortcut means we can have one line of code, but it's better to have five lines of code with null safety, than one line of code that could crash the entire app based on circumstances without our control. For example, if the user has a problem with their gallery app and it does not provide them with a videoPath or something. Believe me, in Android, these things happen all the time.

Conclusion

We should not be using late except in rare circumstances, i.e. when we can guarantee we will definitely initialize the variable, almost immediately, and if initializing the variable is expensive.

We should use nullable types instead. But, we must never assume it will not be null and always check that it is not null, always handle if not null, always handle in a way that makes sense and will not break the app if it happens. And never, ever, use the bang operator unless you have literally just defined the variable in the previous line.

Share this blog:

twitter logo
facebook logo
linkedin logo
link logo

Subscribe to our newsletter

We send bi-weekly blogs on design, technology and business topics.

Similar blogs

Job application illustration

You could use our expertise?

Let's work together.