~/icsd.ir — bash
SYSTEM_ONLINE

Clean Architecture

با رشد پروژه، بدون معماری مناسب کد به‌سرعت غیرقابل نگهداری می‌شود. در این فصل با Clean Architecture برای Flutter و پیاده‌سازی عملی آن آشنا می‌شویم.

۱۴.۱ مقدمه

با رشد پروژه، بدون معماری مناسب کد به‌سرعت غیرقابل نگهداری می‌شود. در این فصل با Clean Architecture برای Flutter و پیاده‌سازی عملی آن آشنا می‌شویم.

۱۴.۲ مشکلات بدون معماری

  • UI و business logic در هم تنیده‌اند
  • Test کردن سخت می‌شود
  • تغییر یک بخش روی همه چیز اثر می‌گذارد
  • Navigation logic در widget ها
  • API calls مستقیم در UI
  • Duplicate code فراوان

۱۴.۳ Clean Architecture

اصول طراحی Clean Architecture (Uncle Bob):


┌─────────────────────────────────────┐
│         Presentation Layer           │  (UI، Widgets، State)
│  - Pages, Widgets                   │
│  - Riverpod/Bloc                    │
├─────────────────────────────────────┤
│            Domain Layer              │  (Pure Dart، بدون Flutter)
│  - Entities                         │
│  - Use Cases                        │
│  - Repository Interfaces            │
├─────────────────────────────────────┤
│            Data Layer                │  (پیاده‌سازی)
│  - Models (DTO)                     │
│  - Repository Implementations       │
│  - Data Sources (API, Local DB)     │
└─────────────────────────────────────┘

⬇ وابستگی فقط به سمت پایین

Presentation → Domain ← Data
    

قانون Dependency

لایه‌های بالاتر می‌توانند به پایین وابسته باشند، اما هرگز برعکس. Domain هیچ چیز نمی‌داند از Data یا Presentation.

۱۴.۴ ساختار پوشه‌ها


lib/
├── main.dart
├── app.dart
├── core/
│   ├── error/
│   │   ├── exceptions.dart
│   │   └── failures.dart
│   ├── network/
│   │   ├── api_client.dart
│   │   └── network_info.dart
│   ├── usecases/
│   │   └── usecase.dart
│   └── theme/
│       └── app_theme.dart
├── features/
│   ├── auth/
│   │   ├── data/
│   │   │   ├── datasources/
│   │   │   │   ├── auth_remote_datasource.dart
│   │   │   │   └── auth_local_datasource.dart
│   │   │   ├── models/
│   │   │   │   └── user_model.dart
│   │   │   └── repositories/
│   │   │       └── auth_repository_impl.dart
│   │   ├── domain/
│   │   │   ├── entities/
│   │   │   │   └── user.dart
│   │   │   ├── repositories/
│   │   │   │   └── auth_repository.dart
│   │   │   └── usecases/
│   │   │       ├── login.dart
│   │   │       ├── register.dart
│   │   │       └── logout.dart
│   │   └── presentation/
│   │       ├── providers/
│   │       │   └── auth_provider.dart
│   │       ├── pages/
│   │       │   ├── login_page.dart
│   │       │   └── register_page.dart
│   │       └── widgets/
│   │           └── login_form.dart
│   ├── products/
│   │   └── ...
│   └── orders/
│       └── ...
└── shared/
    ├── widgets/
    └── extensions/
    

۱۴.۵ Domain Layer

Entity (پاک، بدون JSON)


// features/auth/domain/entities/user.dart
class User {
  final int id;
  final String email;
  final String name;
  final String? avatarUrl;
  
  const User({
    required this.id,
    required this.email,
    required this.name,
    this.avatarUrl,
  });
}

// با freezed برای immutability + equals
@freezed
class User with _$User {
  const factory User({
    required int id,
    required String email,
    required String name,
    String? avatarUrl,
  }) = _User;
}
    

Repository Interface


// features/auth/domain/repositories/auth_repository.dart
abstract class AuthRepository {
  Future> login({
    required String email,
    required String password,
  });
  
  Future> register({
    required String email,
    required String password,
    required String name,
  });
  
