21. Flutter 2 - Null Safety

21. Flutter 2 - Null Safety

Welcome back, Glad that you are here.

We are very near to launching the app in to play store. When I started this series, Flutter 2 was not announced, but now it has. So, in this tutorial, I will teach you how you can migrate an app to Flutter 2 with Null safety.

What is Null Safety?

With null safety, compiler enables us to decide whether a variable can be null or not in its lifetime. Before null safety, we could unintentionally introduce an error when an object is null and we try to access it's properties. But, with null safety awareness, a developer has more control over this situation and will be warned by the compiler to use null safe operators to omit the Billion Dollar Mistake.

Null Safety in Dart

Majorly there are 4-5 tools that can help you achieve null safety. I strongly believe that only knowing what these operators do will not help you in achieving complete null safety in any app. Nobody can reach the deepest of the sea with a dive, they have to swim continuously. For that, you need to understand the tools that you need. I will quickly walk you through these as there is a lot more than these 😉.

1. Nullable Types

You can make a variable or object nullable by using ? after the type declaration of a variable like String?, int?, Object?, etc. By String?, we instruct the compiler that this type of variable can contain either null or String.

2. Null Aware Operator

This needs no introduction to people coming from null-safe languages like Kotlin. The ?. operator is called Null aware operator, which can be invoked on only nullable types. For an instance, you've got a nullable string String? text, you won't be allowed to invoke a method on text without using ?.

//Nullable variable
String? text;

//Not allowed
text.compareTo('5');

//Allowed
text?.compareTo('5');

Using ?. will enforce the compiler to invoke compareTo() only when text is not null.

3. Bang Operator! - Casting away Nullability

The ! operator is a way in which we tell the compiler that even though we have declared a variable as nullable, it will not be null when we use it at this place. For instance:

//Nullable variable
String? text;

//Risky but allowed
text!.compareTo('5')

Personally, there should be no need to use it. These operators will cast the nullable type to non-nullable type and this is a loss of static safety. The cast must be checked at runtime to preserve soundness and it may fail and throw an exception. So, you have to look at your code cautiously before you use this operator.

4. The late keyword

Use the late keyword for a variable when you are initializing it later than declaring it as a field variable.

//late non-nullable declaration
late String text;

//late nullable declaration
late String? text;

//late final - can be assigned once
late final String text;

//lazy initialization
late String _converted = _getConvertedString();

The variable decided to be lately initialized can be nullable or non-nullable. It can be final as well but can be initialized only once. You can also instruct the compiler to initialize a variable when it is used for the first time. This can boost performance when the initialization is heavy and not needed at the start.

5. The required keyword

The required keyword is used when you want the caller to pass a nullable or non-nullable value to the constructor if it is required. This is not a feature that is introduced specifically for null safety but one of the features that make Dart a more complete language.

function ({required int? a, required int b, int c}) {}

Here, the caller will have to pass the nullable value to a, and the non-nullable value to b while initializing. The caller can omit passing c while initializing.

NullSafety ≠ Easy

With the introduction to Flutter 2 and Null Safety, the Dart team has also given us tools like migrate that help in migrating to null safety. So, what else a developer like you has to do? Run this command, sit back and approve the changes that are suggested? 🤔

NO - That's not your job. Use the tool but make smart decisions as per your app and use cases. Not every app will be benefitted from the suggestions from the tool.

NullSafety?.isEasy() ⇒ True

In this tutorial, I will step-by-step show you about decisions that I made while migrating to null-safety which will help you in your apps. Disclaimer - I have not used the migrate tool to migrate to Flutter 2. First, use the flutter 2 version to compile the Movie App.

Steps to Migrate

  1. Use fvm to easily migrate between flutter versions.
  2. Install fvm by following steps mentioned here - https://github.com/leoafarias/fvm
  3. Switch to flutter 2 version fvm use 2.0.0
  4. Change the IDE settings to use flutter 2 version now. Like in VSCode add settings:
"dart.flutterSdkPath": "/Users/prateeksharma/fvm/versions/2.0.0",
  1. Change dart and flutter SDK versions in pubspec.yaml
environment:
  sdk: ">=2.12.0 <3.0.0"
  flutter_sdk: ">=2.0.0"
  1. Check the lint errors by the analyze command
fvm flutter analyze

Solve Errors

600+ Errors

After the first analysis, I got 600+ errors. Don't be afraid, we can solve all those without breaking any functionality of the app. These errors can be because of many reasons.

We haven't still updated our plugins to null safety versions. So, all these errors are not related to our code. If we fix our code errors first and then update the plugins, there will be re-work to fix errors again in our code. So let's first upgrade the plugins.

To identify the right versions of the plugins used in our app, we will run this command -

fvm flutter pub outdated --mode=null-safety

This gives us a list of plugins that are available with null safety versions. Luckily, we have null-safety versions of all the dependencies that we use. If you don't have much luck, either contact the developer of that plugin or help the developer with MR against their plugin for Flutter 2.

outdated_plugins.png

