Freezed 2.1.1 — Embracing Immutable Code Practices with Flutter

The Code Breaker
8 min readOct 14, 2022

--

Read this if you are interesed in easily adding undo|redo functionality to your Flutter app.

Over the last few years, discussions on coding forums and YouTube videos about using some particular immutable coding techniques has become increasingly more frequent. Immutability is the idea that an object should not change once created. For objects or classes, this also includes any variables that they contain. And by using immutable objects, we can avoid some not-so-obvious side effects. This style requires thinking about programming with a more-functional paradigm that sometimes needs a shift in our mindset.

While working with mutable objects, a classic example of a not-so-obvious side effect comes while calling a .sort function on a list of integers in descending order, i.e. [3,2,1]. Now Let’s make a newList using a method call that modifies and returns the list. It turns out that although newList seems to be correct, when we try to print out our original mutableList, it’s different than what we created. The result of doing this is that the order of, Both mutableList AND newList are in ascending order, i.e. [1,2,3].

Example 1. A classic example of a not-so-obvious side effect.

This article intends to increase your awareness of immutable programming, demonstrate a flutter example of this side effect, and then show how to fix it using the Freezed package.

Learning Intentions

  • You will learn about the Freezed Package.
  • You will learn to add undo | redo functionality to your code.
  • You will learn to recognise mutable code.
  • You will learn to refactor to immutable code

Introducing The Minimal Viable App

First, let us understand the app we will convert using the freezed package.

  • This app stores class objects in a list — objects have nested data — .
  • This app cycles between class objects in the list using undo and redo buttons.
  • This app displays the nested data for the current state in the UI.

The class objects have a nested structure that is something like this:

List<NestedClass>
|-- List<NestedClass>
| |-- List<NestedClass>
| | |-- List<NestedClass>

A purple app diagram shows the intended functionality related to the UI. In summary, the app bar will display which class object is in the current state. i.e.

  • When the widget has the index of the class object that is stored first in the list, the app bar will read, ‘Page 1’. If we change the index of the list to that of the second stored class object, the app bar will display ‘Page 2’, etc.
  • The ListView will display the information stored by the class object and any deeply nested data.
  • The Undo and Redo buttons will have icons instead of words and will cause the app to cycle between class objects stored in the list.
A purple app diagram showing the intented functionality of the UI.

Working Demo

GIF of Demo That is Working as Expected

PART 1. THE PROBLEM

The Beginning GitHub Code (With Side Effect)

Please understand that the code is at an essential minimum for being viable and is rudimentary. For this article, I will not go over the entire code but only portions of the code because many of the files do not encompass the discussion topic and are beyond the scope of this article. I include these files to make a viable flutter app. I recommend that you download and look at the code that is relevant to you.

The code contains a simple setup for the initial data. I did this setup intentionally to minimise the complexity of this code. It would not be present in the production code. You will find these files in the data_initialization.dart folder.

Please find the complete beginning code for this project.

Discussing The Code

The nested_class.dart contains a method that violates the immutable programming paradigm.

When we discuss any class object in this article, we are referring to the NestedClass. This class is simple. This class contains the variables: name, surname and age. It also includes a children variable, List <NestedClass>. This List allows the class object to contain deeply nested data. There are two methods: addChild and copyWith, and a NestedClass.initial constructor. The problem code here regarding immutable code is the addChild method because it alters the children’s variable by adding a NestedClass. This modification is a no-no! We cannot change any variable once initialised in an immutable coding paradigm. This code will completely be changed by the freezed package in Part 2.

The Class Objects

For clarity, here is the code for the class objects. The code explicitly hardcodes each class object. This code is unrealistic for a production app but leads to no ambiguity for this article.

load_data.dart contains the hardcoded class objects. They are individually nested in the loading_family_tree.dart snippet.

The individual class objects will be nested next.

Deeply Nesting The Class Objects

Although I would not include it in the production code in its current form, we should discuss loading_family_tree.dart because this is where the code inserts NestedClass objects into the hierarchy.

loading_family_tree.dart explicitly hardcodes the hierarchy in this rudimentary code.

The production code would be entirely different, but it does the same thing: a method is called in the class to add a member to the children’s list during the app’s initialisation. In this case, I thought it would be better to look at the code in its wildest and most primitive form to overemphasise the .addChild calls. This code explicitly hardcodes the entities into the nested hierarchy. Realise, though, that this code, whether as it currently is or in a more sophisticated format, cannot use the .addChild method call when using the immutable coding paradigm because it would cause us to modify the children variable. In this article, I will be referring to these calls, which change a variable (i.e. .add, .remove, .sort), as modification verbs.

We can learn to recognise mutability by looking for ‘modification verbs.

We need to Apply Functional Thinking To Our Code

The file that will require the most change is delete.dart. Since this is the logic centre for removing class objects from the hierarchy, let’s review it.

Functional programming is becoming more and more popular, and for good reasons. The functional programming steps for modifying data are:

Copy-on-write

  1. Make a copy of the class object to prevent it from being mutated,
  2. Modify the copy,
  3. Return the modified copy to the client.

