أنماط هندسة Flutter

دليل شامل لبناء تطبيقات Flutter قابلة للتطوير باستخدام MVC و MVVM و Clean Architecture و DDD.

Flutter 3.9.2+ Dart 3.9.2+

🎯 الهدف

ينفذ هذا المشروع نفس مجموعة الميزات (عداد، ملاحظات، تبديل السمة) عبر أربعة أنماط معمارية مختلفة باستخدام حلين لإدارة الحالة (BLoC و GetX). هذا يسمح بمقارنة مباشرة ودقيقة.

MVC (النموذج-العرض-المتحكم)

بسيط

نظرة عامة

يفصل نمط Model-View-Controller التطبيق إلى ثلاثة مكونات رئيسية: النموذج والعرض والمتحكم.

graph LR View["العرض"] -- إجراء المستخدم --> Controller["المتحكم"] Controller -- تحديث --> Model["النموذج"] Model -- إشعار --> View

تدفق التفاعل

sequenceDiagram participant User as المستخدم participant View as العرض participant Controller as المتحكم participant Model as النموذج User->>View: ضغط الزر View->>Controller: استدعاء دالة Controller->>Model: تحديث البيانات Model-->>View: إشعار المستمعين View->>View: إعادة بناء

المفاهيم الأساسية

💡 المفهوم الأساسي

يعمل المتحكم (Controller) بمثابة "الدماغ". يستقبل مدخلات المستخدم من العرض (View)، ويعالجها (غالبًا بتحديث النموذج)، ثم يطلب من العرض التحديث. العرض سلبي ويعرض فقط ما يُطلب منه.

🍔 تشبيه من العالم الحقيقي: المطعم

العرض (الزبون): يرى القائمة ويطلب الطعام.
المتحكم (النادل): يأخذ الطلب إلى المطبخ ويحضر الطعام.
النموذج (المطبخ): يجهز الطعام (البيانات) ويتعامل مع المكونات (المنطق).

التحليل

الهيكل

  • النموذج (Model): البيانات ومنطق العمل
  • العرض (View): مكونات واجهة المستخدم
  • المتحكم (Controller): يتوسط بين النموذج والعرض

الأفضل لـ

  • ✅ التطبيقات الصغيرة والمتوسطة
  • ✅ النماذج الأولية السريعة
  • ✅ تعلم أساسيات Flutter

✅ الإيجابيات

  • البساطة: سهل الفهم والتنفيذ.
  • الفصل: تمييز واضح بين البيانات (النموذج) وواجهة المستخدم (العرض).
  • سرعة التطوير: ممتاز لإطلاق المنتجات الأولية (MVP) بسرعة.

❌ السلبيات

  • الاقتران القوي: تعتمد العروض غالبًا بشكل مباشر على النماذج.
  • تضخم المتحكمات: يمكن أن تصبح المتحكمات ضخمة وتتحمل الكثير من المنطق.
  • صعوبة الاختبار: قد يكون من الصعب اختبار منطق واجهة المستخدم المختلط بمنطق العمل.

التنفيذ

مثال: متحكم العداد
class CounterController {
  int count = 0;
  
  // المتحكم يعالج المنطق
  void increment() {
    count++;
    // ويخبر العرض يدويًا بالتحديث
    update(); 
  }
}
هيكل المشروع
lib/
├── main.dart            # نقطة الدخول
├── config/              # المسارات، السمات
├── models/              # نماذج البيانات
│   └── user_model.dart
├── views/               # الشاشات والأدوات
│   ├── home_view.dart
│   └── widgets/
└── controllers/         # منطق العمل
    └── home_controller.dart

MVVM (النموذج-العرض-نموذج العرض)

متوسط

نظرة عامة

يسهل MVVM فصل تطوير واجهة المستخدم الرسومية عن تطوير منطق العمل أو المنطق الخلفي.

graph LR View["العرض"] -- ربط --> ViewModel["نموذج العرض"] ViewModel -- تحديث --> Model["النموذج"] Model -- إشعار --> ViewModel ViewModel -- إشعار --> View

تدفق ربط البيانات

