This is the second part of a new series in which we are going to learn how to build a safe, maintainable, and testable app in Flutter using fpdart
and riverpod
.
We will focus less on the implementation details, and more on good practices and abstractions that will helps us to build a flexible yet resilient app in Flutter.
Read part 1: Project Objectives And Configuration
As always, you can find the final Open Source project on Github:
In this article we are going to setup the underlining data model for the application:
- Use the new Dart 3 class modifiers (
final
) and how to make a class immutable - Add equality checks using
Equatable
- Define the API interface using an
abstract class
There is more π€©
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
Requirements: reading and writing in storage
Since this series is focused more on fpdart and riverpod, the requirements for the app are simple:
The app should allow to store events (with
title
anddate
). It then should allow to visualize all the created events.
The API therefore consists in:
- Create
Event
s given atitle
(use the current date and time asdate
) - Read and display the full list of created
Event
s
With these requirements in mind, it's time to jump into code!
Data class definition
The first step is defining a class for an Event
. We are going to call this class EventEntity
:
import 'package:flutter/material.dart';
@immutable
final class EventEntity {
final int id;
final String title;
final DateTime createdAt;
const EventEntity(this.id, this.title, this.createdAt);
}
Immutability
As you can see, this class is defined as immutable. In dart we can make an immutable class by:
- Add
final
class modifier: This prevents the class from being extended of implemented - Mark all fields as
final
: This prevents any field from being changed - Add the
@immutable
annotation (nice to have, not required)
Making EventEntity
immutable allows for better encapsulation.
Immutability allows to pass and share objects without worrying about changes to the object's state. This allows better reasoning and makes the codebase more readable and maintainable.
Immutability 𧱠Every variable is constant, it can never change Instead of reassigning, create a new copy
It is also possible to add mutable fields to the class. In that case if you want to keep the class immutable you must:
- Make all mutable fields private, so that direct access is not allowed
- Create defensive copies of all mutable fields passed to the constructor, so that they cannot be changed from the outside
- Do not provide any methods that give external access to internal mutable fields
- Do not provide any methods that change any fields (except if you make absolutely sure those changes have no external effects, this may be useful for lazy initialization, caching and improving performance)
@immutable
final class EventEntityMutableFields {
final int id;
/// Mutable fields: Make them private β οΈ
String _title;
DateTime _createdAt;
EventEntityMutableFields._(this.id, this._title, this._createdAt);
factory EventEntityMutableFields(int id, String title, DateTime createdAt) {
/// Defensive copy: Avoid changes from the outside π
final createdAtCopy = createdAt.copyWith();
return EventEntityMutableFields._(id, title, createdAtCopy);
}
}
Immutability ππ sandromaglione.com/immutability-pβ¦
freezed
: Immutability and more
Another common solution to create immutable objects in dart is freezed
.
freezed
leverages code generation to automatically define immutable objects.
On top of that, freezed
also generates:
- Overrides
toString
,==
operator, andhashCode
(equality) - Implements a
copyWith
method to clone the object - Handles de/serialization (
toJson
andfromJson
)
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'event_entity.freezed.dart';
part 'event_entity.g.dart';
@freezed
class EventEntity with _$EventEntity {
const factory EventEntity({
required int id,
required String title,
required DateTime createdAt,
}) = _EventEntity;
factory EventEntity.fromJson(Map<String, Object?> json)
=> _$EventEntityFromJson(json);
}
freezed
also supports mutable objects by using the@unfreezed
annotation
Class equality using Equatable
We are also going to implement equality for EventEntity
.
Instead of manually overriding the ==
operator and the hashCode
method, we are going to use the equatable
package:
EventEntity
extends theEquatable
class- Override the
props
method
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
@immutable
final class EventEntity extends Equatable {
final int id;
final String title;
final DateTime createdAt;
const EventEntity(this.id, this.title, this.createdAt);
@override
List<Object?> get props => [id];
}
Now every time we compare two EventEntity
they will be considered equal if their id
is the same:
final event1 = EventEntity(1, "event1", DateTime(2023));
final event2 = EventEntity(1, "event2", DateTime(2024));
final equal = event1 == event2; /// True β
There is more π€©
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
API definition: StorageService
We can now define the API for our application.
As we saw previously, the API consists in reading and writing EventEntity
in storage. Therefore, we create a new StorageService
class with 2 methods:
getAll
: Returns the list of allEventEntity
in storageput
: Create and store a newEventEntity
given itstitle
, and return the new instance ofEventEntity
abstract class StorageService {
Future<List<EventEntity>> get getAll;
Future<EventEntity> put(String title);
}
Note: The
id
ofEventEntity
will be auto-generate, whiledate
will be initialized to the current date and time.
Abstract the concrete implementation: abstract class
StorageService
is defined as an abstract class
.
Making StorageService
abstract allows to define the API methods without providing a concrete implementation.
The advantage is that we can use any storage (shared preferences, local database, remote database) without making the whole codebase dependent on a specific implementation.
Do you know what an *abstract* class is in #dart? π€ Why not a simple class? Why making it abstract? π The distinction is actually important and practical βοΈ Let's see ππ§΅
In practice this allows for better maintainability and testing:
- Maintainability: the app is not tight to any specific storage source, it is possible to update the storage just by changing the concrete
StorageService
implementation
/// All of the classes below are valid `StorageService`
///
/// You can use any of them to store the data β
class SharedPreferencesStorage implements StorageService { ... }
class LocalStorage implements StorageService { ... }
class RemoteDatabaseStorage implements StorageService { ... }
- Testing: you can use a custom
StorageService
specifically for testing
/// Mock `StorageService` for testing with ease π€
class TestStorageService extends Mock implements StorageService {}
This is it for part 2!
We are now ready to jump into fpdart
to implement our API. We are also going to learn how to connect fpdart
with riverpod
to provide the data to the UI.
If you want to stay up to date with the latest releases, you can subscribe to my newsletter here below π
Thanks for reading.