Understanding Null-Safety in Flutter with Real-world example

Jamie
ITNEXT
Published in
10 min readDec 15, 2021

--

In this article, we will see how to implement Null-Safety under different conditions in Flutter with a real world service example.

Watch Video Tutorial

Part I

Understanding Null Safety in Flutter with real world example

Part II

Understanding Null Safety in Flutter with real world example

First create a screen to show the data.

Let’s create a class HomeScreen which can look like

class HomeScreen extends StatefulWidget {  const HomeScreen({Key? key}) : super(key: key);  @override
_HomeScreenState createState() => _HomeScreenState();
}class _HomeScreenState extends State<HomeScreen> { // .... your code}

You might or might not get an error in the below are depending on how your project was created. Your project might have Null Safety support or not. If not, you will get an error in ‘Key?’, this means that the null safety is no supported in your project and let’s start fixing it.

Set up the project for Null-Safety

To support Null-Safety, go to your ‘pubspec.yaml’ file and update the environment to

environment:
sdk: ">=2.12.0 <3.0.0"

Call ‘flutter packages get’ to install all the dependencies.

Project min supported sdk version must be ‘2.12.0’.

Great, now your project supports Null-Safety.

Now the error in the HomeScreen for the “Key?” should go away.

Awesome, the first step successfully completed.

Parse Data from a Service

Let’s use the below service.

https://jsonplaceholder.typicode.com/users

It is going to return 10 records each in the below record format

{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},

Let’s generate the model class for the above json.

Go to app.quicktype.io and paste the whole response from the above service and name the file as ‘User’, You should get something like this below.

import 'dart:convert';List<Users> usersFromJson(String str) => List<Users>.from(json.decode(str).map((x) => Users.fromJson(x)));String usersToJson(List<Users> data) => json.encode(List<dynamic>.from(data.map((x) => x.toJson())));class Users {
Users({
this.id,
this.name,
this.username,
this.email,
this.address,
this.phone,
this.website,
this.company,
});
final int id;
final String name;
final String username;
final String email;
final Address address;
final String phone;
final String website;
final Company company;
factory Users.fromJson(Map<String, dynamic> json) => Users(
id: json["id"] == null ? null : json["id"],
name: json["name"] == null ? null : json["name"],
username: json["username"] == null ? null : json["username"],
email: json["email"] == null ? null : json["email"],
address: json["address"] == null ? null : Address.fromJson(json["address"]),
phone: json["phone"] == null ? null : json["phone"],
website: json["website"] == null ? null : json["website"],
company: json["company"] == null ? null : Company.fromJson(json["company"]),
);
Map<String, dynamic> toJson() => {
"id": id == null ? null : id,
"name": name == null ? null : name,
"username": username == null ? null : username,
"email": email == null ? null : email,
"address": address == null ? null : address.toJson(),
"phone": phone == null ? null : phone,
"website": website == null ? null : website,
"company": company == null ? null : company.toJson(),
};
}
class Address {
Address({
this.street,
this.suite,
this.city,
this.zipcode,
this.geo,
});
final String street;
final String suite;
final String city;
final String zipcode;
final Geo geo;
factory Address.fromJson(Map<String, dynamic> json) => Address(
street: json["street"] == null ? null : json["street"],
suite: json["suite"] == null ? null : json["suite"],
city: json["city"] == null ? null : json["city"],
zipcode: json["zipcode"] == null ? null : json["zipcode"],
geo: json["geo"] == null ? null : Geo.fromJson(json["geo"]),
);
Map<String, dynamic> toJson() => {
"street": street == null ? null : street,
"suite": suite == null ? null : suite,
"city": city == null ? null : city,
"zipcode": zipcode == null ? null : zipcode,
"geo": geo == null ? null : geo.toJson(),
};
}
class Geo {
Geo({
this.lat,
this.lng,
});
final String lat;
final String lng;
factory Geo.fromJson(Map<String, dynamic> json) => Geo(
lat: json["lat"] == null ? null : json["lat"],
lng: json["lng"] == null ? null : json["lng"],
);
Map<String, dynamic> toJson() => {
"lat": lat == null ? null : lat,
"lng": lng == null ? null : lng,
};
}
class Company {
Company({
this.name,
this.catchPhrase,
this.bs,
});
final String name;
final String catchPhrase;
final String bs;
factory Company.fromJson(Map<String, dynamic> json) => Company(
name: json["name"] == null ? null : json["name"],
catchPhrase: json["catchPhrase"] == null ? null : json["catchPhrase"],
bs: json["bs"] == null ? null : json["bs"],
);
Map<String, dynamic> toJson() => {
"name": name == null ? null : name,
"catchPhrase": catchPhrase == null ? null : catchPhrase,
"bs": bs == null ? null : bs,
};
}

In the above model, we don’t have any null-safety implemented. but we are gonna do it now.

Different ways to implement Null-Safety

Let’s look at one of the variables here in the below object, the ‘name’.

So you are going to get a bunch of errors for each variable declared.

But, don’t worry, we are here to fix all of those.

Users({
this.id,
this.name,
this.username,
this.email,
this.address,
this.phone,
this.website,
this.company,
});

Here, the name is declared as

final String name;

To fix the error, you have provide an initial value to the ‘name’ variable.

So how to we do that?

Since this is coming from service and the service can have either a null value or an empty value or a proper name. There might be situations where the service might not return the ‘name’ key at all.

So we have 3 problems here…

How do we manage this in terms of Null-Safety?

If the value is null, the Text widget that we are displaying the name value might result in error because text widgets wont accept a null value.

Option 1:

Make the name variable nullable. In this case make sure that you have logic in place to handle null value in the Text widget in UI.

To make it nullable we will add ‘?’ in front of the type of the variable, like this

final String? name;

This means that the name variable can be null.

The error in the name variable constructor should be gone now.

In Flutter you cannot explicitly initialize a variable to null.

Now in the place you are trying to use this variable, you may need to unwrap to get the value from the Nullable variable. if you are trying to use just ‘name’, your Text widget is going to complain that it cannot accept a null value.

Text(name!)

Notice the ‘!’ symbol at the end of the ‘name’ variable. This will unwrap the nullable name variable to get the value and display in the UI.

Warning!!!

What if the value of name is actually ‘null’ from the service, this will cause your UI to break. So you should avoid using ‘!’ in your production code.

Use ‘!’ only when you are completely sure that the value will never be null.

If the value is actually ‘null’ it can result in ‘null check on a null value’ error.

So try to avoid it as much as possible.

Option 2:

I don’t want my name to have a null value anytime. then???

If the variable is null from service, we can give it a default value. Let’s set it to empty if the value from service is null. In that way the name variable can never be null even if the value from the service is null.

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json["id"] == null ? null : json["id"],
name: json["name"] == null ? '' : json["name"]

This means, if the json[“name”] is null from service, we will set the value to ‘’ (Empty). So we can avoid unnecessary logic to prevent null in the UI.

Let’s make it little more short while parsing…

name: json["name"] ?? ''

The ‘??’ can replace the null check.

The default value must be a constant value.

What if there is no ‘name’ key from the service, but we were expecting?

In this case we can give a default value in the constructor itself.

User({
this.id,
this.name = '',
this.username,
this.email,
this.address,
this.phone,
this.website,
this.company,
});

Finally, if this is your model that you are constructing, you can set the parameter as ‘required’ like below and also make you are already to accept a null value or not if you are not adding ‘?’ at the end, that means, even if it is a required parameter, it can accept null. Here I have made the ‘id’ field as a ‘required’ parameter and didn’t addd ‘?’ at the end. So whenever we create a ‘User’ object, we are force to supply a parameter that is ‘non-null’ in value.

User({
required this.id,
this.name = '',
this.username,
this.email,
this.address,
this.phone,
this.website,
this.company,
});
final int id;
final String name;
final String? username;
final String? email;
final Address? address;
final String? phone;
final String? website;
final Company? company;
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json["id"] == null ? null : json["id"],
name: json["name"] ?? '',
username: json["username"] == null ? null : json["username"],
email: json["email"] == null ? null : json["email"],
address:
json["address"] == null ? null : Address.fromJson(json["address"]),
phone: json["phone"] == null ? null : json["phone"],
website: json["website"] == null ? null : json["website"],
company: json["company"],
);
}

