Table of Contents
Data Transfer Objects
DTOs are object that transfer data between different processes. It encapsulate data and send it to other layers. DTOs are usually mapped to the entities in Domain Layer but can have additional properties. It can have different transformation logics. For example we can have logic to convert Date Format from server to Date Format used by entity of our app. While implementing Flutter Clean Architecture we extend DTO to respective entity and write our transfer logic.
import 'dart:convert';
import 'package:flutter_clean_architecture/feature/todo/domain/entity/todo.dart';
class TodoDto extends Todo {
const TodoDto({
required super.userId,
required super.id,
required super.title,
required super.completed,
});
Map toMap() {
return {
'userId': userId,
'id': id,
'title': title,
'completed': completed,
};
}
factory TodoDto.fromMap(Map map) {
return TodoDto(
userId: map['userId'] as int,
id: map['id'] as int,
title: map['title'] as String,
completed: map['completed'] as bool,
);
}
String toRawJson() => json.encode(toMap());
factory TodoDto.fromRawJson(String source) =>
TodoDto.fromMap(json.decode(source) as Map);
}
Here in above example we have extended our DTO class TodoDto
with Todo
and added our transfer logic (function) toMap()
,fromMap()
, toRawJson()
and fromRawJson()
. These functions are used to convert the data from server to entity and also makes the data from entity ready to be sent to server. In another word these method handles JSON serialization and deserialization.
Data Sources
Data Source are responsible for actual data operation such as API calls or different database queries. There can be multiple data sources but here we mainly use Remote Data Source
and Local Data Source
. Remote Data Source handles datas from remote which are stored on other servers. Local Data Source handles data stored locally. To store data locally we usually use Sqlite Database for large data and Shared Preferences to store data in key value pair. We can also use Hive Storage which is a key value storage written in pure dart.
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
@LazySingleton()
class TodoRemoteSource {
final dio = Dio();
Future getTodoList() {
return dio.get('https://jsonplaceholder.typicode.com/todos');
}
}
Note here we are using Dio
directly. It is a good idea to create a separate Dio Client Class. Here we have list of functions and it returns Dio response. It is example of Remote Data Source. Later in this tutorial we will also take a look at Local Data Source. Data source can be modified based on the client you are using. We can also create abstract interface class and extend it.
Repositories
Repository has combination of different data operations. Here in data layer we implement the interface repository created in Domain Layer. Here creating a separate file for data operations will help in abstraction of data fetching and  manipulation from the rest of the application. Here we fetch data, manipulate data, save data as per requirement and map the data to domain entities.
import 'package:flutter_clean_architecture/feature/todo/data/dto/todo_dto.dart';
import 'package:flutter_clean_architecture/feature/todo/data/source/todo_remote_source.dart';
import 'package:flutter_clean_architecture/feature/todo/domain/entity/todo.dart';
import 'package:flutter_clean_architecture/feature/todo/domain/repository/todo_repository.dart';
import 'package:injectable/injectable.dart';
@LazySingleton(as: TodoRepository)
class TodoRepositoryImpl implements TodoRepository {
final TodoRemoteSource _todoRemoteSource;
TodoRepositoryImpl(this._todoRemoteSource);
@override
Future> getTodoList() async {
try {
var response = await _todoRemoteSource.getTodoList();
final list = (response.data as List)
.map((e) => TodoDto.fromMap(e))
.toList();
return list;
} catch(e) {
rethrow;
}
}
}
Here in above example we created TodoRepositoryImpl
class and we implemented TodoRepository
class. Here @LazySingleton(as: TodoRepository)
helps in Dependency Injection
. We will learn about Dependency Injection in later chapter. Here in repository we have different data sources as properties. These data source object can be used for data operation.
Real Project Implementation
Step1: Creating required DTOs
We will create todo_dto.dart
file in lib/feature/todo/data/dto
directory for todo feature.
import 'dart:convert';
import 'package:flutter_clean_architecture/feature/todo/domain/entity/todo.dart';
class TodoDto extends Todo {
const TodoDto({
required super.userId,
required super.id,
required super.title,
required super.completed,
});
Map toMap() {
return {
'userId': userId,
'id': id,
'title': title,
'completed': completed,
};
}
factory TodoDto.fromMap(Map map) {
return TodoDto(
userId: map['userId'] as int,
id: map['id'] as int,
title: map['title'] as String,
completed: map['completed'] as bool,
);
}
String toRawJson() => json.encode(toMap());
factory TodoDto.fromRawJson(String source) =>
TodoDto.fromMap(json.decode(source) as Map);
}
Here TodoDto
class is created by analyzing json data from https://jsonplaceholder.typicode.com/todos/. Other things about DTO is explained in upper section of this article.
Step 2: Creating required data sources
Create todo_remote_source.dart
file in lib/feature/todo/data/source
directory which is a Remote Data Source.
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
@LazySingleton()
class TodoRemoteSource {
final dio = Dio();
Future getTodoList() {
return dio.get('https://jsonplaceholder.typicode.com/todos');
}
}
Here in this Remote Data Source we call endpoint for fetching required data. We use https://jsonplaceholder.typicode.com/todos/ to fetch list of todos for this tutorial.
Then we will create setting_local_source.dart
file in lib/feature/setting/data/source
directory which is a Local Data Source.
import 'package:injectable/injectable.dart';
import 'package:shared_preferences/shared_preferences.dart';
@LazySingleton()
class SettingLocalSource {
final SharedPreferences _pref;
SettingLocalSource(this._pref);
Future setDarkMode(bool darkMode) async {
_pref.setBool('DARK_MODE', darkMode);
}
bool getDarkMode() {
return _pref.getBool('DARK_MODE') ?? false;
}
}
Here in this Local Data Source we fetch and manipulate data in local storage of our application. Specifically we use shared_preferences. Here we have function to set dark mode and get dark mode. Shared Preferences saves data in key value pair. There should be unique key for each unique type of data to be saved. It is a good idea to save key like DARK_MODE
in separate file. This file should hold all keys used all over the app.
Step 3: Creating required repositories
We will create todo_repository_impl.dart
file in lib/feature/todo/data/repository
directory.
import 'package:flutter_clean_architecture/feature/todo/data/dto/todo_dto.dart';
import 'package:flutter_clean_architecture/feature/todo/data/source/todo_remote_source.dart';
import 'package:flutter_clean_architecture/feature/todo/domain/entity/todo.dart';
import 'package:flutter_clean_architecture/feature/todo/domain/repository/todo_repository.dart';
import 'package:injectable/injectable.dart';
@LazySingleton(as: TodoRepository)
class TodoRepositoryImpl implements TodoRepository {
final TodoRemoteSource _todoRemoteSource;
TodoRepositoryImpl(this._todoRemoteSource);
@override
Future> getTodoList() async {
try {
var response = await _todoRemoteSource.getTodoList();
final list = (response.data as List)
.map((e) => TodoDto.fromMap(e))
.toList();
return list;
} catch(e) {
rethrow;
}
}
}
Here in this repository we have required data source as property. We will use the object of those data source and use it for data operation. In above example we fetch list of todos using getTodoList()
function.
Then we will create setting_repository_impl.dart
file in lib/feature/setting/data/repository
directory.
import 'package:flutter_clean_architecture/feature/setting/data/source/setting_local_source.dart';
import 'package:flutter_clean_architecture/feature/setting/domain/repository/setting_respository.dart';
import 'package:injectable/injectable.dart';
@LazySingleton(as: SettingRepository)
class SettingRepositoryImpl implements SettingRepository {
final SettingLocalSource _settingLocalSource;
SettingRepositoryImpl(this._settingLocalSource);
@override
bool getDarkMode() {
return _settingLocalSource.getDarkMode();
}
@override
Future setDarkMode(bool darkMode) async {
await _settingLocalSource.setDarkMode(darkMode);
}
}
Here in this repository we have required data source as property. We will use the object of those data source and use it for data operation. In above example we have two functions getDarkMode()
to get dark mode status and setDarkMode(bool darkMode)
to set dark mode status for this application.
This completes our Data Layer. In upcoming chapters we will learn about other specific implementation.
You can find all of the implementation in following Github link: Github Repository
Read more in upcoming chapters to learn more about all of the layers of Clean Architecture.