Chuyển đến nội dung chính

Lesson 17: Clean Architecture for Flutter

Clean Architecture layers: Domain, Data, Presentation. Use Cases, Repositories, Entities. Dependency injection with get_it/injectable. Feature-first vs Layer-first folder structure.

💻 Programming — Lesson 17 Lesson 17: Clean Architecture for Flutter

Flutter & Dart: From Basics to Advanced

Part 5: Architecture & Patterns

xdev.asia

1. Clean Architecture

lib/
├── core/
│   ├── error/           # Failures, Exceptions
│   ├── usecases/        # Base UseCase abstract
│   ├── network/         # Network info
│   └── di/              # Dependency injection
├── features/
│   └── product/
│       ├── domain/
│       │   ├── entities/       # Product entity
│       │   ├── repositories/   # Abstract repository
│       │   └── usecases/       # GetProducts, CreateProduct
│       ├── data/
│       │   ├── models/         # ProductModel extends Product
│       │   ├── datasources/    # Remote & Local datasource
│       │   └── repositories/   # Repository implementation
│       └── presentation/
│           ├── bloc/           # ProductBloc
│           ├── pages/          # ProductListPage
│           └── widgets/        # ProductCard

2. Domain Layer

// Entity
class Product {
  final String id;
  final String name;
  final double price;
  const Product({required this.id, required this.name, required this.price});
}

// Repository interface
abstract class ProductRepository {
  Future<Either<Failure, List<Product>>> getProducts();
  Future<Either<Failure, Product>> getProductById(String id);
  Future<Either<Failure, void>> createProduct(Product product);
}

// Use Case
abstract class UseCase<Type, Params> {
  Future<Either<Failure, Type>> call(Params params);
}

class GetProducts implements UseCase<List<Product>, NoParams> {
  final ProductRepository repository;
  GetProducts(this.repository);

  @override
  Future<Either<Failure, List<Product>>> call(NoParams params) {
    return repository.getProducts();
  }
}

// Failure
abstract class Failure {
  final String message;
  const Failure(this.message);
}
class ServerFailure extends Failure { const ServerFailure(super.message); }
class CacheFailure extends Failure { const CacheFailure(super.message); }

3. Data Layer

// Model
class ProductModel extends Product {
  const ProductModel({required super.id, required super.name, required super.price});

  factory ProductModel.fromJson(Map<String, dynamic> json) => ProductModel(
    id: json['id'], name: json['name'], price: (json['price'] as num).toDouble(),
  );
  Map<String, dynamic> toJson() => {'id': id, 'name': name, 'price': price};
}

// Repository implementation
class ProductRepositoryImpl implements ProductRepository {
  final ProductRemoteDataSource remoteDataSource;
  final ProductLocalDataSource localDataSource;
  final NetworkInfo networkInfo;

  ProductRepositoryImpl({
    required this.remoteDataSource,
    required this.localDataSource,
    required this.networkInfo,
  });

  @override
  Future<Either<Failure, List<Product>>> getProducts() async {
    if (await networkInfo.isConnected) {
      try {
        final products = await remoteDataSource.getProducts();
        await localDataSource.cacheProducts(products);
        return Right(products);
      } on ServerException catch (e) {
        return Left(ServerFailure(e.message));
      }
    } else {
      try {
        final products = await localDataSource.getCachedProducts();
        return Right(products);
      } on CacheException {
        return Left(const CacheFailure('No cached data'));
      }
    }
  }
}

4. Dependency Injection with get_it

final sl = GetIt.instance;

Future<void> initDependencies() async {
  // Bloc
  sl.registerFactory(() => ProductBloc(getProducts: sl()));

  // Use Cases
  sl.registerLazySingleton(() => GetProducts(sl()));

  // Repository
  sl.registerLazySingleton<ProductRepository>(() => ProductRepositoryImpl(
    remoteDataSource: sl(),
    localDataSource: sl(),
    networkInfo: sl(),
  ));

  // Data Sources
  sl.registerLazySingleton<ProductRemoteDataSource>(
    () => ProductRemoteDataSourceImpl(dio: sl()),
  );

  // External
  sl.registerLazySingleton(() => Dio());
}

Next article: Code Generation & Freezing.