Result of null safe dependencies used in Movie App

When you have no dependency that is not null safe go ahead and hit the upgrade command -

fvm flutter pub upgrade --null-safety

upgraded_plugins.png

Run fvm flutter analyze again.

Before further fixing lint errors, let me inform you about Deprecated List & Button. With Flutter 2, FlatButton and List() is deprecated instead use these -

list_button_comparison.png

It seems to upgrade all plugins before fixing the code helped in reducing issues. Here is the solution to various types of errors that you generally get in the terminal. Let's solve them one by one.


dead_null_aware_expression

Description - The left operand can't be null, so the right operand is never executed.

Where is this error - In NumExtension where we have used ?? on a non-nullable variable, which is dead code for the compiler.

Solution - The method sets a default value if the value is null, but we didn't mention that this variable can be null. So, use ? to inform the compiler that this extension can run on a nullable variable. With this, we reduce 1 error.

dead_null_aware_expression_comparison.png


missing_default_value_for_parameter

SCENARIO 1

Description - The parameter can't have a value of null because of its type, but the implicit default value is null. I have tackled this error based on places this error has occurred, let's look at this one by one.

Where - This is found in widgets where Key is used.

Solution - Straight-forward solution, wherever the Key is used in widgets, make them nullable - Key? key

Errors reduced by 27.

missing_default_value_for_parameter_widget_comparison.png

SCENARIO 2

Where - Api Client.

Solution - As params is an optional parameter for the method so we have to make it nullable. Change all Map<String, dynamic> params to nullable parameters. Use Uri.parse in getPath(). As of now, params is nullable, we will use ?. to invoke methods on it.

Errors reduced by 8.

missing_default_value_for_parameter_api_comparison.png

SCENARIO 3

Where - Widgets where @required is used but the parameter need not be null.

Solution - Use required where the @required is used in widgets where we control the parameters to be not null in any case. We want to not give anything nullable to UI, this will ensure that our widgets are drawn with non-null values like non-null strings, non-null image paths. Later on, we will only return a list of movies when the mandatory fields that are displayed in UI are not null. This way, we will always save our UI to not break. And it is logical as well, what will you show if a movie doesn't have a title, poster path? There is nothing to show there. Will it be a good User experience, if they see no title or no image for a movie? This logic is also dependent on which type of app it is. Our app very much depends on these fields so we cannot have them null.

Errors reduced by 25.

AppErrorWidget can have errorType and onPressed as null normally, but we are not having any scenario as such. Keeping that in mind, we are making it non-nullable.

missing_default_value_for_parameter_app_error_widget_comparison.png

FavoriteMovieCardWidget - the movie can be null because it will be fetched from Hive which can return nullable value for some key. But, it's in our hands to only add a movie to the list if it is not null. So, add that check in the movie_local_data_source.dart itself. So we will make it non-nullable.

missing_default_value_for_parameter_favorite_mc_widget_comparison.png

missing_default_value_for_parameter_local_datasource_comparison.png

FavoriteMovieGridView movies cannot be null, as we return an empty list in the local data source if there are no movies. So, we can keep it non-nullable and use required.

missing_default_value_for_parameter_grid_view_comparison.png

Similarly, we will do it for AnimatedMovieCardWidget and MoviePageView. MovieTabCardWidget here we will make sure that movieId, title, and posterPath are not null. We will return only those movies, where the title, movieId, and posterPath are not null. Because, this is the crux of our app where we depend on the movie id, title, and poster image of the movie.

missing_default_value_for_parameter_tab_card_widget_comparison.png

SCENARIO 4

Where - All Entities

Solution - Use required where the @required is used in widgets where we control the parameters to be not null in any case. This is very similar to widgets, as widgets only rely on entity data. Errors reduced by 11.

Consider, MovieDetailEntity and you will understand what I am trying to explain here.

missing_default_value_for_parameter_entity_comparison.png

Here, without overview, voteAverage and backdropPath our UI can work, but without title, id, and posterPath it cannot, that's why there are non-nullable required fields. Similar logic has to be applied to all the entities in the app.

SCENARIO 5

Where - Models

Solution - Use required and nullable for fields that are not shown on UI and can be null, make use of late when you are initializing fields later like list. Errors reduced by 122.

missing_default_value_for_parameter_model_comparison.png

We define a late non-nullable list, and using the factory constructor we are returning an empty or filled list.

SCENARIO 6

Where - Cubits

Solution - For all cubits, since we are using GetIt for the service locator, it will always return with some instance when we want, so we can be sure that we have a non-null instance.

missing_default_value_for_parameter_cubit_comparison.png

unchecked_use_of_nullable_value

Description - The method can't be unconditionally invoked because the receiver can be null

Where is this error - In any place where we are calling methods on nullable objects. Like TextTheme

Solution - Make all TextStyles nullable, as they are nullable in the google_fonts library and flutter SDK also allows them to be nullable in TextTheme. After making them nullable, you will use ?. to call copyWith.

Errors reduced by 14.

