Implementing Immutability In Dart Utilising Freezed

Implementing Immutability In Dart Utilising Freezed

Read this short article to learn the basics about making classes immutable in Dart applications

When I came across this topic of immutability in Dart, I wondered why would I ever want to make my classes immutable. Making a class immutable denotes that an object can not be changed after it has been created. I found the answer to my question after exploring Dart for a few months.

When we pass any value and alter it in most of the other programming languages, it doesn't affect the original value of the object; this works differently in Dart!

In this article, we will look into how to manage data in our Dart application in a consistent and efficient manner.

The objectives of this article would be :

  • Understand the importance of making a class immutable
  • Changing data in immutable classes
  • Implementation of Freezed for immutable classes.

The use case :

When a user who is logged into the application changes any of his details, a call is made to the database to store these changes and subsequently update the details of the user in the UI.

  • Let's create a User class using the code given below:
class User {
  String name;
  int age;
  User(this.name, this.age);
}
  • Now, let's write a code to update the user:
void main() {
  User myUser = getUserFromDb();
  var result = changeUserInDb(myUser);

  if (result is User) {
    myUser = result;
  } else {
    /// Nothing
  }

  print('New Name : ${myUser.name}');
}

getUserFromDb() {
  return User('Nitesh', 22);
}

changeUserInDb(User user) {
  /// Give the user a new name
  user.name = 'Akhil';

  /// Save the user to Db

  /// If call to db is successful, return user;
  return user;

  /// else null;
  // return null;
}
  • When we execute this, the information of the user is retrieved from the database.

  • Then, we update the details of the User and based on whether the update in the database was successful or not, the details are updated in the UI.

  • When the update is successful, i.e., when we return a user from changeUserInDb(), it is noticed that everything works as expected.
  • It is also observed that the user's details are updated even if we had received a null from changeUserInDb() and the database update has failed.

How did this happen?

The reason for this occurrence is because the object instances are passed by reference in Dart .

When we changed the name of our user inside the changeUserInDb() function, we had also changed our logged in user ('MyUser') which wasn't the desired result.

How to solve this ???

The solution to this problem is simple! We simply don't need to change any details of the object. We can update the object by creating a new object every time we want to change the value of of our previous object. Thus, making all the objects immutable in nature.

  • Considering this is a long process, we can use the copyWith method which will simplify this process. Here is the code to execute the above explanation:
@immutable
class User {
  final String name;
  final int age;
  User(this.name, this.age);

  User copyWith({String? name, int? age}) {
    return User(name ?? this.name, age ?? this.age);
  }
}
  • After adding the final keyword making the objects immutable in our User class, we wont be able to modify its properties like we did earlier. To make any further conversions, will have to change the changeUserInDb() function using the code given below:
changeUserInDb(User user) {
  ...
  // user.name = 'Akhil'; /// Instead of this
  user = user.copyWith(name: 'Akhil'); /// Add this
  ...
}
  • After executing the code given above, the logged-in user doesn't change even if we receive a null from changeUserInDb(). It will only change when we return an object from changeUserInDb().

To integrate with Freezed

You might be wondering why we need to use a package when we can execute it manually on our own. This is because Freezed generates the code we will need to make our class immutable along with providing a few extra benefits like overriding the toString() method and the == operator. In the example that we have discussed, the User class is small. This is usually not the case in real-life scenarios which make it impractical to code manually. In such a cases, we can resort to using a package like Freezed.

  • We need to add the code given below to our pubspec.yaml to integrate our with Freezed:
dependencies:
  freezed_annotation:
dev_dependencies:
  build_runner:
  freezed: ^0.14.1+3
  • Now create a new file called new_user.dart and import the following statements:
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'new_user.freezed.dart';
  • The next step is to add the definition of the class we want Freezed to complete for us. Use the code given below:
@freezed
class User with _$User {
  factory User(String? name, int? age) = _User;
}
  • Now, we need to run a command that will generate our new_user.freezed.dart file using flutter pub run build_runner build
  • After this, we can import our new_user.dart file and use the new_user.freezed.dart code generated with it. There are no restrictions with Freezed while defining the constructor and we can add all the usual types of constructors that we use normally, i.e.:
  factory User({String? name, int? age}) = _User;
  • On the other hand, we cannot define the default constructor as we do in normal classes. To define something with a default value, we need to add a @Default label to the optional parameter as shown below:
  factory User(String? name, {@Default(42) int? age}) = _User;
  • If we want our class to have some extra methods, then we first need to provide a private constructor in place without which we wont be able to define methods. Post that, we can add the methods we want as shown below:
@freezed
class User with _$User {
  User._(); // Private constructor added
  factory User(String? name, {@Default(42) int? age}) = _User;

  int ten() {
    return 10;
  }
}

Conclusion

The code generated from Freezed also overrides the toString() method and the == operator because of which we don't receive an object instance. Instead, the code just prints all the properties of the object which we can use to compare instances.

There is a lot more to Freezed, which can be found here, but these are the most common things you'll need to start off. Hope you had an enjoyable read!

Thanks for reading.