Table of contents
- What is Null Safety?
- Null Safety in Dart
- NullSafety ≠ Easy
- NullSafety?.isEasy() ⇒ True
- Steps to Migrate
- Solve Errors
- 600+ Errors
- dead_null_aware_expression
- missing_default_value_for_parameter
- unchecked_use_of_nullable_value
- unnecessary_null_comparison
- not_initialized_non_nullable_variable
- not_initialized_non_nullable_instance_field
- argument_type_not_assignable
- invalid_null_aware_operator
- Logical Issues
- Miscellaneous Errors (189)
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
- Use fvm to easily migrate between flutter versions.
- Install fvm by following steps mentioned here - https://github.com/leoafarias/fvm
- Switch to flutter 2 version
fvm use 2.0.0
- Change the IDE settings to use flutter 2 version now. Like in VSCode add settings:
"dart.flutterSdkPath": "/Users/prateeksharma/fvm/versions/2.0.0",
- Change dart and flutter SDK versions in pubspec.yaml
environment:
sdk: ">=2.12.0 <3.0.0"
flutter_sdk: ">=2.0.0"
- 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.
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
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 -
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.
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.
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.
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.
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.
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
.
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.
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.
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.
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.
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.
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.
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_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.
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.
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.
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
.
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:
- When the value is retrieved from a map, it can be null so we have to declare it as nullable. Refer to
MovieApp
. - The type
MockBloc
is declared with 2 type parameters, but now we useMockCubit
as per 8.0.0 bloc_test. - Bloc Test - The
expect
parameter takes a bloc now as a parameter. - The argument type
Function
can't be assigned to the parameter typevoid 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. 👋👋