sequenceDiagram participant User as المستخدم participant View as العرض participant ViewModel as نموذج العرض participant Model as النموذج User->>View: إدخال بيانات View->>ViewModel: تحديث القابل للملاحظة ViewModel->>Model: حفظ البيانات Model-->>ViewModel: تأكيد ViewModel-->>View: تحديث الحالة

المفاهيم الأساسية

💡 المفهوم الأساسي

يعرض نموذج العرض (ViewModel) تدفقات من البيانات (الحالة) التي يستمع إليها العرض. عندما يتغير النموذج، يقوم نموذج العرض بتحديث التدفق، ويعيد العرض بناء نفسه تلقائيًا. هذا يلغي الحاجة لأن يقوم نموذج العرض بتحديث العرض يدويًا.

📺 تشبيه من العالم الحقيقي: إعداد التلفاز

العرض (شاشة التلفاز): تعرض أي إشارة تتلقاها.
نموذج العرض (جهاز الاستقبال): يعالج الإشارة الخام إلى شيء يمكن للتلفاز عرضه.
النموذج (محطة البث): مصدر الإشارة/البيانات الخام.
ملاحظة: التلفاز لا يطلب البيانات من المحطة؛ هو فقط يتفاعل مع جهاز الاستقبال.

التحليل

الهيكل

  • النموذج (Model): كيانات البيانات
  • العرض (View): مكونات واجهة المستخدم
  • نموذج العرض (ViewModel): منطق العرض مع القابلة للملاحظة

الأفضل لـ

  • ✅ التطبيقات المتوسطة إلى الكبيرة
  • ✅ حالة واجهة المستخدم المعقدة
  • ✅ البرمجة التفاعلية

✅ الإيجابيات

  • فك الارتباط: العرض لا يعرف شيئًا عن النموذج، فقط نموذج العرض.
  • قابلية الاختبار: نماذج العرض سهلة الاختبار (بدون تبعية لواجهة المستخدم).
  • إعادة الاستخدام: يمكن إعادة استخدام نماذج العرض عبر شاشات مختلفة.

❌ السلبيات

  • التعقيد: يتطلب فهم البرمجة التفاعلية (Streams/Observables).
  • العبء الإضافي: قد يكون مبالغًا فيه للشاشات البسيطة جدًا.
  • التصحيح: تتبع تدفق البيانات في الكود التفاعلي قد يكون صعبًا.

التنفيذ

مثال: نموذج عرض تفاعلي
class CounterViewModel {
  // كشف تدفق من البيانات (الحالة)
  final _countController = StreamController<int>();
  Stream<int> get countStream => _countController.stream;
  
  int _count = 0;
  
  void increment() {
    _count++;
    // إضافة قيمة جديدة للتدفق - العرض يتحدث تلقائيًا
    _countController.add(_count);
  }
}
هيكل المشروع
lib/
├── main.dart
├── core/                # الثوابت، الأدوات
├── models/              # نماذج البيانات
├── views/               # طبقة العرض
│   ├── login_view.dart
│   └── widgets/
├── viewmodels/          # الحالة والمنطق
│   └── login_vm.dart
└── services/            # استدعاءات API، التخزين المحلي

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

معقد

نظرة عامة

تفصل العمارة النظيفة البرنامج إلى طبقات. تحتوي الطبقات الداخلية على قواعد العمل، بينما تحتوي الطبقات الخارجية على تفاصيل التنفيذ.

