Kodax

Clean Architecture — Flutter المتقدم

المعمارية النظيفة (Clean Architecture) في Flutter

تطبيق صغير يعمل جيداً بكل شيء داخل الـ Widgets. لكن عندما يكبر: شاشة تستدعي API مباشرة، وتحفظ في قاعدة بيانات، وتحسب الأسعار، وتعرض النتائج — يصبح أي تعديل مخيفاً ولا يمكن اختبار المنطق دون تشغيل الواجهة. المعمارية تحدد من يعرف من.

الطبقات الثلاث وقاعدة الاعتماد

Presentation (Widgets + إدارة الحالة)
        │ تعتمد على
        ▼
Domain (الكيانات + حالات الاستخدام + واجهات المستودعات)   ← لا تعرف Flutter ولا HTTP
        ▲ تنفّذ واجهاتها
        │
Data (نماذج JSON + مصادر البيانات + تنفيذ المستودعات)

قاعدة الاعتماد: الطبقات الخارجية تعتمد على الداخلية، لا العكس. Domain في المركز ولا يستورد شيئاً من Data أو Flutter؛ لذلك يمكن اختباره بـ Dart نقي وبسرعة.

مكونات كل طبقة

| الطبقة | المكوّن | المثال |

|---|---|---|

| Domain | Entity | User — كائن العمل النقي |

| Domain | Repository (واجهة) | abstract class UserRepository — "ماذا" نحتاج دون "كيف" |

| Domain | Use Case | GetUser — عملية عمل واحدة |

| Data | Model | UserModel مع fromJson/toJson |

| Data | Data Source | UserRemoteDataSource (HTTP)، UserLocalDataSource (تخزين محلي) |

| Data | Repository (تنفيذ) | يقرر: من الشبكة أم من الذاكرة المحلية؟ ويحوّل الاستثناءات إلى أخطاء مفهومة |

| Presentation | الحالة | Riverpod/Bloc يستدعي Use Cases ويعرض النتيجة |

الأخطاء كقيم: Either

المثال الكامل يعيد Either<Failure, User> (من حزمة dartz أو fpdart): إما فشل معروف النوع أو نتيجة — بدل رمي استثناءات تعبر الطبقات. الواجهة تُجبر على معالجة الحالتين:

final result = await getUser('42');
result.fold(
  (failure) => showError(failure.message),
  (user) => showProfile(user),
);

أضف إلى pubspec: dartz (أو fpdart)، وعرّف Failure في core/error.

حقن الاعتماديات

المستودع يحتاج مصدر بيانات، وحالة الاستخدام تحتاج المستودع. بدل إنشائها داخل الـ Widgets، نسجّلها في مكان واحد (get_it أو Providers في Riverpod) ونمرر الواجهات. في الاختبارات نمرر تنفيذاً وهمياً.

هل أحتاج كل هذا؟

لا دائماً. تطبيق من ثلاث شاشات لا يحتاج 15 مجلداً. ابدأ بفصل بسيط: واجهة / منطق / بيانات، وأضف الطبقات عندما يكبر التطبيق ويكثر المطورون. التنظيم حسب الميزة (features/auth، features/cart) أفضل من حسب النوع عند الكبر.

تمرين

أضف إلى المثال حالة استخدام GetAllUsers واكتب لها اختباراً بـ Dart نقي يمرر مستودعاً وهمياً يعيد قائمة ثابتة — دون أي Widget.

// ===== Domain Layer =====

// Entity
class User {
  final String id;
  final String name;
  final String email;
  
  User({required this.id, required this.name, required this.email});
}

// Repository Interface
abstract class UserRepository {
  Future<Either<Failure, User>> getUser(String id);
  Future<Either<Failure, List<User>>> getAllUsers();
}

// Use Case
class GetUser {
  final UserRepository repository;
  
  GetUser(this.repository);
  
  Future<Either<Failure, User>> call(String id) {
    return repository.getUser(id);
  }
}

// ===== Data Layer =====

// Model (يرث من Entity)
class UserModel extends User {
  UserModel({
    required String id,
    required String name,
    required String email,
  }) : super(id: id, name: name, email: email);
  
  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
  
  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'email': email,
  };
}

// Repository Implementation
class UserRepositoryImpl implements UserRepository {
  final RemoteDataSource remoteDataSource;
  final LocalDataSource localDataSource;
  final NetworkInfo networkInfo;
  
  UserRepositoryImpl({
    required this.remoteDataSource,
    required this.localDataSource,
    required this.networkInfo,
  });
  
  @override
  Future<Either<Failure, User>> getUser(String id) async {
    if (await networkInfo.isConnected) {
      try {
        final user = await remoteDataSource.getUser(id);
        await localDataSource.cacheUser(user);
        return Right(user);
      } on ServerException {
        return Left(ServerFailure());
      }
    } else {
      try {
        final user = await localDataSource.getCachedUser(id);
        return Right(user);
      } on CacheException {
        return Left(CacheFailure());
      }
    }
  }
}

// ===== Presentation Layer =====

// BLoC
class UserBloc extends Bloc<UserEvent, UserState> {
  final GetUser getUser;
  
  UserBloc({required this.getUser}) : super(UserInitial()) {
    on<GetUserEvent>((event, emit) async {
      emit(UserLoading());
      
      final result = await getUser(event.userId);
      
      result.fold(
        (failure) => emit(UserError(failure.message)),
        (user) => emit(UserLoaded(user)),
      );
    });
  }
}
اختبر فهمك