أنماط هندسة Flutter
دليل شامل لبناء تطبيقات Flutter قابلة للتطوير باستخدام MVC و MVVM و Clean Architecture و DDD.
🎯 الهدف
ينفذ هذا المشروع نفس مجموعة الميزات (عداد، ملاحظات، تبديل السمة) عبر أربعة أنماط معمارية مختلفة باستخدام حلين لإدارة الحالة (BLoC و GetX). هذا يسمح بمقارنة مباشرة ودقيقة.
MVC (النموذج-العرض-المتحكم)
بسيطنظرة عامة
يفصل نمط Model-View-Controller التطبيق إلى ثلاثة مكونات رئيسية: النموذج والعرض والمتحكم.
تدفق التفاعل
المفاهيم الأساسية
💡 المفهوم الأساسي
يعمل المتحكم (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 فصل تطوير واجهة المستخدم الرسومية عن تطوير منطق العمل أو المنطق الخلفي.
تدفق ربط البيانات
المفاهيم الأساسية
💡 المفهوم الأساسي
يعرض نموذج العرض (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)
معقدنظرة عامة
تفصل العمارة النظيفة البرنامج إلى طبقات. تحتوي الطبقات الداخلية على قواعد العمل، بينما تحتوي الطبقات الخارجية على تفاصيل التنفيذ.
(BLoC / Controllers)"] Presentation --> Domain["طبقة المجال
(Use Cases / Entities)"] Data["طبقة البيانات
(Repositories / Data Sources)"] --> Domain
نمط المستودع (Repository Pattern)
كيفية تدفق البيانات بين الطبقات دون انتهاك قواعد التبعية.
(المجال)"] RepoImpl["تطبيق المستودع
(البيانات)"] -- ينفذ --> RepoInterface RepoImpl -- يستخدم --> DataSource["مصدر البيانات
(API/DB)"]
تدفق التحكم (طلب/استجابة)
المفاهيم الأساسية
💡 قاعدة التبعية
يمكن لتبعي الكود المصدري أن تشير فقط إلى الداخل. لا يمكن لأي شيء في دائرة داخلية (مثل المجال) أن يعرف أي شيء على الإطلاق عن شيء في دائرة خارجية (مثل العرض أو البيانات). هذا يجعل المنطق الأساسي محصنًا ضد تغييرات واجهة المستخدم أو قاعدة البيانات.
🏰 تشبيه من العالم الحقيقي: القلعة
المجال (الملك): يعيش في المركز، يضع القواعد، ولا يعرف شيئًا عن العالم الخارجي.
العرض/البيانات (الحراس): يحمون الملك، ويتعاملون مع الرسل (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 على منطق المجال الأساسي وتفاعلات منطق المجال. ينطوي على تعاون بين الخبراء التقنيين وخبراء المجال.
مثال على جذر التجميع (Aggregate Root)
المفاهيم الأساسية
💡 التصميم الاستراتيجي مقابل التكتيكي
يحدد التصميم الاستراتيجي الحدود واسعة النطاق (السياقات المحددة) وكيفية تعاون الفرق. يوفر التصميم التكتيكي الأنماط (الكيانات، كائنات القيمة، التجميعات) لبناء المنطق الداخلي لتلك السياقات.
🏢 تشبيه من العالم الحقيقي: شركة كبيرة
السياقات المحددة (الأقسام): المبيعات، الموارد البشرية، والهندسة هي أقسام منفصلة.
اللغة الموحدة (المصطلحات): كلمة "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
نمط GetX
| الميزة | 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
- قسّم الأدوات المعقدة إلى مكونات أصغر قابلة لإعادة الاستخدام.
- استخدم
constconstructors حيثما أمكن لتحسين الأداء. - تعامل مع الأخطاء ببراعة واعرض رسائل سهلة الاستخدام.
- حافظ على تحديث التبعيات.
العمارة النظيفة
- حافظ على استقلالية الطبقات.
- يجب ألا تحتوي طبقة المجال على أي تبعيات لـ Flutter.
- استخدم المستودعات لتجريد مصادر البيانات.
إدارة الحالة
- استخدم فئات حالة غير قابلة للتغيير (Equatable).
- أبقِ المنطق خارج واجهة المستخدم (Widgets).
- استخدم
buildWhen/listenWhenلتحسين إعادة البناء.
مصادر التعلم
الوثائق الرسمية
مقالات عن الهيكلة
قنوات الفيديو
استكشاف الأخطاء وإصلاحها
تأكد من أنك قمت بتغليف شجرة الأدوات الخاصة بك بـ BlocProvider. إذا كنت تنتقل إلى مسار جديد، مرر BLoC الموجود باستخدام BlocProvider.value.
تأكد من أنك تقوم بإصدار نسخة جديدة من الحالة. إذا كنت تستخدم Equatable، تأكد من تعيين props بشكل صحيح. لا تقم بتغيير الحالة مباشرة.
استدعِ HydratedStorage.build في دالة main() الخاصة بك قبل runApp(). تأكد من استدعاء WidgetsFlutterBinding.ensureInitialized() أولاً.