In the above code, we have made the ‘id’ a required parameter and other parameters as Nullable, but with proper checks for null except for ‘Company’ which is an Object itself.

Now let’s see how can provide a default value for Company.

This is how the Company class looks like

class Company {
Company({
this.name,
this.catchPhrase,
this.bs,
});
final String name;
final String catchPhrase;
final String bs;
}

Just like name, either we can accept a null value from the service or provide a default value.

  1. Assuming that we are ready to accept null from service, then there is no change in the above code.
final Company? company;

this works.

How to we use it?

Since ‘company’ is nullable, we need to unwrap it.

Text(user.company!.name!), // not recommended

2. We are not accepting null from Service, in that case we need a default value.

For Flutter the default values must be a constant, just like we set ‘’ for name.

In-order to do that we have to make the ‘Company’ constructor constant.

class Company {  const Company({
this.name = '',
this.catchPhrase,
this.bs,
});
final String name;
final String? catchPhrase;
final String? bs;
}

So we made the const constructor, and also set the name to ‘’ as default value(not necessary).

Let’s create a const default company value.

const defaultCompany = Company(bs: '', catchPhrase: '', name: '');

Here we are setting all the values to empty.

Now we can set the value in the constructor as default value if there is no company key in the response from server and also if the server sends a null value for the ‘company’ key.