graph TD UI["واجهة المستخدم"] --> Presentation["طبقة العرض
(BLoC / Controllers)"] Presentation --> Domain["طبقة المجال
(Use Cases / Entities)"] Data["طبقة البيانات
(Repositories / Data Sources)"] --> Domain

نمط المستودع (Repository Pattern)

كيفية تدفق البيانات بين الطبقات دون انتهاك قواعد التبعية.

graph LR UseCase["حالة الاستخدام"] -- تستدعي --> RepoInterface["واجهة المستودع
(المجال)"] RepoImpl["تطبيق المستودع
(البيانات)"] -- ينفذ --> RepoInterface RepoImpl -- يستخدم --> DataSource["مصدر البيانات
(API/DB)"]

تدفق التحكم (طلب/استجابة)

sequenceDiagram participant UI as واجهة المستخدم participant BLoC as BLoC participant UseCase as حالة الاستخدام participant Repository as المستودع participant DataSource as مصدر البيانات UI->>BLoC: إضافة حدث BLoC->>UseCase: تنفيذ UseCase->>Repository: جلب البيانات Repository->>DataSource: إحضار DataSource-->>Repository: بيانات خام Repository-->>UseCase: كيان المجال UseCase-->>BLoC: نتيجة BLoC-->>UI: إصدار حالة

المفاهيم الأساسية

💡 قاعدة التبعية

يمكن لتبعي الكود المصدري أن تشير فقط إلى الداخل. لا يمكن لأي شيء في دائرة داخلية (مثل المجال) أن يعرف أي شيء على الإطلاق عن شيء في دائرة خارجية (مثل العرض أو البيانات). هذا يجعل المنطق الأساسي محصنًا ضد تغييرات واجهة المستخدم أو قاعدة البيانات.

🏰 تشبيه من العالم الحقيقي: القلعة

المجال (الملك): يعيش في المركز، يضع القواعد، ولا يعرف شيئًا عن العالم الخارجي.
العرض/البيانات (الحراس): يحمون الملك، ويتعاملون مع الرسل (API) والزوار (UI)، ويترجمون طلباتهم إلى شيء يفهمه الملك.

التحليل

الهيكل

  • طبقة البيانات: المستودعات، مصادر البيانات، النماذج
  • طبقة المجال: حالات الاستخدام، الكيانات، الواجهات
  • طبقة العرض: المتحكمات، العروض، الروابط

الأفضل لـ

  • ✅ تطبيقات كبيرة قابلة للتطوير
  • ✅ فرق متعددة
  • ✅ متطلبات اختبار عالية

✅ الإيجابيات

  • الاستقلالية: يمكن تغيير واجهة المستخدم وقاعدة البيانات والأطر دون التأثير على قواعد العمل.
  • قابلية الاختبار: يمكن اختبار منطق العمل (حالات الاستخدام) بمعزل عن غيره.
  • قابلية الصيانة: الحدود الواضحة تجعل من السهل التنقل وإصلاح الأخطاء.

❌ السلبيات

  • الكود النمطي: يتطلب كتابة العديد من الملفات (DTOs, Mappers, Interfaces).
  • منحنى التعلم: مفاهيم مثل عكس التبعية قد تكون صعبة الفهم.
  • هندسة زائدة: معقد جدًا لتطبيقات CRUD البسيطة.

التنفيذ

مثال: حالة استخدام المجال
// طبقة المجال: Dart نقي، بدون تبعيات Flutter
class GetUserUseCase {
  final UserRepository repository;

  GetUserUseCase(this.repository);

  // ينفذ قاعدة عمل محددة
  Future<User> execute(String userId) async {
    // يمكن إضافة التحقق أو منطق آخر هنا
    if (userId.isEmpty) throw InvalidIdException();
    
    return await repository.getUser(userId);
  }
}
هيكل المشروع
lib/
├── main.dart
├── core/                # الأخطاء، الشبكة، الأدوات
├── config/              # المسارات، السمة
└── features/            # قائم على الميزات
    └── auth/
        ├── data/        # تنفيذ المستودعات، مصادر البيانات، النماذج
        │   ├── datasources/
        │   ├── models/
        │   └── repositories/
        ├── domain/      # الكيانات، واجهة المستودعات، حالات الاستخدام
        │   ├── entities/
        │   ├── repositories/
        │   └── usecases/
        └── presentation/# BLoC/Cubit، الصفحات، الأدوات
            ├── bloc/
            ├── pages/
            └── widgets/

DDD (التصميم القائم على المجال)

خبير

نظرة عامة

يركز DDD على منطق المجال الأساسي وتفاعلات منطق المجال. ينطوي على تعاون بين الخبراء التقنيين وخبراء المجال.

graph TD subgraph Domain["طبقة المجال (الأساس)"] Entities["الكيانات"] VO["كائنات القيمة"] end subgraph App["طبقة التطبيق"] UseCases["حالات الاستخدام"] end subgraph Infra["طبقة البنية التحتية"] Repos["تطبيق المستودعات"] API["بيانات عن بعد"] end UseCases --> Entities Repos --> Entities App --> Domain Infra --> Domain

مثال على جذر التجميع (Aggregate Root)

classDiagram class OrderAggregate { +String id +List~OrderItem~ items +addItem() +removeItem() } class OrderItem { +String productId +int quantity } class Address { +String street +String city } OrderAggregate *-- OrderItem : يحتوي OrderAggregate *-- Address : كائن قيمة

المفاهيم الأساسية

💡 التصميم الاستراتيجي مقابل التكتيكي

يحدد التصميم الاستراتيجي الحدود واسعة النطاق (السياقات المحددة) وكيفية تعاون الفرق. يوفر التصميم التكتيكي الأنماط (الكيانات، كائنات القيمة، التجميعات) لبناء المنطق الداخلي لتلك السياقات.

🏢 تشبيه من العالم الحقيقي: شركة كبيرة

السياقات المحددة (الأقسام): المبيعات، الموارد البشرية، والهندسة هي أقسام منفصلة.
اللغة الموحدة (المصطلحات): كلمة "Lead" تعني شيئًا مختلفًا في المبيعات (عميل محتمل) مقابل الهندسة (مطور رئيسي).
رسم خرائط السياق (التواصل): كيف تتحدث هذه الأقسام مع بعضها البعض رسميًا.

التحليل

الهيكل

  • طبقة المجال: الكيانات، كائنات القيمة (Pure Dart)
  • طبقة التطبيق: حالات الاستخدام التي تنسق المنطق
  • طبقة البنية التحتية: مصادر البيانات، التنفيذ
  • طبقة العرض: واجهة المستخدم والمتحكمات

الأفضل لـ

  • ✅ تطبيقات المؤسسات
  • ✅ منطق العمل المعقد
  • ✅ المتطلبات المتطورة

✅ الإيجابيات

  • توافق الأعمال: الكود يتحدث نفس لغة خبراء الأعمال (اللغة الموحدة).
  • المرونة: تسمح السياقات المحددة (Bounded Contexts) بتطور أجزاء مختلفة من النظام بشكل مستقل.
  • نماذج غنية: تغليف المنطق داخل كائنات المجال، مما يمنع "النماذج الهزيلة".

❌ السلبيات

  • التعقيد: منحنى تعلم عالٍ جدًا.
  • استهلاك الوقت: يتطلب تحليلًا ونمذجة عميقة قبل البرمجة.
  • الخبرة: يحتاج إلى مطورين يفهمون التقنية والمجال معًا.

التنفيذ

مثال: نموذج مجال غني
// كيان المجال مع منطق التحقق الذاتي
class EmailAddress extends ValueObject<String> {
  @override
  final Either<ValueFailure<String>, String> value;

  // منشئ خاص
  const EmailAddress._(this.value);

  // منشئ المصنع الذي يتحقق عند الإنشاء
  factory EmailAddress(String input) {
    return EmailAddress._(
      validateEmailAddress(input),
    );
  }
}
هيكل المشروع
lib/
├── main.dart
├── domain/              # قواعد عمل المؤسسة
│   ├── auth/
│   │   ├── value_objects.dart
│   │   └── i_auth_facade.dart
│   └── core/
├── infrastructure/      # محولات الواجهة
│   ├── auth/
│   │   ├── auth_facade_impl.dart
│   │   └── user_dtos.dart
│   └── core/
├── application/         # قواعد عمل التطبيق
│   └── auth/
│       ├── sign_in_form_bloc.dart
│       └── auth_bloc.dart
└── presentation/        # الأطر وبرامج التشغيل
    ├── sign_in/
    └── app_widget.dart

إدارة الحالة: BLoC مقابل GetX

يوفر هذا المستودع تنفيذين كاملين لكل نمط معماري.

نمط BLoC

graph LR UI["واجهة المستخدم"] -- حدث --> Bloc["BLoC"] Bloc -- معالجة --> Logic["منطق العمل"] Logic -- حالة جديدة --> Bloc Bloc -- حالة --> UI

نمط GetX

graph LR View["العرض (Obx)"] -- يستدعي --> Controller["GetxController"] Controller -- يحدث --> State["حالة Rx"] State -- تحديث تلقائي --> View
الميزة BLoC (مكون منطق العمل) GetX
الفلسفة قائم على التدفق (Stream)، يمكن التنبؤ به، صريح نمط المراقب، بسيط، منتج
منحنى التعلم شديد الانحدار سهل
الكود النمطي عالي (الأحداث، الحالات) منخفض (أقل كود)
الأداء ممتاز ⚡⚡⚡⚡⚡ ممتاز ⚡⚡⚡⚡⚡
قابلية الاختبار ممتاز (blocTest) جيد
الأفضل لـ الفرق الكبيرة، العمارة الصارمة التطوير السريع، المنتجات القابلة للتطبيق (MVP)

تحليل الأداء

مقارنة تفصيلية لاستخدام الذاكرة وأوقات البناء ومعدلات الإطارات.

استخدام الذاكرة (خامل)

  • GetX: ~45MB
  • BLoC: ~48MB
  • Provider: ~46MB

GetX أخف قليلاً بسبب عدم وجود Streams.

وقت البدء البارد

  • GetX: ~400ms
  • BLoC: ~420ms
  • Provider: ~410ms

الفروق ضئيلة بالنسبة لمعظم التطبيقات.

تجربة المطور

تجربة BLoC

  • يمكن التنبؤ به: تدفق البيانات أحادي الاتجاه يجعل التصحيح سهلاً.
  • الأدوات: ملحقات VS Code ممتازة وتكامل DevTools.
  • الكود النمطي: يتطلب كتابة الأحداث والحالات و BLoCs.

تجربة GetX

  • السرعة: سريع جداً لكتابة الميزات. كود أقل.
  • الكل في واحد: يشمل التنقل، الحوارات، Snackbars، إلخ.
  • السحر: قد يكون من الصعب تصحيح سلوك "السحر".

أفضل الممارسات

نصائح عامة لـ Flutter

  • قسّم الأدوات المعقدة إلى مكونات أصغر قابلة لإعادة الاستخدام.
  • استخدم const constructors حيثما أمكن لتحسين الأداء.
  • تعامل مع الأخطاء ببراعة واعرض رسائل سهلة الاستخدام.
  • حافظ على تحديث التبعيات.

العمارة النظيفة

  • حافظ على استقلالية الطبقات.
  • يجب ألا تحتوي طبقة المجال على أي تبعيات لـ Flutter.
  • استخدم المستودعات لتجريد مصادر البيانات.

إدارة الحالة

  • استخدم فئات حالة غير قابلة للتغيير (Equatable).
  • أبقِ المنطق خارج واجهة المستخدم (Widgets).
  • استخدم buildWhen / listenWhen لتحسين إعادة البناء.

مصادر التعلم

استكشاف الأخطاء وإصلاحها

لم يتم العثور على BLoC/Cubit في السياق

تأكد من أنك قمت بتغليف شجرة الأدوات الخاصة بك بـ BlocProvider. إذا كنت تنتقل إلى مسار جديد، مرر BLoC الموجود باستخدام BlocProvider.value.

الحالة لا تتحدث

تأكد من أنك تقوم بإصدار نسخة جديدة من الحالة. إذا كنت تستخدم Equatable، تأكد من تعيين props بشكل صحيح. لا تقم بتغيير الحالة مباشرة.

لم يتم تهيئة تخزين HydratedBloc

استدعِ HydratedStorage.build في دالة main() الخاصة بك قبل runApp(). تأكد من استدعاء WidgetsFlutterBinding.ensureInitialized() أولاً.

تم الإنشاء بواسطة YoussefSalem582 | أنماط هندسة Flutter