المعمل 05: بناء تطبيق Flutter متكامل - من المعمارية إلى منطق التشغيل
نظرة عامة
يرشدك هذا المعمل خلال العملية الكاملة لبناء تطبيق Flutter متكامل من الصفر. ستتعلم كيفية تخطيط المتطلبات قبل كتابة الكود، وبناء هيكل تطبيقك باستخدام معمارية ثلاثية الطبقات نظيفة، وإدارة الألوان عبر نظام ثيمات مركزي (Centralized Theming)، وحل مشكلات التمرير وأحجام الشاشات، وتصميم أصناف نماذج البيانات، وبناء طبقة منطق (Logic Layer) تحسب قيمًا حقيقية وتستخدم حزمًا خارجية.
الأهداف
بنهاية هذا المعمل ستكون قادرًا على:
- تحديد متطلبات التطبيق قبل كتابة أي كود
- فصل الواجهة (UI)، ومنطق الأعمال (Business Logic)، والبيانات (Data) في طبقات منفصلة
- تطبيق مبدأ DRY لكتابة كود Flutter قابل للصيانة
- التعامل مع قيود الواجهة وأحداث تفاعل المستخدم
- تنفيذ نظام ثيمات مركزي باستخدام
Theme.of(context)وThemeData - إصلاح أخطاء تجاوز التخطيط (Overflow) الناتجة عن اختلاف أحجام شاشات الأجهزة
- إنشاء أصناف نماذج Dart لتمثيل البيانات المُهيكلة
- بناء عنصر واجهة لطبقة منطق (Logic Layer) مع فرض صارم لأنواع البيانات
- البحث عن الحزم ودمجها من مستودع Flutter Pub
المتطلبات الأساسية
- الإلمام بعناصر واجهة Flutter (
Column،Row،Text،Container) - صياغة أساسية لأصناف Dart
- بيئة تطوير Flutter تعمل بشكل جيد
خلفية نظرية
المعمارية ثلاثية الطبقات
قبل كتابة سطر كود واحد، يبدأ كل تطبيق Flutter احترافي بفصل واضح للمسؤوليات. هذا هو أهم مبدأ أساسي في بناء تطبيقات قابلة للصيانة.
graph TD
subgraph UI["طبقة الواجهة (UI Layer)"]
A[عناصر الواجهة والشاشات]
B[تفاعلات المستخدم]
C[الحسابات المرئية]
end
subgraph Logic["طبقة منطق الأعمال (Business Logic Layer)"]
D[معالجة التواريخ]
E[منطق البحث]
F[حسابات الأيام]
end
subgraph DataLayer["طبقة البيانات (Data Layer)"]
G[النماذج والأصناف]
H[البيانات الخارجية]
end
A --> D
B --> D
D --> G
E --> G
F --> Gلماذا يهم هذا: عندما تضع المنطق مباشرة داخل الواجهة بشكل ثابت (Hardcoded)، يجبرك كل تحديث على تغيير عدة أماكن في آنٍ واحد. أما التطبيق جيد البنية فيتيح لك تغيير الأشياء في مكان واحد بالضبط. فإذا وجدت نفسك تجري نفس التغيير في عدة أماكن، فهذه علامة واضحة على أن كودك سيء التنظيم.
التخطيط: تحديد المتطلبات
الخطوة الأولى في بناء أي تطبيق هي ليست فتح محرر الأكواد - بل تحديد ما يجب أن يقوم به التطبيق بالضبط. تُسمى هذه العملية بتحديد متطلبات البرنامج.
ما هي المتطلبات؟
تحدد المتطلبات:
- الإجراءات التي يجب أن يكون المستخدم قادرًا على القيام بها
- العناصر المرئية التي يجب أن يعرضها التطبيق
- البيانات التي يحتاج التطبيق للوصول إليها ومعالجتها
الطبقات الثلاث بالتفصيل
بمجرد تحديد المتطلبات، تنظّم تطبيقك في ثلاث طبقات منعزلة:
| الطبقة | المسؤولية | أمثلة |
|---|---|---|
| الواجهة (UI) | العناصر المرئية، مدخلات المستخدم، تخطيط الشاشة | عناصر الواجهة، الأزرار، النصوص |
| منطق الأعمال (Business Logic) | الحسابات، المعالجة، القواعد | حسابات التاريخ، تصفية البحث |
| البيانات (Data) | تخزين المعلومات الخام وتوفيرها | أصناف النماذج، مصادر البيانات |
القاعدة الحاسمة هي: طبقة الواجهة لا تلمس طبقة البيانات مباشرة أبدًا. تعمل طبقة منطق الأعمال كجسر بينهما.
flowchart LR
User([إجراء المستخدم]) --> UI[طبقة الواجهة]
UI -->|طلب| Logic[منطق الأعمال]
Logic -->|استعلام| Data[طبقة البيانات]
Data -->|إرجاع| Logic
Logic -->|نتيجة| UI
UI -->|عرض| Userالتمرين 1 - اكتب وثيقة متطلبات
قبل بدء أي كود في هذه الجلسة، اكتب وثيقة متطلبات لتطبيق ملف شخصي بسيط:
- اسرد كل شاشة سيحتوي عليها التطبيق
- لكل شاشة، صف: ما الذي يراه المستخدم، وما الذي يمكنه فعله، وما البيانات التي تحتاجها الشاشة
- حدد أي العناصر تنتمي لطبقة الواجهة، وأيها لطبقة منطق الأعمال، وأيها لطبقة البيانات
التمرين 2 - ارسم مخطط فصل الاهتمامات (Separation of Concerns)
اختر ميزة واحدة (مثلاً وظيفة بحث):
- ارسم مخططًا كتليًا (Block Diagram) بثلاثة أقسام: الواجهة، منطق الأعمال، والبيانات
- تتبّع كيف يتدفق إدخال بحث المستخدم من الواجهة إلى منطق الأعمال إلى طبقة البيانات ثم يعود
- تأكد من عدم وجود أي سهم يربط الواجهة مباشرة بالبيانات - يجب أن تمر جميع الأسهم عبر منطق الأعمال دائمًا
أفضل ممارسات الواجهة
طريقة build وتخطيط عناصر الواجهة
طريقة build هي المكان الذي يبني فيه Flutter واجهتك. توضع عناصر الواجهة بداخلها الواحد تحت الآخر مباشرة وتُنفَّذ باستخدام منطق متسق.
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('First Item'),
SizedBox(height: 60), // spacing greater than 50 pixels
Text('Second Item'),
SizedBox(height: 60),
Text('Third Item'),
],
);
}شرح سطرًا بسطر:
Column- يرتّب العناصر الفرعية رأسيًا، الواحد أسفل الآخرSizedBox(height: 60)- يضيف فجوة مقدارها 60 بكسل بين العناصر- يوضع كل عنصر
Textبشكل تسلسلي أسفل العنصر السابق، وتُنفَّذ بنفس منطق التخطيط
مبدأ DRY - قابلية صيانة الكود
DRY تعني: لا تكرر نفسك (Don't Repeat Yourself).
القاعدة الأساسية:
إذا أردت تغيير شيء ما ووجدت نفسك مضطرًا لتغييره في أكثر من مكان، فإن كودك مكتوب بشكل سيء. الكود جيد الكتابة يتيح لك إجراء هذا التغيير في مكان واحد بالضبط.
نمط سيء - اللون مكرر في كل عنصر واجهة:
Container(color: Color(0xFF1565C0), child: Text('Header')),
Container(color: Color(0xFF1565C0), child: Text('Section')),
Container(color: Color(0xFF1565C0), child: Text('Footer')),تغيير اللون يتطلب تحديث ثلاثة أسطر منفصلة.
نمط DRY - اللون مُعرَّف مرة واحدة:
final primaryColor = Color(0xFF1565C0);
Container(color: primaryColor, child: Text('Header')),
Container(color: primaryColor, child: Text('Section')),
Container(color: primaryColor, child: Text('Footer')),الآن تغيّر اللون في مكان واحد وتتحدث كل الحاويات الثلاث فورًا.
تفاعل المستخدم: عرض تفاصيل العنصر
عندما ينقر المستخدم على عنصر، يجب أن يعرض التطبيق فورًا التفاصيل الخاصة بذلك العنصر تحديدًا:
- الخطوة 1: ينقر المستخدم على عنصر
- الخطوة 2: يُطلق التطبيق استجابة تكشف عن التفاصيل الخاصة بذلك العنصر
GestureDetector(
onTap: () {
showDetails(item); // displays details for this specific item
},
child: ListTile(title: Text(item.name)),
)ضبط القيود - القيم الدنيا
يمكنك تحديد أصغر حجم يُسمَح لعنصر باتخاذه:
Container(
constraints: BoxConstraints(
minWidth: 100, // smallest possible width
minHeight: 50, // smallest possible height
),
child: Text('Content'),
)هذا يضمن ألا يصغر العنصر عن الحد الأدنى المحدد، بغض النظر عن محتواه.
إعادة استخدام منطق الكود لاستقبال البيانات
عندما ينتهي المستخدم من إجراء اختيار، يجب أن يستخدم التطبيق منطقًا متسقًا وقابلًا لإعادة الاستخدام لاستقبال البيانات المختارة ومعالجتها:
// Reusable handler called after any selection completes
void onSelectionComplete(SelectedItem item) {
processSelection(item); // same logic handles all selection results
}التمرين 3 - إعادة هيكلة كود متكرر
- افتح مشروع Flutter الخاص بك وابحث عن أي عنصر واجهة يتكرر فيه نفس النمط (اللون، الحشو، أو نمط النص) في ثلاثة أماكن أو أكثر
- استخلص هذا النمط إلى متغيّر أو ثابت واحد
- استبدل كل التكرارات بهذا المتغيّر
- غيّر المتغيّر مرة واحدة وتأكد أن كل الحالات تتحدث في آنٍ واحد
التمرين 4 - إضافة تفاعل وقيود
- أنشئ قائمة من ثلاثة عناصر معروضة داخل
Column - لُف كل عنصر داخل
GestureDetectorيعرضSnackBarباسم ذلك العنصر عند النقر عليه - أضف
ContainerمعBoxConstraintsتحددminWidthبقيمة 150 وminHeightبقيمة 80 - تحقق من أن الحاوية تحترم الحد الأدنى للحجم حتى عندما يكون محتواها صغيرًا جدًا
نظام الثيمات المركزي عبر Theme.of
المشكلة: الألوان الثابتة (Hardcoded)
عندما تحدد لونًا مباشرة على كل عنصر واجهة، يصبح تحديث التطبيق بشكل شامل مستحيلًا:
// BAD — same color hardcoded on every element
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Color(0xFF1565C0)),
),
child: Text('Submit'),
onPressed: () {},
),
Text('Title', style: TextStyle(color: Color(0xFF1565C0))),
Container(color: Color(0xFF1565C0), child: Text('Box')),لتطبيق الوضع الليلي (Dark Mode) أو لون علامة تجارية جديد، يجب عليك تحديد وتحديث كل سطر على حدة.
الحل: نظام ثيمات مركزي
عرّف خصائص التصميم مرة واحدة داخل كائن ThemeData. تشير كل عناصر الواجهة إلى هذا المصدر الوحيد:
MaterialApp(
theme: ThemeData(
primaryColor: Color(0xFF1565C0),
colorScheme: ColorScheme.fromSeed(
seedColor: Color(0xFF1565C0),
),
),
home: MyHomePage(),
)الوصول للثيم - Theme.of(context)
بدلًا من قيمة لون ثابتة، أشِر إلى الثيم المركزي:
Container(
color: Theme.of(context).primaryColor,
child: Text(
'Hello',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
),
),
)شرح سطرًا بسطر:
Theme.of(context)- يبحث عن كائنThemeDataالمُعرَّف على مستوىMaterialApp.primaryColor- يسترجع اللون الأساسي من ذلك المصدر المركزي.colorScheme.onPrimary- يسترجع لون النص المصمم ليتباين مع اللون الأساسي- تغيير
primaryColorداخلThemeDataمرة واحدة يحدّث تلقائيًا كل عنصر واجهة يشير إليه
تنفيذ تغييرات شاملة - الوضع الليلي (Dark Mode)
مع نظام الثيمات المركزي، يصبح تطبيق الوضع الليلي مجرد تغيير في سطر واحد:
MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.dark, // change this one line to switch the entire app
home: MyHomePage(),
)لا حاجة لأي تغييرات في كود عناصر الواجهة. يتحدث التطبيق بأكمله عبر ThemeData المركزي.
flowchart TD
A["ThemeData\nمصدر مركزي واحد"] --> B[عنصر Button]
A --> C[عنصر Text]
A --> D[عنصر Container]
A --> E[عنصر AppBar]التمرين 5 - الانتقال إلى نظام الثيمات المركزي
- اختر شاشة تحتوي على ثلاثة عناصر واجهة أو أكثر تستخدم نفس اللون الثابت
- احذف كل قيم الألوان الثابتة من تلك العناصر
- عرّف كائن
ThemeDataداخلMaterialAppالخاص بك بذلك اللون كـprimaryColor - استبدل كل لون محذوف بـ
Theme.of(context).primaryColor - غيّر
primaryColorداخلThemeDataمرة واحدة وتحقق من تحديث كل العناصر في آنٍ واحد
التمرين 6 - تبديل الوضع الليلي
- أضف كلًا من
theme: ThemeData.light()وdarkTheme: ThemeData.dark()إلىMaterialApp - اضبط
themeMode: ThemeMode.dark - شغّل التطبيق ولاحظ التغيير المرئي الكامل دون أي تعديل على أي عنصر واجهة
حل مشكلات حجم الشاشة والتمرير
مشكلة تعدد أجهزة العرض (Device Fragmentation)
قد يبدو تصميم تطبيقك مثاليًا على جهاز التطوير الخاص بك، لكن المستخدمين النهائيين لديهم هواتف بأبعاد شاشة مختلفة - أطوال وعروض مختلفة. التخطيط الذي يلائم شاشتك قد يتجاوز حدود شاشة أصغر ويصبح غير مرئي بالكامل.
flowchart TD
A[التطبيق يبدو صحيحًا على جهازك] --> B{جهاز المستخدم بشاشة أصغر}
B -->|العناصر تلائم الشاشة| C[التطبيق يُعرَض بشكل صحيح]
B -->|العناصر تتجاوز الحدود| D[خطأ ظاهر على الشاشة\nالمحتوى مقصوص]
D --> E{اختر حلًا}
E --> F[SingleChildScrollView\nيلف الـ Column]
E --> G[ListView\nيستبدل الـ Column]
E --> H[Expanded\nالعناصر تُعاد تحجيمها لتلائم الشاشة]عندما يتجاوز المحتوى حدود Column، يعرض Flutter خطأ تجاوز مرئيًا (خطوط صفراء وسوداء) عند حافة الشاشة.
الحل الأساسي: SingleChildScrollView
خذ كل العناصر داخل Column الخاص بك ولُفّها داخل SingleChildScrollView:
// BEFORE — causes overflow on small screens
Column(
children: [
WidgetOne(),
WidgetTwo(),
WidgetThree(),
WidgetFour(),
],
)
// AFTER — user can scroll to see all content
SingleChildScrollView(
child: Column(
children: [
WidgetOne(),
WidgetTwo(),
WidgetThree(),
WidgetFour(),
],
),
)شرح سطرًا بسطر:
SingleChildScrollView- عنصر غلاف يتيح التمرير الرأسي لعنصره الفرعي الوحيد- تبقى بنية
Columnكما هي بداخله دون تغيير - يمرر المستخدمون للأعلى لكشف العناصر التي تمتد خارج منطقة الشاشة المرئية
- يختفي خطأ التجاوز لأن المحتوى لم يعد مجبرًا على الملاءمة داخل حدود ثابتة
البديل 1: ListView
استبدل Column بالكامل بـ ListView، الذي يحتوي على تمرير مدمج بشكل أصلي:
ListView(
children: [
WidgetOne(),
WidgetTwo(),
WidgetThree(),
WidgetFour(),
],
)لا حاجة لأي غلاف - يتعامل ListView مع التمرير الخاص به تلقائيًا.
البديل 2: Expanded
استخدم Expanded مع العناصر داخل Column لجعلها تملأ وتتكيف مع المساحة المتاحة:
Column(
children: [
Expanded(child: WidgetOne()), // stretches to fill remaining height
WidgetTwo(),
],
)يُجبر Expanded العنصر الفرعي على إعادة التحجيم ليلائم حدود الشاشة. الشاشة لا تُمرَّر؛ بل يتكيف العنصر مع حجمه ليلائم المساحة.
الاختيار بين الأساليب الثلاثة
| الأسلوب | متى يُستخدم |
|---|---|
SingleChildScrollView | يجب أن يبقى المحتوى بحجمه الكامل؛ يُمرِّر المستخدم لرؤية كل شيء |
ListView | قائمة من عناصر متكررة تحتاج إلى تمرير |
Expanded | يجب أن ينكمش المحتوى ليلائم الشاشة دون تمرير |
التمرين 7 - إحداث وإصلاح خطأ تجاوز (Overflow)
- أنشئ
Columnتحتوي على 8 عناصرContainerكبيرة، كل منها بارتفاعheight: 120 - شغّل التطبيق على محاكي صغير (بروفايل جهاز 4 بوصات)
- لاحظ خطأ التجاوز الظاهر في أسفل الشاشة
- لُف
ColumnداخلSingleChildScrollView - تأكد أن الخطأ اختفى وأنك تستطيع التمرير عبر كل الحاويات
التمرين 8 - قارن بين الأساليب الثلاثة
- نفّذ القائمة المتجاوزة باستخدام
SingleChildScrollView+Column - أعد تنفيذها باستخدام
ListViewفقط - أعد تنفيذها باستخدام
Expanded(لاحظ: يُعاد تحجيم المحتوى بدلًا من تمريره) - اكتب ملاحظة قصيرة تقارن ما يختبره المستخدم في كل حالة
نماذج بيانات الواجهة (UI Data Models)
ما هو نموذج البيانات؟
نموذج البيانات (Data Model) هو صنف Dart يجمّع قيمًا مترابطة معًا. فبدلًا من تمرير متغيرات فردية وغير مترابطة بين عناصر الواجهة، تجمّعها داخل صنف. يصبح هذا الصنف الحاوية الوحيدة لقطعة من البيانات، ويحتفظ بتلك القيم طوال المدة التي تحتاجها.
إنشاء نموذج بيانات أساسي
// person_model.dart
class PersonModel {
final String name;
final int age;
final DateTime birthDate;
PersonModel({
required this.name,
required this.age,
required this.birthDate,
});
}شرح سطرًا بسطر:
class PersonModel- يعرّف صنف Dart جديد باسمPersonModelfinal String name- قيمة محفوظة داخل الصنف: اسم الشخص (لا يمكن تغييرها بعد ضبطها)final int age- قيمة محفوظة داخل الصنف: عمر الشخصfinal DateTime birthDate- قيمة محفوظة داخل الصنف: تاريخ ميلاد الشخصPersonModel({required this.name, ...})- الباني (Constructor)؛ يجب توفير كل الحقول عند إنشاء نسخة (Instance)
ربط نموذج بعنصر واجهة
مرّر النموذج كمعامل مُحدَّد النوع (Typed Parameter) إلى عنصر الواجهة الذي سيعرض بياناته:
class PersonCard extends StatelessWidget {
final PersonModel person; // only accepts a PersonModel — no other type
const PersonCard({required this.person, super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(person.name),
Text('Age: ${person.age}'),
Text('Born: ${person.birthDate.year}'),
],
);
}
}الاستخدام:
PersonCard(
person: PersonModel(
name: 'Ahmed',
age: 22,
birthDate: DateTime(2003, 5, 15),
),
)التمرين 9 - ابنِ صنف نموذج بيانات
- أنشئ ملفًا جديدًا
lib/models/person_model.dart - عرّف صنف
PersonModelبثلاثة حقول:name(نص)،age(عدد صحيح)،birthDate(تاريخ) - أضف باني (Constructor) يتطلب الحقول الثلاثة جميعها
- أنشئ
StatelessWidgetباسمPersonCardيستقبلPersonModelكمعامل مطلوب - اعرض الحقول الثلاثة داخل عنصر الواجهة باستخدام عناصر
Text
التمرين 10 - املأ قائمة من النماذج
- أنشئ
List<PersonModel>تحتوي على ثلاث عناصر (أسماء وأعمار وتواريخ ميلاد مختلفة) - استخدم
ColumnلعرضPersonCardلكل عنصر في القائمة - تحقق من أن تغيير قيمة داخل
PersonModelيحدّث فورًا البطاقة المعروضة
طبقة المنطق (The Logic Layer)
ما هي طبقة المنطق؟
تحتوي طبقة المنطق على الحسابات والمعالجة التي يقوم بها تطبيقك، منفصلة تمامًا عن الواجهة. في هذا الجزء من المعمل، يبني التطبيق عنصر واجهة يحسب عدد الأيام المرتبطة بشخص معين. حساب الأيام مهمة تخص منطق الأعمال، وليست مهمة تخص الواجهة - فالواجهة تعرض النتيجة فقط.
فرض الأنواع الصارم في عناصر الواجهة
مبدأ أساسي: يجب أن يستقبل عنصر الواجهة نوع البيانات الذي يتوقعه بالضبط. إذا كان عنصر الواجهة مصممًا للعمل مع PersonModel، فإن تمرير أي نوع آخر يُعد خطأً يكتشفه Dart وقت الترجمة (Compile Time).
class DaysCalculatorWidget extends StatelessWidget {
final PersonModel person; // strictly requires a PersonModel
const DaysCalculatorWidget({required this.person, super.key});
int calculateDaysAlive() {
final today = DateTime.now();
return today.difference(person.birthDate).inDays;
}
@override
Widget build(BuildContext context) {
return Text('Days alive: ${calculateDaysAlive()}');
}
}شرح سطرًا بسطر:
final PersonModel person- يفرض الباني (Constructor) نوعًا صارمًا؛ لا يُقبَل إلاPersonModelcalculateDaysAlive()- دالة المنطق، منفصلة تمامًا عن طريقة buildDateTime.now().difference(person.birthDate).inDays- يطرح تاريخ الميلاد من اليوم الحالي ويُرجع النتيجة بعدد الأيام الكاملةbuild- تعرض النتيجة فقط؛ لا تُجري الحساب بنفسها
مسار تنفيذ المنطق
قبل كتابة منطق مخصص من الصفر، اسأل نفسك دائمًا:
هل أحتاج فعلًا لبناء هذا بنفسي، أم أن هناك حلًا جاهزًا موجودًا بالفعل؟
flowchart TD
A[حدد المنطق المطلوب] --> B{ابحث في Flutter Pub}
B -->|الحزمة موجودة| C[قيّم الحزمة]
B -->|لا توجد حزمة| D[اكتب منطقًا مخصصًا]
C --> E{تحقق من تبعياتها}
E --> F[ادمج الحزمة]
D --> G[نفّذ من الصفر]
F --> H[المنطق مكتمل]
G --> Hالبحث عن الحزم في Flutter Pub
يستضيف مستودع حزم Flutter (pub.dev) حزمًا أنشأها مطورون آخرون. قبل تنفيذ منطق معقد يدويًا:
- اذهب إلى pub.dev
- ابحث عن كلمات مفتاحية تتعلق بمهمتك (مثلاً: "date"، "time difference"، "age calculator")
- قيّم شعبية الحزمة، وحالة صيانتها، والترخيص الخاص بها
- اقرأ توثيقها وتحقق من تبعياتها (Dependencies) - بعض الحزم تتطلب حزمًا أخرى لتعمل
فهم التبعيات (Dependencies)
التبعية هي عندما تعتمد حزمة على أخرى لكي تعمل بشكل صحيح:
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
intl: ^0.19.0 # date formatting package added from Pubكيف تعمل التبعيات:
- تستخدم الحزمة أ الحزمة ب داخليًا
- عند إضافة الحزمة أ لمشروعك، يجب أن تكون الحزمة ب موجودة أيضًا
- تشغيل
flutter pub getيحل ويحمّل كل التبعيات المطلوبة تلقائيًا
بناء صنف منطق معزول
إنشاء صنف متكامل والتحقق منه بشكل مستقل هو أسلوب ممتاز لتطوير المنطق:
// lib/logic/days_calculator.dart
class DaysCalculator {
final DateTime birthDate;
DaysCalculator(this.birthDate);
int calculateDaysAlive() {
return DateTime.now().difference(birthDate).inDays;
}
bool isOver1000DaysOld() {
return calculateDaysAlive() > 1000;
}
}شرح سطرًا بسطر:
class DaysCalculator- صنف منطق خالص: بدون عناصر واجهة، بدون UI، بدونBuildContextfinal DateTime birthDate- المُدخَل الوحيد الذي يحتاجه الصنفcalculateDaysAlive()- يحسب ويُرجع عدد الأيام منذ الميلادisOver1000DaysOld()- يستخدمcalculateDaysAlive()داخليًا للإجابة عن سؤال منطقي (Boolean)- يمكن اختبار هذا الصنف بشكل مستقل دون تشغيل التطبيق كاملًا
التحقق من الصنف:
void main() {
final calc = DaysCalculator(DateTime(2003, 5, 15));
print(calc.calculateDaysAlive()); // prints the number of days
print(calc.isOver1000DaysOld()); // prints true or false
}معالجة الأحداث في طبقة المنطق
تتعامل طبقة المنطق أيضًا مع الأحداث (Events) - وهي المواقف التي يحدث فيها شيء ما ويجب أن تتولى طبقة المنطق معالجته. تُطلِق الواجهة الحدث؛ وتعالجه طبقة المنطق؛ ثم تعود النتيجة إلى الواجهة.
// When a selection event occurs, the logic layer takes over
void onPersonSelected(PersonModel selectedPerson) {
final calculator = DaysCalculator(selectedPerson.birthDate);
final days = calculator.calculateDaysAlive();
displayResult(days); // passes the result back to the UI
}لا تعرف طبقة المنطق ولا تهتم أبدًا بكيفية عرض النتيجة.
التمرين 11 - ابنِ عنصر واجهة حاسبة أيام صارم النوع
- استخدم
PersonModelمن القسم السابق - أنشئ
DaysCalculatorWidgetيستقبل فقطPersonModelكمعامل مطلوب - نفّذ
calculateDaysAlive()داخل عنصر الواجهة باستخدامDateTime.now().difference(person.birthDate).inDays - اعرض النتيجة داخل عنصر
Text - حاول تمرير
intبسيط بدلًا منPersonModelللتأكد من أن Dart يرفضه وقت الترجمة
التمرين 12 - ابحث عن حزمة وادمجها
- افتح pub.dev في متصفحك
- ابحث عن "date difference" أو "age calculator"
- جد حزمة تحسب الفوارق الزمنية بين التواريخ
- أضفها إلى
pubspec.yamlوشغّلflutter pub get - اقرأ توثيق الحزمة وحدد كل تبعية تتطلبها
- أعد كتابة حساب الأيام لديك باستخدام الحزمة
- تأكد أن الناتج يطابق حسابك اليدوي
التمرين 13 - اعزل المنطق في صنف منفصل
- أنشئ ملفًا جديدًا
lib/logic/days_calculator.dart - عرّف صنف
DaysCalculatorبخاصيةbirthDate - أضف
calculateDaysAlive()تُرجعint - أضف
isOver1000DaysOld()تُرجعbool - شغّل اختبارًا سريعًا باستخدام
void main()لطباعة كلتا النتيجتين لتاريخ ميلاد معروف - اربط الصنف بـ
DaysCalculatorWidgetعن طريق إنشاء نسخة منه داخل عنصر الواجهة
إدارة الحالة باستخدام StatefulWidget
ما هي الحالة (State)؟
الحالة (State) هي أي بيانات يمكن أن تتغير مع الوقت، ويجب أن يتسبب تغيّرها في تحديث الواجهة. في تطبيق حاسبة العمر، تُعد التواريخ المختارة والنتائج المحسوبة حالة - فعندما يختار المستخدم تاريخًا جديدًا، يجب أن يُعاد عرض الواجهة لإظهاره.
عنصر الواجهة الذي لا يحمل بيانات قابلة للتغيير هو StatelessWidget. أما عنصر الواجهة الذي يملك بيانات تتغير أثناء التشغيل فهو StatefulWidget.
StatelessWidget مقابل StatefulWidget
| النوع | متى يُستخدم | هل يمكن أن يتغير بعد البناء؟ |
|---|---|---|
StatelessWidget | عرض فقط، بدون تفاعل | لا |
StatefulWidget | مدخلات المستخدم، النتائج المحسوبة، أي شيء يتحدث | نعم، عبر setState() |
نمط StatefulWidget
يُقسَّم StatefulWidget إلى صنفين:
// 1. The widget itself — immutable, holds configuration
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
// 2. The State object — holds the mutable data
class _HomeScreenState extends State<HomeScreen> {
DateTime _birthDate = DateTime(2000, 1, 1); // mutable state
Map<String, int>? _age; // mutable state
@override
Widget build(BuildContext context) {
return Text('$_birthDate');
}
}شرح سطرًا بسطر:
StatefulWidget- صنف عنصر الواجهة نفسه يبقى غير قابل للتغيير (Immutable)؛ فقط يعرف كيف ينشئStatecreateState()- يُرجع كائنStateالذي سيملك البيانات القابلة للتغيير_HomeScreenState- الصنف الخاص (Private) حيث تعيش كل الحقول القابلة للتغييرDateTime _birthDate- متغيّر حالة (State Variable)؛ تغييره يُطلق عملية إعادة بناء
setState() - مُحفِّز إعادة البناء
setState() هي الآلية التي تُخبر Flutter: "البيانات تغيّرت - أعد بناء الواجهة."
// Without setState: data changes but UI stays the same (stale)
_birthDate = newDate; // UI never updates
// With setState: data changes AND Flutter schedules a rebuild
setState(() {
_birthDate = newDate; // UI updates on next frame
});كيف تعمل:
- تستدعي
setState(() { ... })مع تغيير البيانات داخل الدالة المغلقة (Closure) - يضع Flutter علامة على هذا العنصر بأنه "متسخ" (Dirty)
- في الإطار التالي، يستدعي Flutter
build()مرة أخرى بالبيانات الجديدة - تعكس الواجهة الحالة المُحدَّثة
ربط الحالة بالمعمارية ثلاثية الطبقات
void _calculate() {
// DATA LAYER: build the typed model from current state
final model = AgeInputModel(
birthDate: _birthDate,
todayDate: _todayDate,
);
// BUSINESS LOGIC LAYER: run the calculation
final calculator = AgeCalculator(model);
// UI LAYER: setState triggers rebuild with new results
setState(() {
_age = calculator.calculateAge();
_nextBirthday = calculator.calculateNextBirthday();
});
}المسار: المستخدم ينقر CALCULATE → طبقة الواجهة تقرأ الحالة → تنشئ نموذج بيانات → تمرره لطبقة المنطق → يُرجع المنطق النتيجة → تُحدّث setState() الواجهة.
التمرين 14 - أضف حالة إلى حاسبة العمر
- حوّل
HomeScreenمنStatelessWidgetإلىStatefulWidget - عرّف
_birthDate،_todayDate،_age، و_nextBirthdayكمتغيرات حالة - استدعِ
setState()داخلonDateSelectedبحيث يتحدث حقل التاريخ عندما يختار المستخدم تاريخًا - استدعِ
setState()داخل_calculate()بعد تشغيلAgeCalculatorلعرض النتائج - استدعِ
setState()داخل_clear()لإعادة ضبط كل الحقول - تحقق من: اختيار تاريخ جديد ← يعرض الحقل التاريخ الجديد؛ الضغط على CALCULATE ← تمتلئ مربعات النتائج؛ الضغط على CLEAR ← تُعاد المربعات إلى الصفر
الملخص
| الموضوع | المفهوم الأساسي |
|---|---|
| المتطلبات | حدد ما يحتاجه التطبيق قبل كتابة أي كود |
| أفضل ممارسات الواجهة | مبدأ DRY: غيّر في مكان واحد، وليس عدة أماكن |
| نظام الثيمات | Theme.of(context) لإدارة الألوان بشكل مركزي |
| التمرير | SingleChildScrollView يحل مشكلة التجاوز على الشاشات الصغيرة |
| نماذج الواجهة | أصناف Dart تحمل بيانات مُهيكلة لعناصر الواجهة |
| طبقة المنطق | افصل الحسابات عن الواجهة؛ تحقق من Flutter Pub أولًا |
| إدارة الحالة | StatefulWidget + setState() لبيانات الواجهة القابلة للتغيير |
الدرس الأهم: المعمارية تأتي أولًا. تخطيط الطبقات الثلاث - الواجهة، ومنطق الأعمال، والبيانات - قبل كتابة الكود هو ما يجعل تطبيق Flutter قابلًا للصيانة واحترافيًا.