Flutter Clean Architecture: Data Layer

October 29, 2024
Flutter Clean Architecture: Data Layer
Flutter Clean Architecture: Data Layer
In Clean Architecture, Data Layer is responsible for handling data operations such as fetching data using Api from remote or fetching locally stored data. This is a layer that serves as a bridge between external source and local data to domain layer ensuring domain logic remains clean. The primary components of Data Layer are Data Transfer Objects (DTOs), repositories and sources.

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<String, dynamic> toMap() {
    return <String, dynamic>{
      'userId': userId,
      'id': id,
      'title': title,
      'completed': completed,
    };
  }

  factory TodoDto.fromMap(Map<String, dynamic> 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<String, dynamic>);
}
				
			

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<Response> 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<List<Todo>> getTodoList() async {
    try {
      var response = await _todoRemoteSource.getTodoList();
      final list = (response.data as List<dynamic>)
          .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<String, dynamic> toMap() {
    return <String, dynamic>{
      'userId': userId,
      'id': id,
      'title': title,
      'completed': completed,
    };
  }

  factory TodoDto.fromMap(Map<String, dynamic> 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<String, dynamic>);
}
				
			

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<Response> 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<void> 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<List<Todo>> getTodoList() async {
    try {
      var response = await _todoRemoteSource.getTodoList();
      final list = (response.data as List<dynamic>)
          .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<void> 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.

Flutter Clean Architecture: Introduction & Project Setup
Flutter Clean Architecture: Domain Layer
Flutter Clean Architecture: Data Layer
Flutter Clean Architecture: Presentation Layer
Flutter Clean Architecture: Dependency Injection

Related

Annapurna Circuit Trek

10 Best Treks in Nepal

Flutter Clean Architecture: Dependency Injection

Flutter Clean Architecture: Dependency Injection

Flutter Clean Architecture: Presentation Layer

Flutter Clean Architecture: Presentation Layer

Flutter Clean Architecture: Domain Layer

Flutter Clean Architecture: Domain Layer

Flutter Clean Architecture Introduction & Project Setup

Flutter Clean Architecture: Introduction & Project Setup