Flutter Clean Architecture 103: Mastering the Data Layer (Beginner-Friendly)

Welcome back to our Flutter Clean Architecture series!

In the previous post, we delve deep into the Domain Layer — the home of our pure business logic.
Now, it’s time to get more practical: the Data Layer

Quick Reminder: What Is Clean Architecture?

  • Domain Layer: What your app does (business rules, logic)

  • Data Layer: How your app talks to the outside world (APIs, databases, cache)

  • Presentation Layer: Flutter widgets that show data and interact with the user

  • In this post, we focus fully on the Data Layer.

Why Do We Need a Data Layer?

Imagine your app needs to:

  • Fetch data from an API.

  • Store data locally.

  • Read from device storage.

The Data Layer handles all of these. It:

  • Talks to APIs and databases.

  • Converts external data into your app’s internal Entities.

  • Implements the repository interfaces we defined in the Domain Layer.


The Building Blocks of Data Layer

1.    Models (DTOs - Data Transfer Objects)

  • Represent raw data as received from external sources (APIs, databases).

  • Responsible for converting to/from Entities. (Not strictly. to/from Entities or Models can be done in a separate file outside the model file. eg. a mapper file)

2.     Data Sources

  • Responsible for actually talking to APIs, databases, or local storage.

  • Can be split into:

    • Remote Data Sources (e.g. REST APIs)

    • Local Data Sources (e.g. SQLite, Hive)

3.    Repository Implementations

  • Implements the repository interfaces from Domain Layer.

  • Uses data sources to get/save data.

  • Converts models to entities before returning to Domain Layer.


Let’s Build It Step-by-Step

🔧 The Scenario

We want to fetch user profiles from a REST API.

📝 Domain Layer Recap

We already have:

User Entity:

class User {
  final String id;
  final String name;
  final String email;

  User({
  required this.id,
  required this.name,
  required this.email,
  });
}
User Repository Interface:
abstract class UserRepository {
  Future<User> getUserProfile();
}

Now: The Data Layer

Data Layer folder structure:
Main dir: Data/
Sub dirs: /Model/, /Repository/, and /Data Source/.

1. Create the User Model (DTO)

Create a file; user_model.dart under data/model/

This matches the API response format.

class UserModel {
  final String id;
  final String name;
  final String email;

  UserModel({required this.id, required this.name, required this.email});

  // Factory constructor to parse from JSON
  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }

  // Convert back to JSON if needed
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
    };
  }

  // Convert to Entity (if we were to use mappers, this toEntity method wouldn't be here)
  User toEntity() {
    return User(id: id, name: name, email: email);
  }
}

2. Create the Remote Data Source

Here we make the actual API call:

Create a file; remote_source.dart under data/data_source/
import 'package:http/http.dart' as http;
import 'dart:convert';

class RemoteUserDataSource {
  final http.Client client;

  RemoteUserDataSource({required this.client});

  Future<UserModel> fetchUser() async {    //Note: Replace https://api.example.com/user with your actual API endpoint when implementing in your app.
  final response = await client.get(Uri.parse('https://api.example.com/user'));

    if (response.statusCode == 200) {
      final jsonMap = json.decode(response.body);
      return UserModel.fromJson(jsonMap);
    } else {
      throw Exception('Failed to load user');
    }
  }
}

3. Implement the Repository

Now we connect the data source to the repository interface:

Create a file; user_repo_impl.dart under data/repository/

class UserRepositoryImpl implements UserRepository {
  final RemoteUserDataSource remoteUserDataSource;

  UserRepositoryImpl({required this.remoteUserDataSource});

  @override
  Future<User> getUserProfile() async {
    final userModel = await remoteUserDataSource.fetchUser();
    return userModel.toEntity();
  }
}

Full Data Flow Summary

UI --> UseCase --> UserRepository (interface) --> UserRepositoryImpl --> RemoteUserDataSource --> API

Key Takeaways

  • The Data Layer handles external data sources and converts data to usable Entities.

  • Models (DTOs) are only used inside the Data Layer — the Domain Layer stays clean.

  • Repository Implementations connect Domain and Data Layers.


What’s Next?

In the next post, we’ll move to the most exciting part for many Flutter developers:
The Presentation Layer — how to actually display this data in your Flutter app.


I hope this tutorial made the Data Layer simple and clear. Feel free to leave questions or suggestions in the comments!

Comments