Apr 25, 2022
8 min read
Immutability & Equality in Flutter & Dart [Functional Programming — Part 4]
Previously, we covered how to build modular and scalable applications using composition. This article covers immutability and equality in Dart & Flutter.
Immutable?
Firstly, Mutable: something that can change over time. For instance, when we define a variable int a = 10
, it is susceptible to change. a=20
is entirely valid. In contrast, we have immutability.
Immutability — as you could have guessed — is something that cannot be changed. It didn’t make much (or any) sense when I heard it the first time. What’s the use of an application that does not change its state? Now, I ask myself how I didn’t come across this earlier.
Immutability can be achieved using two keywords in Dart: final
and const
. Which to prefer when? Let’s find out!
final vs const
final
final
variables are evaluated at runtime. If we don’t intend to change the value, it is recommended to use final instead of the var keyword or specify explicit types(int, String). The Dart analyzer is smart enough to infer the types.
const
On the other hand, const
is a compile-time constant and is implicitly final. So the value must be known at compile-time and can not be reassigned.
Okay, But why should we strive for immutable data structures?
Why prefer immutability?
Fewer bugs: Changing values in place has the possibility of breaking codebases. Immutability helps to avoid accidental reassignments. With mutable, someone can accidentally update the object we were accessing, resulting in hard to trace bugs.
Code becomes predictable and easier to read.
As const evaluates at compile-time, the compiler knows the value in advance and stores it in the memory. The exact value is referred to throughout the application instead of creating new objects every time. So this helps to save some memory and has small performance benefits.
In our Flutter application, we have EdgeInsets.all(8.0)
being used at 10 places, as an example. By using const
, we tell the compiler to create only a single instance and use the same instance throughout the application where padding of 10 is required.
const
can not only be used with primitives but also with classes, lists, maps, and sets whose values do not change.
Okay, Fair enough! But if we keep using immutable data structures everywhere, how will we update the state?
How to update immutable states?
Of course, how useful is an application that does not change and only shows static content? Not much, Right? So How do we update the state then? Instead of changing values, we replace them. We create a copy of the instance we need to access and use it. It prevents unexpected side effects.
We can use the copyWith
method to make our life easier, but we need to implement it first.
copwWith
creates a copy of the instance and only updates the values passed explicitly.
Primitives like int
are by default immutable. For example, an 8 is always an 8; we can’t mutate it to any other number. In the above snippet, we saw how to make a class immutable.
Use the @immutable annotation to get analyzer support in creating immutable classes.
What about other data structures like lists, maps, etc.? Let’s find out!
Immutable lists, Maps
We can use final
and const
with lists, but there’s a caveat.
Since both lists are either final
or const
, we can not assign a new list(Ref. lines 5 and 6). However, in the case of the final, we can still add/remove the elements inside the list. We can utilize unmodifiable
factory constructors to make these data structures completely immutable.
If we try to add/remove elements, we will get the following error at runtime. Well, this certainly works but wouldn’t it be better if we could achieve this at compile time? The kt_dart package helps us with immutable data structures, but Since we’re already using dartz, we can use the IList
, which provides an immutable list.
On line no. 2, we create an IList
instance from a regular list. It is entirely immutable. To add/remove elements, we have the appendElement
and prependElement
, which adds the element at the last position and first position, respectively. It does not update the elements in place but instead returns a new list.
Equality
Before jumping in, here’s a fun exercise. Try to guess the output of the following.
Equality is the means to check if two objects are equal by using the ==
operator. Equality is of two types: Referential equality and Value equality. For example, we have a User class with two fields: name and age. When we initialize the User class, objects are created and stored in the memory. Each object has a unique memory address assigned to it.
Referential Equality returns true when two objects refer to the same object. For the objects to return true, they should point to the same object in the memory.
Value Equality returns true when both two objects have the same value. Objects can be at a different location in the memory, but if they have the same value, it returns true.
Equality operator
By default, Dart checks for referential equality. If we compare any two non-constant objects in dart with the same value, it evaluates to false. The default implementation of the ==
operator is to return true if both the objects are identical, in other words, if both the objects are the same.
To support value equality, a class needs to be:
Total: It should always return a boolean and never throw an error or return null.
Reflexive: a == a should always return true.
Symmetric: a == b should be the same as b == a. Either both should evaluate to true, or both should evaluate to false. If a is equal to b, then b should be equal to a.
Transitive: If a == b is true and b == c is true then a == c should be true.
User
, a regular class, and User2
, a class with value equality. When comparing 2 User
objects with the same value, it evaluates to false, as it uses referential equality by default. However, when comparing two User2
objects with similar values, it evaluates to true as we have implemented the value equality.
But wait, that’s not all!
hashCode
While overriding the ==
operator, we need to make sure to override the hashCode. But first, what’s a hashCode anyway?
The hashCode is of type int
and represents the state of the object. The default implementation represents the identity of the object.
If the ==
operator is overridden, we should also override the hashCode to represent the state; otherwise, the objects can not be used in hash-based data structures like Map.
hash_and_equals add this in your analysis_options.yaml to get analyzer support for hashCode.
List, Map, Set Equality
Collections like lists and maps, when compared, checks for referential equality. Methods like listEquals
and mapEquals
are to our rescue when we need to check for value equality.
To check element by element, we’ll need deep equality provided by the collection package via DeepCollectionEquality
, and it is one of the packages that the Flutter uses as a dependency. It supports two modes: ordered and unordered.
If we’re using IList
, we get the value equality support by default.
Now will be a good time to reveal the answers to the above quiz.
print(1 == 1)
and print(10 == 01)
as you would have guessed returns true and false respectively.
Line no. 5 returns false because, by default, dart checks for referential equality. To support value equality, we need to override the ==
operator and hashCode
.
For lines 6–11, the expression evaluates to false. Methods like listEquals
and mapEquals
to compare the value equality or the DeepCollectionEquality
from the collection package.
For print(1 == 1.0)
and print(1 is double)
there’s a lot more going on under the hood, visit this article for more.
Final Thoughts
So, Immutability and equality will help reduce unexpected bugs and make the code predictable. At the start, it might seem like a lot of work, but it’s worth the effort, in my opinion. In the next article, we will go in-depth into higher-order functions and recursion.
Awesome! Pat yourself on the back for reaching till the end. I hope I added some value to the time you invested. Find out more examples on the GitHub repository, and reach out on Twitter or LinkedIn for suggestions/Questions or any topic you’d like me to cover. You can support it by clapping👏, and thanks for reading :) Follow for more😄
Until next time, folks!