Intro
So, as a coder, using conditionals, switch cases, and operators are a daily kinda thing, & as a developer/coder, we should know how these things work internally. Whenever we are developing something we don't really focus on how things work, do we? And it's fair cause mostly the focus is to get the feature running, doesn't matter how.
Whenever I am developing a feature or fixing a bug, I also focus on getting it working, but at the same time, I note that new concept I am learning enough to implement the feature to explore it in my free time. But it doesn't happen despite getting free time, reason? well, it doesn't seem that important. So, pro tip, always try to cover & learn as much as you can while it's important i.e. whenever you are working on the implementation. It will definitely help you as a developer in the long run. I do the same nowadays.
I know it's been quite a time since my last blog, I was willing to maintain consistency but joining a new organization & the struggle to sustain there in your initial phase is kinda crucial, so, had to keep it the only priority & hence couldn't post any content. While I was focusing on the job, I realized that as a developer, whenever we are learning some tech as a fresher, the first focus is to learn the easy practices to develop end-to-end products, but as we get more & more proficient in it, the focus switches to understanding concepts & best practices in that tech to deliver scalable products.
It took quite a bit of time to decide on what to explain on the very first blog, but with simplicity & a bit of relevance & I came up with the equality concept. Think of it, any level of developer be it just a programming enthusiast or a pro developer, everyone uses equality in their day-to-day life so it has huge relevance right? So, without further ado, let's dive into it...
Equality in General:
The very first & easiest question is, what is equality...? two things possessing equal value are called to be equal, & we represent it by the "==" sign in nearly all programming languages. In some places, "===" is used to check equality as well as the data type used,
e.g. if you equate integer 1 with String "1" then they both are equal w.r.t. "==" but they aren't for the "===" operator. This can be understood easily in the below code example,
var a = 1;
var b = 1.0;
console.log(a == b) // true
// string & integer are different datatypes, & therefore despite of havins equal value, value comes to be
console.log(a === b) // false
Cool, isn't it? well if you think this is it.... well, wait for it.
Specific to Flutter/Dart:
To specifically talk about Dart/flutter you might've heard something like value equality as well as referential equality & trust me when you'll develop Flutter applications following standard MVVM structure or say, clean architecture, you'll be developing data models (classes containing API data). And maintaining equality of these kinda class objects might become complicated if you already aren't familiar with referential & value equality.
Before understanding them deeply, we'll go through the definitions of each,
Referential Equality: Two objects are said to be referentially equal if all the values of the reference to the objects are equal.
Value Equality: Two objects are said to have value equality if all the values included in them are equal, doesn't matter what the references are.
Whenever we create an object from a class, the object is mapped to a hashcode internally & that hashcode works as a reference to our object. A hashcode is a pseudo-random number generated by DartVM while creating the object. It works just like hashmaps, where the key is hashcode & the value is the object (instance of the class). So, as the hashcode is unique for each object, so if two objects possess the same hashcodes, we could easily say that they both are equal without even checking the values. -> Referentical Equality
But this isn't everything, cause each created object will contain a unique hashcode, so if we create two objects with the same values, still they will have different hashcodes & hence will be considered not equal. The example below shows exactly the same thing,
class Temp{
final int value;
const Temp(this.value);
}
void main() {
final a = Temp(1);
final b = Temp(1);
print('${a.hashCode} ${b.hashCode}');
print(a == b); //false
}
Then the question would be, how to create multiple objects with equal values & same hashcode for all? Well, Dart is very smart in that case, if we create const objects with the constructor defined in the class, then Dart creates only one object in the memory for as many const variables as we want.
If you compare the example above & below, you'll notice that I've just made the variables constant, & magic happened. DartVM created only one object in memory & just shared its reference(hashcode) with the variables, In conclusion - the objects are equal now, check in the compiler yourself.
class Temp{
final int value;
//const constructor
const Temp(this.value);
}
void main() {
//const variables
const a = Temp(1);
const b = Temp(1);
print('${a.hashCode} ${b.hashCode}');
// result is true now
print(a == b); //true
}
So, the objects we're comparing are equal, wasn't that our objective? but, have we completely achieved it? definitely not. Let me ask you another important question, are we going to be using const variables or const constructors throughout the application?
In every application, we get data from local storage, from API's or from other means, & fetching that data objects to a variable won't be possible implementing with const variables as these are asynchronous tasks. So, mostly we will be using non-constant (final mostly) variables which will definitely be equipped with different hashcode values. We've discussed this situation already in the first dart code box above.
To counter this issue, value equality comes into the picture, in Dart, value equality is something we need to implement explicitly i.e. we need to tell DartVM that, whenever you are comparing two objects with different hashcodes, please mark them equal if the values of all fields are equal. Now, the question is, how to do it? well, there are different approaches, & we will be looking at them one by one,
By overriding the "==" operator:
Yes guys, if "==" doesn't give us the answer we will simply change the meaning of it to fit our considerations.
okay, we've decided to override the == operator, but what conditions should it check?
The objects should have the same class type
All the properties defined by the class should be equal
So, either the object should contain the same hashcode or it should follow the above cases to be called equal, & the same is implemented in the code below,
class Temp{
final int value;
const Temp(this.value);
@override
bool operator ==(Object other)=>
// identical checks referential equality
identical(this, other) ||
// object is of type Temp
other is Temp &&
other.runtimeType == runtimeType &&
// properts of both should be equal
other.value == value
;
}
void main() {
// non-constant variables are equal now
final a = Temp(1);
final b = Temp(1);
print('${a.hashCode} ${b.hashCode}');
print(a == b); // true
}
What if we could manipulate the Hashcode:
Suppose we've say 30-40 fields/properties in the class, so if we want to override the equality we will need to compare all individual values which will lengthen our code unnecessarily. We can actually avoid this by actually overriding the hashcode basis on the conditionals we want, isn't that amazing?
The hashcode override is actually a getter returning an integer value, so whatever value we want to set as out hashcode, we can save it. The standard code practices say to generate a unique hashcode by the combination of hashcodes of all the fields/properties present, makes sense, right? eventually, the hashcode will depend on the field's/property's hashcode which will be the same for equal values. The code below does the same,
class Temp{
final int valueOne;
final int valueTwo;
final int valueThree;
const Temp(this.valueOne, this.valueTwo, this.valueThree);
@override
bool operator ==(Object other)=>
// identical checks referential equality
identical(this, other) ||
// object is of type Temp
other is Temp &&
other.runtimeType == runtimeType &&
// hashcode comparision insted of three equality statements
other.hashCode == hashCode;
@override
int get hashCode =>
Object.hash(valueOne,valueTwo,valueThree);
}
void main() {
final a = Temp(1,2,3);
final b = Temp(1,2,3);
// the hashcodes of both should be same now
print('${a.hashCode} ${b.hashCode}');
print(a == b); // true
}
Following best practices with the Equatable plugin:
So, we've optimized the code by overriding the hashcode getter, but still, this is a lot of repetitive code if we think of creating say 40 model classes for some application as we will need to write the override each time & the only difference would be the fields/properties. Keeping that in mind, Equatable plugin is developed which does the same thing we just did to update the hashcode & help us write less code.
We just have to return all the fields/properties as a list by overriding the props getter. Check the code below, & how optimized & easy it is.
import 'package:equatable/equatable.dart';
class Temp extends Equatable{
final int valueOne;
final int valueTwo;
final int valueThree;
const Temp(this.valueOne, this.valueTwo, this.valueThree);
// just this props override & we are done
@override
List<Object> get props => [valueOne,valueTwo, valueThree];
}
void main() {
final a = Temp(1,2,3);
final b = Temp(1,2,3);
print('${a.hashCode} ${b.hashCode}');
print(a == b);
}
Outro
To wrap up, understanding concepts becomes essential when you achieve a certain level & begin working on production-level projects, which need following best practices & small mistakes in code could lead to major bugs in the application.
This was it for today, have fun learning & exploring, I will catch you in the next one... till then it's a farewell.