  Future> logout();
  
  Future> getCurrentUser();
}

// Either از package dartz - برای error handling functional
// Left = Failure, Right = Success
    

Use Case


// core/usecases/usecase.dart
abstract class UseCase {
  Future> call(Params params);
}

// features/auth/domain/usecases/login.dart
class Login implements UseCase {
  final AuthRepository repository;
  
  Login(this.repository);
  
  @override
  Future> call(LoginParams params) async {
    // اعتبارسنجی business
    if (!_isValidEmail(params.email)) {
      return Left(ValidationFailure("ایمیل نامعتبر"));
    }
    if (params.password.length < 8) {
      return Left(ValidationFailure("رمز کوتاه است"));
    }
    
    return repository.login(
      email: params.email,
      password: params.password,
    );
  }
  
  bool _isValidEmail(String email) {
    return RegExp(r"^[w-.]+@([w-]+.)+[w-]{2,4}$").hasMatch(email);
  }
}

class LoginParams {
  final String email;
  final String password;
  
  const LoginParams({required this.email, required this.password});
}
    

Failures


// core/error/failures.dart
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);
}

class NetworkFailure extends Failure {
  const NetworkFailure(super.message);
}

class ValidationFailure extends Failure {
  const ValidationFailure(super.message);
}

class AuthFailure extends Failure {
  const AuthFailure(super.message);
}
    

۱۴.۶ Data Layer

Model (با JSON)


// features/auth/data/models/user_model.dart
@freezed
class UserModel with _$UserModel {
  const UserModel._();
  
  const factory UserModel({
    required int id,
    required String email,
    required String name,
    @JsonKey(name: "avatar_url") String? avatarUrl,
  }) = _UserModel;
  
  factory UserModel.fromJson(Map json) => 
      _$UserModelFromJson(json);
  
  // Map to domain entity
  User toEntity() => User(
    id: id,
    email: email,
    name: name,
    avatarUrl: avatarUrl,
  );
}
    

Data Source


// features/auth/data/datasources/auth_remote_datasource.dart
abstract class AuthRemoteDataSource {
  Future login(String email, String password);
  Future register(...);
  Future logout();
}

class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
  final Dio dio;
  
  AuthRemoteDataSourceImpl(this.dio);
  
  @override
  Future login(String email, String password) async {
    try {
      final response = await dio.post(
        "/auth/login/",
        data: {"email": email, "password": password},
      );
      return UserModel.fromJson(response.data["user"]);
    } on DioException catch (e) {
      if (e.response?.statusCode == 401) {
        throw AuthException("ایمیل یا رمز اشتباه");
      }
      throw ServerException(e.message ?? "خطا");
    }
  }
}
    

Repository Implementation


// features/auth/data/repositories/auth_repository_impl.dart
class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource remote;
  final AuthLocalDataSource local;
  final NetworkInfo networkInfo;
  
  AuthRepositoryImpl({
    required this.remote,
    required this.local,
    required this.networkInfo,
  });
  
  @override
  Future> login({
    required String email,
    required String password,
  }) async {
    if (!await networkInfo.isConnected) {
      return const Left(NetworkFailure("اتصال اینترنت ندارید"));
    }
    
    try {
      final userModel = await remote.login(email, password);
      await local.cacheUser(userModel);
      return Right(userModel.toEntity());
    } on AuthException catch (e) {
      return Left(AuthFailure(e.message));
    } on ServerException catch (e) {
      return Left(ServerFailure(e.message));
    }
  }
}
    

۱۴.۷ Presentation Layer


// features/auth/presentation/providers/auth_provider.dart
@riverpod
class AuthNotifier extends _$AuthNotifier {
  @override
  FutureOr build() async {
    final getCurrentUser = ref.read(getCurrentUserUseCaseProvider);
    final result = await getCurrentUser(NoParams());
    return result.fold((_) => null, (user) => user);
  }
  
  Future login(String email, String password) async {
    state = const AsyncValue.loading();
    
    final login = ref.read(loginUseCaseProvider);
    final result = await login(LoginParams(
      email: email,
      password: password,
    ));
    
    state = result.fold(
      (failure) => AsyncValue.error(failure, StackTrace.current),
      (user) => AsyncValue.data(user),
    );
  }
  
