Lesson 36 of 51 intermediate

Clean Architecture Overview

Building a skyscraper with a solid blueprint

Open interactive version (quiz + challenge)

Real-world analogy

Imagine building a skyscraper. You would not start by randomly placing windows and doors. You start with a blueprint that has clear layers: the foundation (domain), the plumbing and electrical systems (data), and the interior design (presentation). Each layer has a specific job, and a plumber never decides where the couch goes. Clean Architecture works the same way -- each layer has a clear responsibility, and inner layers never know about outer layers.

What is it?

Clean Architecture is a software design philosophy that separates an application into distinct layers with clear responsibilities and a strict dependency rule: inner layers never depend on outer layers. In team_mvp_kit, the four layers are Domain (business logic and entities), Data (API calls, DTOs, and storage), Presentation (UI and BLoCs), and Infrastructure (DI, routing, and shared utilities). Code is organized feature-first, so each feature contains its own domain, data, and presentation folders.

Real-world relevance

Clean Architecture is the reason team_mvp_kit can scale from a starter kit to a full production app without rewriting. When the team decided to switch from one payment provider to another, they only changed the data layer. When adding offline support, they added a local data source and updated the repository without changing a single line in domain or presentation.

Key points

Code example

// Complete data flow through layers:
//
// 1. DOMAIN: Entity (pure Dart)
class Product {
  final String id;
  final String name;
  final double price;
  final String imageUrl;

  const Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
  });
}

// 2. DOMAIN: Repository interface
abstract class ProductRepository {
  Future<List<Product>> getAll();
  Future<Product> getById(String id);
}

// 3. DOMAIN: Use case
class GetProductsUseCase {
  final ProductRepository _repository;
  const GetProductsUseCase(this._repository);

  Future<List<Product>> call() {
    return _repository.getAll();
  }
}

// 4. DATA: DTO (data transfer object)
class ProductDto {
  final String id;
  final String name;
  final double price;
  final String imageUrl;

  const ProductDto({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
  });

  factory ProductDto.fromJson(Map<String, dynamic> json) {
    return ProductDto(
      id: json['id'] as String,
      name: json['name'] as String,
      price: (json['price'] as num).toDouble(),
      imageUrl: json['image_url'] as String,
    );
  }

  Product toEntity() {
    return Product(
      id: id,
      name: name,
      price: price,
      imageUrl: imageUrl,
    );
  }
}

// 5. DATA: Repository implementation
class ProductRepositoryImpl implements ProductRepository {
  final ProductRemoteDataSource _remote;
  const ProductRepositoryImpl(this._remote);

  @override
  Future<List<Product>> getAll() async {
    final dtos = await _remote.fetchProducts();
    return dtos.map((dto) => dto.toEntity()).toList();
  }

  @override
  Future<Product> getById(String id) async {
    final dto = await _remote.fetchProductById(id);
    return dto.toEntity();
  }
}

// 6. PRESENTATION: BLoC uses use case
class ProductListBloc
    extends Bloc<ProductEvent, ProductState> {
  final GetProductsUseCase _getProducts;

  ProductListBloc(this._getProducts)
      : super(const ProductState()) {
    on<LoadProducts>(_onLoad);
  }

  Future<void> _onLoad(
    LoadProducts event,
    Emitter<ProductState> emit,
  ) async {
    emit(state.copyWith(isLoading: true));
    try {
      final products = await _getProducts();
      emit(state.copyWith(
        products: products,
        isLoading: false,
      ));
    } catch (e) {
      emit(state.copyWith(
        isLoading: false,
        failure: ServerFailure(e.toString()),
      ));
    }
  }
}

Line-by-line walkthrough

  1. 1. The Product entity is a pure Dart class with no framework dependencies.
  2. 2. ProductRepository is an abstract class defining the contract that the data layer must fulfill.
  3. 3. GetProductsUseCase wraps a single business operation and depends only on the repository interface.
  4. 4. ProductDto mirrors the entity but adds fromJson for deserialization and toEntity for conversion.
  5. 5. The fromJson factory handles JSON field types, including snake_case to camelCase conversion.
  6. 6. toEntity converts the DTO to a domain entity, decoupling API format from business format.
  7. 7. ProductRepositoryImpl implements the interface by delegating to the remote data source.
  8. 8. The getAll method fetches DTOs, then maps each to an entity using toEntity.
  9. 9. ProductListBloc depends on the use case, not the repository or data source directly.
  10. 10. The _onLoad handler follows the loading/success/failure pattern from Lesson 35.
  11. 11. The flow: Screen event -> BLoC -> UseCase -> Repository interface -> Implementation -> DataSource.

Spot the bug

// domain/usecases/get_products_usecase.dart
import 'package:dio/dio.dart';
import '../repositories/product_repository.dart';

class GetProductsUseCase {
  final ProductRepository _repository;
  GetProductsUseCase(this._repository);

  Future<List<Product>> call() async {
    try {
      return await _repository.getProducts();
    } on DioException catch (e) {
      throw ServerFailure(e.message ?? 'Error');
    }
  }
}
Need a hint?
Look at the imports. Which layer is this file in, and what is it importing?
Show answer
The use case is in the domain layer but imports 'package:dio/dio.dart', which is a networking package. This violates the dependency rule -- the domain layer must not know about HTTP or Dio. The DioException handling should happen in the data layer (repository implementation). Remove the Dio import and let the repository catch DioExceptions and convert them to domain Failure types.

Explain like I'm 5

Think about a sandwich shop. The recipe (domain layer) says 'put cheese between two slices of bread.' The recipe does not care if the bread comes from Store A or Store B -- that is the data layer's job. The person at the counter who takes orders and serves sandwiches is the presentation layer. And the building itself -- the kitchen setup, the ordering system -- that is the infrastructure. If you change where you buy bread, the recipe stays the same. If you redesign the counter, the recipe still stays the same. That is the power of layers!

Fun fact

Clean Architecture was introduced by Robert C. Martin (Uncle Bob) in 2012. The core idea was actually inspired by even older patterns like Hexagonal Architecture (Alistair Cockburn, 2005) and Onion Architecture (Jeffrey Palermo, 2008). All three share the same fundamental insight: business logic should be at the center and never depend on frameworks, databases, or UI.

Hands-on challenge

Design the complete folder structure for a new 'notifications' feature in team_mvp_kit style. Create the Notification entity, NotificationRepository interface, GetNotificationsUseCase, NotificationDto with fromJson and toEntity, NotificationRepositoryImpl, and NotificationListBloc. Write out each file with proper imports showing the dependency direction. Verify that domain never imports from data or presentation.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart