DEV Community

Cover image for Understanding Clean Architecture: Models vs Entities in Flutter Applications
Yusuf Umar Hanafi
Yusuf Umar Hanafi

Posted on

Understanding Clean Architecture: Models vs Entities in Flutter Applications

Introduction

Clean Architecture has become increasingly popular in modern software development, especially for those building scalable and maintainable Flutter applications. One of the most common points of confusion for developers implementing Clean Architecture is understanding the distinction between Models and Entities. While they may seem similar, they serve fundamentally different purposes in a well-structured application.

In this article, we'll explore the key differences between Models and Entities, when to use each, and how they fit into the broader Clean Architecture pattern in Flutter applications. Whether you're new to software architecture or looking to refine your understanding, this guide will help you make better design decisions.

Programming Paradigms in Clean Architecture

Before diving into Models and Entities, it's important to understand the programming paradigms that underpin Clean Architecture:

  1. Object-Oriented Programming (OOP): Clean Architecture leverages encapsulation, inheritance, and polymorphism to create modular, reusable components.

  2. Functional Programming: With libraries like dartz, we incorporate functional concepts such as immutability, pure functions, and algebraic data types (like Either) for more predictable code behavior.

  3. Dependency Inversion Principle: Higher-level modules do not depend on lower-level modules; both depend on abstractions.

  4. Separation of Concerns: Each layer has a specific responsibility, making the system easier to maintain and test.

Setting Up Your Flutter Project

To implement Clean Architecture in Flutter, you'll need several packages. Let's start by setting up our pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  # For functional programming constructs
  dartz: ^0.10.1
  # For value equality
  equatable: ^2.0.5
  # For state management
  flutter_bloc: ^8.1.3
  # For immutable classes
  freezed_annotation: ^2.4.1
  # For dependency injection
  get_it: ^7.6.4
  injectable: ^2.3.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  # For code generation
  build_runner: ^2.4.6
  freezed: ^2.4.5
  injectable_generator: ^2.4.1
  json_serializable: ^6.6.2
Enter fullscreen mode Exit fullscreen mode

After adding these dependencies, run:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

Clean Architecture Layers

Clean Architecture typically consists of three main layers:

  1. Domain Layer: Contains business logic and rules, completely independent of other layers.
  2. Data Layer: Responsible for data retrieval from external sources like APIs or databases.
  3. Presentation Layer: Handles UI and user interaction.

Now let's focus on where Models and Entities fit within these layers.

Entities: The Heart of Domain Layer

What are Entities?

Entities are core business objects within your application domain. They represent the most fundamental business rules and are independent of how the data is stored or presented.

Key characteristics of Entities:

  • They encapsulate the most critical business rules
  • They're independent of frameworks or external systems
  • They don't contain any presentation or data access logic
  • They should be stable and change rarely

Example Entity in Flutter

Let's create a simple User entity:

import 'package:equatable/equatable.dart';

class User extends Equatable {
  final String id;
  final String name;
  final String email;
  final int age;

  const User({
    required this.id,
    required this.name,
    required this.email,
    required this.age,
  });

  @override
  List<Object> get props => [id, name, email, age];
}
Enter fullscreen mode Exit fullscreen mode

Notice that our entity:

  • Uses Equatable for value equality
  • Contains only business-relevant data
  • Has no annotations for JSON serialization or database mapping
  • Is completely framework-independent

Models: The Bridge in Data Layer

What are Models?

Models are data structures that typically mirror your entities but are designed to work with external data sources. They handle serialization, deserialization, and data conversion between your domain entities and external systems.

Key characteristics of Models:

  • They're specific to data sources (API, database, etc.)
  • They handle conversion between external data formats and domain entities
  • They may contain framework-specific annotations
  • They can change whenever external interfaces change

Example Model in Flutter

Let's create a UserModel that corresponds to our User entity:

import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/entities/user.dart';

part 'user_model.g.dart';
part 'user_model.freezed.dart';

@freezed
class UserModel with _$UserModel {
  const UserModel._();

  const factory UserModel({
    required String id,
    required String name,
    required String email,
    @JsonKey(name: 'user_age') required int age,
  }) = _UserModel;

  factory UserModel.fromJson(Map<String, dynamic> json) => 
      _$UserModelFromJson(json);

  // Convert model to domain entity
  User toEntity() => User(
    id: id,
    name: name,
    email: email,
    age: age,
  );