  Future logout() async {
    final logout = ref.read(logoutUseCaseProvider);
    await logout(NoParams());
    state = const AsyncValue.data(null);
  }
}

// در UI
class LoginPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(authNotifierProvider);
    
    ref.listen(authNotifierProvider, (prev, next) {
      next.whenOrNull(
        data: (user) {
          if (user != null) context.go("/");
        },
        error: (error, _) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text((error as Failure).message)),
          );
        },
      );
    });
    
    return Scaffold(
      // form
    );
  }
}
    

۱۴.۸ Dependency Injection

با Riverpod (پیشنهاد)


@riverpod
Dio dio(Ref ref) {
  return Dio(BaseOptions(baseUrl: "https://api.shop.com/api/v1/"));
}

@riverpod
AuthRemoteDataSource authRemoteDataSource(Ref ref) {
  return AuthRemoteDataSourceImpl(ref.read(dioProvider));
}

@riverpod
AuthRepository authRepository(Ref ref) {
  return AuthRepositoryImpl(
    remote: ref.read(authRemoteDataSourceProvider),
    local: ref.read(authLocalDataSourceProvider),
    networkInfo: ref.read(networkInfoProvider),
  );
}

@riverpod
Login loginUseCase(Ref ref) => Login(ref.read(authRepositoryProvider));

@riverpod
Register registerUseCase(Ref ref) => Register(ref.read(authRepositoryProvider));
    

با get_it (option جایگزین)


import "package:get_it/get_it.dart";

final getIt = GetIt.instance;

void setupDependencies() {
  // Singleton
  getIt.registerLazySingleton(() => Dio(...));
  getIt.registerLazySingleton(() => NetworkInfoImpl());
  
  // Factory (هر بار جدید)
  getIt.registerFactory(
    () => AuthRemoteDataSourceImpl(getIt()),
  );
  
  getIt.registerLazySingleton(
    () => AuthRepositoryImpl(
      remote: getIt(),
      local: getIt(),
      networkInfo: getIt(),
    ),
  );
  
  getIt.registerFactory(() => Login(getIt()));
}

// استفاده
final login = getIt();
    

۱۴.۹ اصول SOLID در Flutter

  • S — Single Responsibility: هر class یک کار کند
  • O — Open/Closed: برای extension باز، برای modification بسته
  • L — Liskov Substitution: subclass باید جای parent کار کند
  • I — Interface Segregation: interface کوچک بهتر از بزرگ
  • D — Dependency Inversion: به abstraction وابسته باشید نه concrete

Clean Architecture این اصول را رعایت می‌کند.

۱۴.۱۰ معماری‌های جایگزین

MVVM

Model-View-ViewModel. ساده‌تر از Clean. مناسب پروژه‌های متوسط.

BLoC Pattern

سخت‌گیر، با Event/State.

Feature-First (Layer-First)

به جای تقسیم بر اساس layer، بر اساس feature تقسیم کنید (مثل ساختار بالا).

Riverpod Notifier Pattern

ساده‌ترین و Flutter-idiomatic. توصیه برای پروژه‌های متوسط.

۱۴.۱۱ بهترین تجربیات

  1. Feature-first folder.
  2. Domain pure Dart. بدون Flutter import.
  3. Either برای error handling functional.
  4. Repository abstract. پیاده‌سازی در data.
  5. Use case برای business logic.
  6. DI با Riverpod یا get_it.
  7. Test هر لایه جدا.
  8. Naming consistent. XxxImpl، XxxModel، XxxEntity.
  9. Code generation. با freezed و json_serializable.
  10. عملی باشید. برای پروژه کوچک، Clean overkill است.

۱۴.۱۲ خلاصه فصل

  • Clean Architecture: Presentation، Domain، Data
  • Domain pure Dart با Entity، UseCase، Repository interface
  • Data layer با Model، DataSource، Repository impl
  • Either برای error handling
  • DI با Riverpod یا get_it
  • SOLID principles
در فصل بعد: پروژه عملی پایانی — اپ e-commerce کامل!

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.