Non-Nullable Dart: Understanding Null Safety

Learn how to use null safety in Dart. Get to know Dart’s type system and how to utilize language features in production code. By Sardor Islomov.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Understanding the Never Type

Never is at the bottom of the Dart type system. It has no value. You don’t really use Never in your code; when an expression returns Never, the program will throw an exception or abort when the execution reaches it.

However, for the sake of this tutorial, you’ll use Never to test a scenario when the app promotes errors to the user interface because it encounters an unhandled exception.

Add checkRelation() to Friend. This method checks whether relation is defined:

String? checkRelation() {
  //1
  if (relation != null) {
    return relation;
  //2
  } else {
    relationIsNotDefined();
  }
}

Never relationIsNotDefined() {
  throw ArgumentError('Friend relation is not defined');
}

This is how the method works:

  1. This condition checks whether relation is null. If it isn’t null, the condition returns relation.
  2. If relation is null, the condition calls relationsIsNotDefined(), which throws an ArgumentError exception. Notice how the code is not wrapped in a try-catch statement. Never signals flow analysis that the app will throw an exception when it reaches relationsIsNotDefined().

Understanding Flow Analysis

Flow analysis is a mechanism that determines the control flow of a program. Dart uses it most of the time at runtime for type promotion and code reachability analysis.

Flow analysis helps you write null-safe code. By analyzing the code at compile time, it prompts you to handle nullable types better in order to avoid NullPointerExceptions. It comes embedded in the Dart language.

In summary, the main responsibilities of flow analysis are:

  • Reachability analysis, which is the process of evaluating a function or expression.
  • Code warnings.
  • Null checks at compile time and runtime.
  • Type promotion.
  • Ensuring you assign values to all local and global variables.
Note: Read more about how it works in the official flow analysis documentation.

Testing the Never Type

Call checkRelation() inside _addMember() in _AddMemberPageState. Before calling checkRelation(), you need to cast _person to Friend using as. Make it the last call in the else block:

  (_person as Friend).checkRelation();

Build and run. Go to the Add member page and fill in the information:

Adding a new user with no relationship

Don’t provide a value for Friend Relation. Press Add member. Your program should throw an exception.

When you open your Dart analysis terminal, you’ll see the exception:

Illegal argument exception when Friend Relation is empty

To add a friend to the list, comment out the call to checkRelation(). Now, hot restart the app and submit the information. The app will work as it did before.

Using Type Promotion

To display user names on the home screen, you need to write two methods that filter the list of people into separate groups: friends and family members. Each method appends the names of the people to the names variable.

Open data_manager.dart inside lib/utils. Define these methods as static in DataManager:

import 'package:profile_app/model/family_member.dart';
import 'package:profile_app/model/friend.dart';


class DataManager {

  DataManager._();
  static List people = List.empty(growable: true);

  static void addPerson(Person person) {
    people.add(person);
  }


  //1
  static String getFamilyMemberNames() {
    var names = '';
    for (var i = 0; i < people.length; i++) {
      final person = people[i];
      //2
      if (person is FamilyMember) {
        names += '${person.name} ${person.surname},';
      }
    }
    return names;
  }
  //3
  static String getFriendNames() {
    var names = '';
    for (var i = 0; i < people.length; i++) {
      final person = people[i];
      //4
      if (person is Friend) {
        names += '${person.name} ${person.surname},';
      }
    }
    return names;
  }
}

The code above:

  1. Goes through the list of people to get only FamilyMembers, then puts their names into names.
  2. Checks if person is of type FamilyMember. Notice how you don’t have to cast person to FamilyMember.  Automatic type promotion takes care of that. Dart automatically promotes person to FamilyMember, allowing you to access its properties and methods.
  3. Similar to the method you used to get FamilyMembers, except it gets Friends instead.
  4. Checks if person is of type Friend, then, if true, automatically promotes person to the type Friend. This allows you to access the methods and properties available to Friend.

Displaying Member Names on the Home Screen

To display the names of the Family members and Friends, you need to define _updateNames() in _HomePageState, which you’ll find in home_page.dart.

void _updateNames() {
  setState(() {
    _friendNames = DataManager.getFriendNames();
    _familyMemberNames = DataManager.getFamilyMemberNames();
  });
}

This simply assigns friend and family member names to _friendNames and _familyMemberNames using DataManager. setState() rebuilds the widget tree.

Inside _HomePageState, you’ll find IconButton. Call _updateNames() in .then():

IconButton(
      icon: const Icon(Icons.add_circle),
      onPressed: () {
          Navigator.of(context)
              .push(MaterialPageRoute(
                   builder: (context) => const AddMemberPage()))
              .then((value) => {_updateNames()});
}),

This updates the names in _HomePageState after you add a member to AddMemberPage.

Build and run. Go to the Add member page to add a friend:

Add a new member to see them on the home page

Add family members to the user profile:

Add new family member

On the home screen, you can now view the people you added in the friend and family sections:

Added members

At this point, you’ve set up the two screens, but you still can’t see the member details in the dialog. You’ll address that next.

Displaying User Details in the Dialog

To show user data in the dialog, you first need to finish setting up User. Specifically, you need to add friendsAndFamily to User:

late List friendsAndFamily;

Although friendsAndFamily could be either nullable or late, you declared it late to avoid null checks and casting before you use it.

Retrieving User Input From the TextFields

Declare _user in _HomePageState:

User? _user;

When a user taps Save & Preview, it triggers _displayUserInfo(). Add this block of code to _displayUserInfo() to collect the information the user entered:

void _displayUserInfo() {
  // 1
  final name = _nameController.text;
  final surname = _surnameController.text;
  final birthDate = _birthDateController.text;
  final gender = _dropdownValue;
  //2
  _user = User(name: name, surname: surname, birthDate: birthDate, gender: gender);
  //3
  _user!.friendsAndFamily = DataManager.persons;

  //4
  _showPreview();
}

Here’s what the code does:

  1. Retrieves user-provided data from the TextFields.
  2. Creates a new _user.
  3. Assigns a list of people to _user. However, before you assign anything, you need to ensure that _user isn’t null. To do this, you use the Postfix null assertion bang operator, !, to cast the nullable _user to its non-nullable type. This is called casting away nullability. But if _user is not null from this code, why do you need to cast it to a non-nullable type? Because you declared that _user is nullable. Consequently, Dart believes it might have been assigned a null somewhere in the code. To be on the safe side, Dart requires you to cast it before you can access its properties.
  4. _showPreview() is a predefined method that shows user information in a dialog.