unchecked_use_of_nullable_value_textstyle_comparison.png

unnecessary_null_comparison

Description - The operand can't be null, so the condition is always true, which means that we have defined some variables as non-null but still having statements of null checks.

Where is this error - Mainly found in assert statements for widgets.

Solution - Remove unnecessary null assert statements because now they are redundant as we will control this with required keyword instead of @required annotation.

Errors reduced by 13.

unnecessary_null_comparison_assert_comparison.png

not_initialized_non_nullable_variable

Description - The non-nullable variable must be initialized. Compiler forces you to initialize a non-nullable variable in a class.

Where is this error - ScreenUtils

Solution - By adding the late modifier to the fields, we can tell the compiler that these variables will be initialized later. Add late for those variables. For instance, we will add late for those variables which will be initialized in the init method. If we don't compiler is there to correct us anyway. Because these are non-nullable variables, that's why the compiler enforces us. If this is a nullable variable, the compiler understands. Errors reduced by 12.

not_initialized_non_nullable_variable_screenutil_comparison.png

not_initialized_non_nullable_instance_field

Description - The non-nullable instance fields must be initialized. Like variables, the compiler also applies the same rules to instance fields.

Where is this error - ScreenUtils & Any screen where Cubit is initialized in initState but declared as an instance variable.

Solution - By adding late modifier to the instance fields as well. By this, we also make _instance non-nullable because we will initialize it in init() anyway. Together with this, we also alter the setSp() to work with the default false value. Because bool cannot be null. In the case of screens, we are sure that we will initialize cubits. Errors reduced by 34.

not_initialized_non_nullable_instance_field_screenutil_comparison.png

argument_type_not_assignable

Description - The argument type 'String?' or any nullable can't be assigned to the parameter type 'String' or any non-nullable.

Where is this error - This can be found at any place where we have explicitly declared a non-nullable type but while calling we call with a nullable type. Here, the compiler will not allow us to assign a nullable type to a non-nullable type unless you use the bang operator.

Solution - This has multiple instances, hence multiple solutions. Let's take it one by one quickly.

Where - Text widget

Solution - Text widget can not take a null string, that's why you cannot assign a nullable string to it. So, always us ??. Errors reduced by 4.

argument_type_not_assignable_text_comparison.png

Where - ScreenUtil and SizeExtension, All places where we have dimensions

Solution - Multiple changes - Convert the SizeExtension to work on double, instead of num. We are sure that this method will not return a nullable value because we will initialize ScreenUtil before MaterialApp so we will not use ? here. Use double instead of num in ScreenUtil for width, height and sp. Errors reduced by 88.

argument_type_not_assignable_size_comparison.png

not_initialized_non_nullable_instance_field_cubit_comparison.png argument_type_not_assignable_screenutil_comparison.png

invalid_null_aware_operator

Description - The receiver can't be null, so the null-aware operator ?.

Where is this error - Wherever we are using the ?. operator on a non-nullable variable.

Solution - Remove all ?. for non-nullable, as initially we used them to be null safe, but now we decided that they can not null. Errors reduced by 13.

Logical Issues

Description - We decide to not use movies that have some mandatory things as null, like id, title, poster path, backdrop path.

Where is this error - Data Sources

Solution - Decide to use the default value and valid movie. Return only valid values as decided before when making all nullable fields in widgets. While parsing, if a non-nullable field is coming as null from API, I am defaulting it to a value. Then, in the data source, I am validating whether a movie should be part of UI or not by filtering out only those movies which have id, title, and posterPath.

logical_errors_model_change.png

logical_errors_datasource_change.png

Miscellaneous Errors (189)

I have covered almost all types of errors and solutions with you. Now, some miscellaneous ones are left. They are mostly because of newer versions of plugins, some flutter SDK breaking changes. Find below the findings:

  1. When the value is retrieved from a map, it can be null so we have to declare it as nullable. Refer to MovieApp.
  2. The type MockBloc is declared with 2 type parameters, but now we use MockCubit as per 8.0.0 bloc_test.
  3. Bloc Test - The expect parameter takes a bloc now as a parameter.
  4. The argument type Function can't be assigned to the parameter type void Function()?. For this, use () ⇒ efficiently otherwise there will be infinite calls to the function. In the last commit of 21_null_safety for this app, I have fixed some issues related to function calls.

Finally, I have explained everything I had planned for Null Safety. Thanks for reading till the end. It was a lengthy one, but I believe this tutorial can act as a reference guide for errors while migrating and possible solutions based on situations.

To summarise very quickly, we used all the operators that Dart 2.12 has given us. Together with this, we created a ground rule that our widget will not work on null values and we will filter out the movies which do not have data that is very important for our UI. The rest is simple, IDE is gonna always help you with proper messages. And, I must admit the error messages provided by the dart team to understand what needs to be done for a certain error are excellent.

Thanks again for reading. Take care. See you in the next tutorial. 👋👋

Did you find this article valuable?

Support Prateek Sharma by becoming a sponsor. Any amount is appreciated!