class User {
User({
required this.id,
this.name = '',
this.username,
this.email,
this.address,
this.phone,
this.website,
this.company = defaultCompany,
});
final int id;
final String name;
final String? username;
final String? email;
final Address? address;
final String? phone;
final String? website;
final Company company;
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json["id"] == null ? null : json["id"],
name: json["name"] ?? '',
username: json["username"] == null ? null : json["username"],
email: json["email"] == null ? null : json["email"],
address:
json["address"] == null ? null : Address.fromJson(json["address"]),
phone: json["phone"] == null ? null : json["phone"],
website: json["website"] == null ? null : json["website"],
company: json["company"] == null
? defaultCompany
: Company.fromJson(json["company"]),
);
}
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
"username": username == null ? null : username,
"email": email == null ? null : email,
"address": address == null ? null : address?.toJson(),
"phone": phone == null ? null : phone,
"website": website == null ? null : website,
"company": company.toJson(),
};
}

The whole user object will look like above.

Here we set the default value in constructor like this

this.company = defaultCompany,

And also where we parse the json from server, so if the server sends null value, we set it to ‘defaultCompany’.

company: json["company"] == null
? defaultCompany
: Company.fromJson(json["company"]),

How do we use it now?

Text(user.company.name),

Much cleaner, is n’t it?

What if just the company was nullable and name inside the company object is not? then

// not recommended because company can be
// null and textview won't accept it.
Text(user.company!.name),

If you set it to nullable and handle in UI, then

Text(user.company!.name! ?? ''),

this way, even the value is null, we are doing a null check in the UI and setting it to empty.

We can also do like this

Text('Address ${user.address?.city}'),

In this case if the address object is null, flutter stops accessing the city variable and returns null. In this way we can avoid causing the ‘null check on null value’ error. But remember still the value is null in Text Widget.

Now let’s declare some variables in the HomeScreen widget for displaying the values from service.

class _HomeScreenState extends State<HomeScreen> {

List<User> _userList;
String _error;
...

This will result in error because we have not initialized both variables.

Let’s fix this.

// Make it nullable
List<User>? _userList;
String? _error;
// or give default values
List<User> _userList = [];
String _error = '';
// or use lateinit
late List<User> _userList;
late String _error;

If you are using lateinit, make sure to initialize it before accessing it.

Here we can initialize it in the initState of the HomeScreen.

class _HomeScreenState extends State<HomeScreen> {

List<User> _userList = [];
late String _error;
@override
void initState() {
super.initState();
_error = '';
}
...

Our whole HomeScreen will look like this if we are setting default values.

class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {

List<User> _userList = [];
String _error = '';
@override
void initState() {
super.initState();
_getUsers();
}
_getUsers() async {
var result = await UserServices.getUsers();
if (result is Success) {
_userList = result.response as List<User>;
setState(() {});
return;
}
if (result is Failure) {
_error = result.errorResponse as String;
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Users'),
),
body: Container(
child: Column(
children: [
Visibility(visible: _error.isNotEmpty, child: Text(_error)),
ListView.separated(
padding: const EdgeInsets.all(20.0),
shrinkWrap: true,
itemBuilder: (context, index) {
return UserListRow(
user: _userList[index],
onTap: (User user) async {
user.address!.city = null;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserDetailsScreen(user: user),
),
);
},
);
},
separatorBuilder: (context, index) => Divider(),
itemCount: _userList.length,
),
],
),
),
);
}
}

Beware that if you set the _error to be nullable or null value, then Visibility widget will still render the child inside it. Visibility widget is meant to just make the widget visible and invisible even if the value of child is null. if the child is null, it will result in error.

Make sure to checkout my youtube channel for more detailed practice explanation.

You can clap up to 50 if you find my article useful.

Or Buy me a coffee.

Source Code

Thanks

--

--