In the first part of this article, we set up melos as a tool for managing monorepo projects and installed dart_frog as a web framework for building server-side apps with Dart. We also created a new Dart Frog project, which included adding the dart_frog
dependency on the project's pubspec.yaml
file and creating the necessary files and directories.
We will create models, data sources, repositories, and failure classes as a separate Dart package in part 2.
To do this, we will be using very_good_cli.
Setting up Models, Repositories, and Data Services 💻
Installing very_good_cli
very_good_cli , a Command-Line Interface that provides helpful commands and templates for generating various types of projects, including Flutter apps, Flame games, Flutter packages, Dart packages, federated plugins, and Dart CLI projects.
Very Good CLI makes it easy to create and set up these types of projects with a single command, so let's get started!
To install Very Good CLI , open a terminal and enter the following command:
dart pub global activate very_good_cli
To confirm that Very Good CLI has been installed correctly, you can enter the following command in a terminal very_good --version
🚀
Creating typedefs
We will have our typedefs , which will be shared between the backend and frontend of our project. To create a new Dart package for these typedefs, we can use the following command:
very_good create -t dart_pkg typedefs
This will generate a new dart package typedefs
. Here -t dart_pkg
means we are creating a new dart package.
Once the typedefs
package has been created and the necessary files and directories have been generated, you can navigate to typedefs/lib/src/typedefs.dart
to create a new typedef.
In this file, you can create a new typedef by adding the following code:
/// Primary key type for a Todo.
typedef TodoId = int;
This will create a new typedef called TodoId
, an alias for the type int
. This typedef can then be used throughout the backend and frontend of your project to represent an identifier for a to-do item. Once you are done, make sure to export the file from lib/typedefs.dart
library typedefs;
export 'src/typedefs.dart';
We will add additional shared typedefs to this file as needed. GitHub Link
Creating models
Similarly, we will have a separate package to house shared data models.
CreateTodoDto
This will be used to send and receive payload for creating a new todo.UpdateTodoDto
This will be used to send and receive payload for updating an existing todo.Todo
This is the representation of theTodo
entity.
To create a separate package for models you can use the very_good
CLI:
very_good create -t dart_pkg models
Once the package has been created, we will install freezed for JSON serialization and value equality , as this library provides helpful tools for these tasks. We will use json_serializable for JSON serialization. To install freezed and freezed_annotation, open your terminal inside the models
package and use the command:
dart pub add freezed json_serializable build_runner -d
dart pub add freezed_annotation json_annotation
Inside models/lib
we will create files in the given order. We have created a parent folder for each model to house the generated codes so that it will look cleaner.
.
models.dart
src
create_todo_dto
create_todo_dto.dart
todo
todo.dart
update_todo_dto
update_todo_dto.dart
Now, in todo.dart
we will use freezed package to create a data class.
freezed package is a code generation tool that allows you to create immutable classes with concise, boilerplate-free syntax. It uses the
@freezed
annotation to generate a number of useful methods and operators for your class, such as==
,hashCode
, andcopyWith
.
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:typedefs/typedefs.dart';
part 'todo.freezed.dart';
part 'todo.g.dart';
@freezed
class Todo with _$Todo {
factory Todo({
required TodoId id,
required String title,
@Default('') String description,
@Default(false) bool? completed,
required DateTime createdAt,
DateTime? updatedAt,
}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}
Since TodoId
is in a separate package, we will have to add it in the models/pubspec.yaml
file as a dependency. After adding the pubspec.yaml
should look like this.
name: models
description: A Very Good Project created by Very Good CLI.
version: 0.1.0+1
publish_to: none
environment:
sdk: ">=2.18.0 <3.0.0"
dependencies:
freezed_annotation: ^2.2.0
json_annotation: ^4.7.0
typedefs:
path: ../typedefs
dev_dependencies:
build_runner: ^2.3.3
freezed: ^2.3.2
json_serializable: ^6.5.4
lints: ^2.0.0
mocktail: ^0.3.0
test: ^1.19.2
very_good_analysis: ^3.1.0
Similarly, we will create create_todo_dto.dart
for CreateDotoDto
class.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'create_todo_dto.freezed.dart';
part 'create_todo_dto.g.dart';
@freezed
class CreateTodoDto with _$CreateTodoDto {
factory CreateTodoDto({
required String title,
required String description,
}) = _CreateTodoDto;
factory CreateTodoDto.fromJson(Map<String, dynamic> json) =>
_$CreateTodoDtoFromJson(json);
}
Similarly, we will create update_todo_dto.dart
for UpdateTodoDto
class.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'update_todo_dto.freezed.dart';
part 'update_todo_dto.g.dart';
@freezed
class UpdateTodoDto with _$UpdateTodoDto {
factory UpdateTodoDto({
String? title,
String? description,
bool? completed,
}) = _UpdateTodoDto;
factory UpdateTodoDto.fromJson(Map<String, dynamic> json) =>
_$UpdateTodoDtoFromJson(json);
}
Once this is done, we run build_runner and generate the necessary code. To generate the missing code, you can run the following command:
dart pub run build_runner build --delete-conflicting-outputs
This will generate *.g.dart
and *.freezed.dart
files. Once this is done, you will have files inside models/lib
in the given order.
.
models.dart
src
create_todo_dto
create_todo_dto.dart
create_todo_dto.freezed.dart
create_todo_dto.g.dart
todo
todo.dart
todo.freezed.dart
todo.g.dart
update_todo_dto
update_todo_dto.dart
update_todo_dto.freezed.dart
update_todo_dto.g.dart
Once you are done creating the models, be sure to export them from lib/models.dart
library models;
export 'src/create_todo_dto/create_todo_dto.dart';
export 'src/todo/todo.dart';
export 'src/update_todo_dto/update_todo_dto.dart';
You can find the GitHub link for models here.
Creating failures
In the next step, we will create a separate package to handle failures in our application. This package will contain all the failures that may occur, such as NetworkFailure
or ServerFailure
. Whenever we need to handle an error, we will encapsulate the value or error in a union type and return it from a repository. For example, when we make an API call to retrieve data, we may receive either the requested data or an error. By encapsulating this in an Either
type, we can effectively handle and manage failures in our app.
This is inspired by ReSo Coder's Clean Architecture tutorial.
To create the failures package:
very_good create -t dart_pkg failures
This will create a new package in failures
directory. We will use freezed
and json_serializable
libraries for data serialization here as well.
dart pub add freezed json_serializable build_runner -d
dart pub add freezed_annotation json_annotation
To begin, we will create a Failure
class inside the lib/src/failure.dart
directory. This class will be the base for all failure types and will handle and manage our app's failures.
abstract class Failure {
String get message;
}
We can then create specific failure classes, such as NetworkFailure
, to return to the caller whenever there is a network exception. For instance, if we have a TodosRemoteDataSource
that makes a call to a backend and encounters a HTTP exception, the data source can throw a NetworkException
which will be caught by a repository TodoRepository
. The repository will then encapsulate the error in a union type and return either the requested data or the failure to the caller. This allows us to effectively handle and manage failures in our app.
To create a NetworkFailure
class, we will create a new file in the src
directory called network_failure/network_failure.dart
.
import 'package:failures/failures.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'network_failure.freezed.dart';
part 'network_failure.g.dart';
@freezed
class NetworkFailure extends Failure with _$NetworkFailure {
const factory NetworkFailure({
required String message,
required int code,
@Default([]) List<String> errors,
}) = _NetworkFailure;
factory NetworkFailure.fromJson(Map<String, dynamic> json) =>
_$NetworkFailureFromJson(json);
}
Once you have finished creating the NetworkFailure
class, you can run the build_runner
command to generate the necessary files for your project.
Make sure to export the failure class from failures/lib/failures.dart
library failures;
export 'src/failure.dart';
export 'src/network_failure/network_failure.dart';
You can view the code from here.
Creating data_source
We will create a new package with very_good_cli
for our data source.
very_good create -t dart_pkg data_source
Once the package is created, we can create an abstract data source contract.
import 'package:models/models.dart';
import 'package:typedefs/typedefs.dart';
abstract class TodoDataSource {
Future<List<Todo>> getAllTodo();
Future<Todo> getTodoById(TodoId id);
Future<Todo> createTodo(CreateTodoDto todo);
Future<Todo> updateTodo({
required TodoId id,
required UpdateTodoDto todo,
});
Future<void> deleteTodoById(TodoId id);
}
You can import the missing packages in pubspec.yaml
. After importing, it should look like this.
name: data_source
description: A Very Good Project created by Very Good CLI.
version: 0.1.0+1
publish_to: none
environment:
sdk: ">=2.18.0 <3.0.0"
dependencies:
models:
path: ../models
typedefs:
path: ../typedefs
dev_dependencies:
mocktail: ^0.3.0
test: ^1.19.2
very_good_analysis: ^3.1.0
The TodoDataSource
class will be used by both the frontend and backend of our app to access and manage data related to to-dos. This class will throw known exceptions that can be handled in the TodoRepository
. To manage these exceptions, we will create a separate exceptions
package where we can register all the exceptions used in our app. The TodoRepository
will then be responsible for handling these exceptions , allowing us to effectively manage and handle errors in our application.
Once you are done adding the data source, make sure to export it in data_sources/data_sources.dart
library data_source;
export 'src/todo_data_source.dart';
You can view the code for the data_source from GitHub.
Creating repository
We will make use of the TodoDataSource
class to access and manage data related to to-dos. We will also catch any exceptions thrown by the TodoDataSource
and serialize them as failures. TodoRepository
will then return the failure or the requested data to the caller.
To create the TodoRepository
class, we will first navigate to the root directory of our project and create a new package using the very_good
CLI:
very_good create -t dart_pkg repository
Once we are done, we will add all the dependencies in pubspec.yaml
and run dart pub get
to get all the packages.
dependencies:
either_dart: ^0.3.0
models:
path: ../models
failures:
path: ../failures
typedefs:
path: ../typedefs
data_source:
path: ../data_source
To create an abstract TodoRepository
class, we will navigate to our project's lib/src
directory and create a new file called todo_repository.dart
. Inside this file, we will make the TodoRepository
class and add the necessary abstract methods that will be used to manage and access data related to to-dos.
import 'package:either_dart/either.dart';
import 'package:failures/failures.dart';
import 'package:models/models.dart';
import 'package:typedefs/typedefs.dart';
abstract class TodoRepository {
Future<Either<Failure, List<Todo>>> getTodos();
Future<Either<Failure, Todo>> getTodoById(TodoId id);
Future<Either<Failure, Todo>> createTodo(CreateTodoDto createTodoDto);
Future<Either<Failure, Todo>> updateTodo({
required TodoId id,
required UpdateTodoDto updateTodoDto,
});
Future<Either<Failure, void>> deleteTodo(TodoId id);
}
We are using either_dart to encapsulate the failure and data in the same place.
either_dart is the error handling and railway-oriented programming library is a Dart library that supports async "map" and async "then" functions for working with asynchronous computations and handling errors with
Future<Either>
values.
After you are done, make sure to export it in src/repository.dart
library repository;
export 'src/todo_repository.dart';
You can find the code for this on GitHub.
Damn! that was a lot of code and a lot of writing 😮💨
🎉 In Part 3, we're going to get started on the implementation of our repository! 💻 We'll begin by setting up the backend and establishing database connections. 🔌 It's going to be a lot of fun and a great opportunity to get hands-on with the code. So stay tuned! 😎
Top comments (0)