Performing Copy-on-write to the Code

First, the code copies the passed hierarchy class object. The problem here is that although we are copying the hierarchy, we are not ‘deep copying’ it, and changes to the copiedHierarchy will indeed cause changes to the hierarchy class object.

Second, the code modifies the data by altering the children’s list with .remove(object). This modification, again, is a no-no. We will convert this code in Part 2.

Third, the code passes back the copied and modified class object to the client.

deleted.dart is going to need changing because it modifies data of all class objects.

Notably, ‘shallow copying’ does not stop mutability. The freezed package enables us to immutably ‘deep copy’ class objects.

When implementing the freezed package, we will use a different strategy for a truly functional programming paradigm.

Seeing The Unintentional Side Effect

Pressing the redo button allows cycling through the list of seven class objects. Because each class object is different from the others, we should see a change in the UI when we scroll through the list by pressing the button. However, the GIF reveals that the UI stays the same for each class object. The class objects are unexpectedly changing because they are mutable! This kind of side effect is the same one as in Example 1.

GIF showing the unitentional side effect

PART 2. THE SOLUTION

Let’s make our code immutable!

Please find the complete finished code for this project.

Freeze Your Code

There is a wonderful tutorial by ResoCoder that shows all of the steps for using the freezed package. So, use this video to change the NestedClass.

Link for using freezed data class in dart and flutter programming.

To save you some time, essentially, it’s all about using the correct format, i.e.

import 'package:freezed_annotation/freezed_annotation.dart';part 'nested_class.freezed.dart';@freezed
class NestedClass with _$NestedClass {
const NestedClass._();
const factory NestedClass({
required String name,
required String surname,
required int age,
required List<NestedClass> children,
}) = _NestedClass;
}

How I changed The Code And What You Can Do Too

Essentially there were 3 steps

  1. Add the dependencies and followed ResoCoders detailed instructions for Refactoring Your Class Object (see 3a).

Add these libraries to your pubspec.yaml file.

freezed: ^2.1.1freezed_annotation: ^2.1.0build_runner: ^2.2.1

2. Run Freezed using a console command.

dart run build_runner watch --delete-conflicting-outputs

3a. Added NestedClass.initial constructor to the Freeze revised NestedClass.

By following ResoCoder’s instructions, the nested_class.dart file becomes cleaner. You can add to the class. For example, I needed to add an initialisation constructor to accommodate my legacy code, which I like to store in my data class.

Freezed revised nested_class.dart

3b. Change the loadFamilyTree to .copyWith functionality to remove .addChild calls.

Now Let’s make our code immutable! The alternative to using modification verbs (.add or .remove call functions) with the immutable coding paradigm is to use .copyWith instead.

Freezed revised load_family_tree.dart

3c. Change the delete method with functional programming techniques.

I applied the copy-on-write technique to produce truly immutable code.

The tricky part of refactoring this code was getting the deletion logic correct compared to the legacy logic. The legacy way was simple. Essentially the code recursively looked through the hierarchy and then used .remove(object) to eliminate the class object from the children variable’s list. The logic had to change because it could not use ‘modification verbs’ to change the data.

Having To Rethink Coding For The Paradigm

Instead, the new version is much more reliant on using the goodness of .copyWith.

This evaluation happened by creating a new null-able class object (copiedHierarchy) using a ternary operator. The logic changed to determine whether the parent class object contained the sought-after class object. If the predicatehierarchy.children.contains(object) — was true, it returned a parent class object. If the predicate was false, it returned a null value.

Conditionally Copy The Hierarchy With A Ternary Operator Assignment.

Downstream of the previously discussed ternary operator assignment, the null-able copiedHierarchy object encounters an if-else statement searching for a null or class object of the copiedHierarchy. Here, we want to change the children of the hierarchy if it is the object we want to delete. So the code works something like this. If the returned class object was null, the logic resulted in the return of a new list of children — without the class object.

Downstream Conditionally Modify The Children Of The Hierarchy.

Step-wise Logic

  1. Ternary operator assignment — If the parent contains the class object, pass it or pass a null.
  2. Downstream — If there IS a class object, a new list of the children is made excluding the object. Then, copyWith the class object with the latest list of children and return the copy.
  3. Downstream — If there IS a null, copyWith the hierarchy returns an unmodified copy.
  4. Safe Recursion — There is a recursive call where the function calls itself.

The Proof Is In The Pudding

The code works as expected!

Demonstration Of The App Working As Expected.

Conclusion

Freezed is an excellent addition to any programmer’s toolbox. Please consider using it in your next project.

I hope this article has opened your mind to immutable programming and using some functional programming tenets. I have demonstrated some unexpected side effects that could cause bugs in your code that arise from unexpected mutability. The freezed package is easy to use and helps flutter developers to produce safer modern immutable code. The trends of several online communities have been shifting to using more and more immutable programming techniques, and I only foresee them becoming even more mainstream. Happy coding!

The official Flutter Video for Freezed

--

--