  // Create model from domain entity
  factory UserModel.fromEntity(User user) => UserModel(
    id: user.id,
    name: user.name,
    email: user.email,
    age: user.age,
  );
}
Enter fullscreen mode Exit fullscreen mode

After creating this file, generate the necessary code:

flutter pub run build_runner build --delete-conflicting-outputs
Enter fullscreen mode Exit fullscreen mode

Note that our model:

  • Uses freezed for immutability and code generation
  • Has JSON annotations for serialization
  • Provides methods to convert between domain entities and models
  • Is aware of external data formats (like snake_case field names)

Models vs Entities: Key Differences

Now let's compare the two concepts:

Aspect Entity Model
Purpose Represents core business objects Translates between domain and external data
Layer Domain Data
Dependencies None (pure business logic) May depend on frameworks or libraries
Serialization No serialization logic Handles JSON/XML parsing
Stability Changes rarely Changes when external interfaces change
Framework Framework-independent May contain framework-specific annotations

Practical Implementation in Clean Architecture

Repository Pattern

The Repository pattern serves as a bridge between your domain and data layers. Repositories deal with entities in their interfaces but use models internally:

import 'package:dartz/dartz.dart';
import '../entities/user.dart';
import '../../core/errors/failures.dart';

abstract class UserRepository {
  Future<Either<Failure, User>> getUserById(String id);
  Future<Either<Failure, List<User>>> getAllUsers();
  Future<Either<Failure, void>> saveUser(User user);
}
Enter fullscreen mode Exit fullscreen mode

And the implementation:

import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/user_repository.dart';
import '../../core/errors/failures.dart';
import '../datasources/user_remote_data_source.dart';
import '../models/user_model.dart';

@Injectable(as: UserRepository)
class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource dataSource;

  UserRepositoryImpl(this.dataSource);

  @override
  Future<Either<Failure, User>> getUserById(String id) async {
    try {
      final userModel = await dataSource.getUserById(id);
      return Right(userModel.toEntity());
    } catch (e) {
      return Left(ServerFailure());
    }
  }

  @override
  Future<Either<Failure, List<User>>> getAllUsers() async {
    try {
      final userModels = await dataSource.getAllUsers();
      return Right(userModels.map((model) => model.toEntity()).toList());
    } catch (e) {
      return Left(ServerFailure());
    }
  }

  @override
  Future<Either<Failure, void>> saveUser(User user) async {
    try {
      final userModel = UserModel.fromEntity(user);
      await dataSource.saveUser(userModel);
      return const Right(null);
    } catch (e) {
      return Left(ServerFailure());
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice how:

  1. The repository interface in the domain layer only deals with entities
  2. The implementation in the data layer converts between models and entities
  3. External systems (data sources) only interact with models, never entities

Best Practices

  1. Keep Entities Simple: Entities should contain only business logic and validation rules, nothing related to external systems.

  2. Models Should Be Disposable: Models can change whenever external APIs change without affecting your core business logic.

  3. Convert Early: Convert models to entities as early as possible, typically in your repository implementation.

  4. One-Way Dependency: Domain entities should never depend on data models; only models should know about entities.

  5. Test Separately: Write separate tests for entities and models to ensure they both work correctly.

Common Pitfalls to Avoid

  1. Using Models Throughout the App: Models should be confined to the data layer; using them in presentation layers breaks clean architecture principles.

  2. Making Entities Aware of Persistence: Entities shouldn't know how they're stored or retrieved.

  3. Missing Conversion Logic: Always ensure you have proper methods to convert between models and entities.

  4. Tight Coupling: Avoid having your domain layer depend on specific data formats or structures.

Conclusion

The distinction between Models and Entities is fundamental to implementing Clean Architecture properly. Entities represent your core business objects and rules, while Models handle the translation between your domain and external systems.

By maintaining this separation, you achieve several benefits:

  • Your business logic remains isolated from external concerns
  • Changes to APIs or databases don't affect your core domain
  • Your code becomes more testable and maintainable
  • Your application becomes more adaptable to changing requirements

Remember, Clean Architecture is about creating systems that are independent of frameworks, UI, databases, and external agencies. The proper use of Models and Entities is crucial to achieving this independence.

Connect With Me

  • Source Code - Get example code for this approach
  • LinkedIn - Connect with me professionally
  • GitHub - Check out my other open source projects
  • Instagram - Follow me for behind-the-scenes and daily updates
  • Telegram - Join my channel for Flutter tips and tricks
  • Website - Visit my portfolio and blog for more tutorials

Happy coding!

Top comments (0)