إتقان Google Flutter و Dart — بناء تطبيقات جميلة وعالية الأداء تعمل على أنظمة متعددة للجوال والويب وسطح المكتب
وصف الدورة
تأخذك هذه الدورة الشاملة القائمة على المشاريع من مبتدئ تمامًا إلى مطوّر فلاتر محترف. ستتعلم لغة Dart من الصفر، وتتقن إطار عمل فلاتر، وتصمم واجهات مستخدم متجاوبة وقابلة للتكيف، وتدير الحالة (State) باحترافية، وتدمج واجهات برمجة التطبيقات (APIs) وقواعد البيانات، وتطبّق المصادقة والأمان، وتحسّن الأداء، وتكتب الاختبارات، وتطبّق العمارة النظيفة (Clean Architecture)، وتنشر تطبيقات جاهزة للإنتاج على أندرويد و iOS والويب و Windows و macOS و Linux.
يتم تدريس كل مفهوم بشرح واضح، وصياغة حديثة، وأمثلة برمجية كاملة وعملية، والأخطاء الشائعة، وأفضل الممارسات، وتمارين، ومشاريع مصغّرة. بنهاية الدورة ستكون قادرًا على تصميم وتطوير واختبار وتأمين وتحسين ونشر تطبيقات احترافية متعددة المنصات بشكل مستقل.
الفئة المستهدفة
- المبتدئون تمامًا الذين لا تتوفر لديهم خبرة سابقة (أو تتوفر خبرة محدودة) في تطوير تطبيقات الجوال
- مطورو الويب الراغبون في التوسع نحو الجوال
- الطلاب والمتحولون المهنيون الساعون للحصول على وظائف مطوّر فلاتر
- المطورون الذين يعرفون لغات أخرى ويريدون إتقان فلاتر باحترافية
المتطلبات الأساسية
- إلمام أساسي بالحاسوب
- الإلمام بأي لغة برمجة مفيد وليس شرطًا
- الاستعداد للتدرّب يوميًا وإكمال المشاريع
أهداف التعلّم
بنهاية هذه الدورة ستكون قادرًا على:
- كتابة كود Dart نظيف وحديث مع الأمان من القيم الفارغة (null safety) و async/await والتدفقات (Streams) وميزات اللغة المتقدمة
- بناء واجهات مستخدم معقدة ومتجاوبة وقابلة للتكيف باستخدام عناصر فلاتر (widgets)
- تنفيذ التنقّل الاحترافي والنماذج (forms) وإدخال المستخدم
- اختيار وتطبيق الحل المناسب لإدارة الحالة (setState، Provider، Riverpod، BLoC)
- استهلاك واجهات REST API، والتعامل مع المصادقة، وإدارة البيانات دون اتصال
- دمج خدمات Firebase
- تطبيق العمارة النظيفة ومبادئ SOLID
- كتابة اختبارات الوحدة (unit) والواجهة (widget) والتكامل (integration)
- تحسين الأداء وتشخيص المشكلات باستخدام Flutter DevTools
- نشر التطبيقات على Google Play و App Store والويب
- استخدام الأدوات الحديثة و CI/CD والتطوير بمساعدة الذكاء الاصطناعي
الوقت التقديري للتعلّم
- مكثّف بدوام كامل: 10–12 أسبوعًا
- بدوام جزئي (10–15 ساعة/أسبوع): 4–6 أشهر
- إجمالي المحتوى الموجّه + التدريب: حوالي 180–220 ساعة
بيئة التطوير الموصى بها
- Visual Studio Code أو Android Studio / IntelliJ IDEA
- Flutter SDK (القناة المستقرة)
- Dart SDK (مرفق مع فلاتر)
- محاكي أندرويد و/أو جهاز أندرويد فعلي
- Xcode (لنظام macOS فقط) لتطوير iOS
الأجهزة/البرامج المطلوبة
- حاسوب يعمل بنظام Windows 10/11، أو macOS 12+، أو Linux (يُفضّل Ubuntu 20.04+)
- ذاكرة وصول عشوائي (RAM) لا تقل عن 8 جيجابايت (يُفضّل 16 جيجابايت)
- مساحة تخزين فارغة 20+ جيجابايت
- اتصال إنترنت مستقر لتنزيل الحزم واختبار واجهات برمجة التطبيقات
الفرص الوظيفية بعد إكمال الدورة
- مطوّر تطبيقات جوال بفلاتر
- مهندس تطبيقات جوال متعددة المنصات
- مطوّر فلاتر مبتدئ إلى متوسط
- مطوّر فلاتر مستقل (فريلانسر)
- مهندس معماري لتطبيقات الجوال (مع اكتساب خبرة إضافية)
- أدوار full-stack تتضمن واجهات فلاتر الأمامية
نظرة عامة على هيكل الدورة
- المستوى 1 — مقدمة في تطوير تطبيقات الجوال وفلاتر
- المستوى 2 — تثبيت وإعداد فلاتر
- المستوى 3 — برمجة Dart من الصفر
- المستوى 4 — أول تطبيق فلاتر لك
- المستوى 5 — عناصر فلاتر (Widgets)
- المستوى 6 — واجهة المستخدم والتصميم المتجاوب
- المستوى 7 — التنقّل والتوجيه (Navigation & Routing)
- المستوى 8 — إدخال المستخدم والنماذج
- المستوى 9 — إدارة الحالة (State Management)
- المستوى 10 — العمل مع واجهات برمجة التطبيقات (APIs)
- المستوى 11 — التخزين المحلي وقواعد البيانات
- المستوى 12 — Firebase مع فلاتر
- المستوى 13 — المصادقة والأمان
- المستوى 14 — فلاتر المتقدم
- المستوى 15 — عمارة فلاتر (Architecture)
- المستوى 16 — الاختبار (Testing)
- المستوى 17 — تحسين الأداء
- المستوى 18 — الحزم والإضافات (Packages & Plugins)
- المستوى 19 — ميزات الجهاز
- المستوى 20 — فلاتر على الويب وسطح المكتب
- المستوى 21 — نشر التطبيقات
- المستوى 22 — CI/CD
- المستوى 23 — الذكاء الاصطناعي + تطوير فلاتر
- المستوى 24 — مشاريع احترافية
- المستوى 25 — أفضل ممارسات فلاتر
- المستوى 26 — الأخطاء الشائعة في فلاتر
- المستوى 27 — خارطة طريق مطوّر فلاتر
- المستوى 28 — التمارين والتقييمات
- المستوى 29 — اختبار الشهادة الاحترافية النهائي
المستوى 1 — مقدمة في تطوير تطبيقات الجوال وفلاتر
الدرس 1.1 — ما هو تطوير تطبيقات الجوال؟
أهداف التعلّم
- فهم ماهية تطوير تطبيقات الجوال
- التمييز بين الأسلوب الأصلي (Native) والأسلوب متعدد المنصات (Cross-Platform)
- التعرّف على المنصتين الرئيسيتين (أندرويد و iOS)
لماذا هذا مهم؟
يحتاج كل عمل تجاري حديث تقريبًا إلى حضور على الجوال. فهم هذا المشهد يساعدك على اختيار التقنية المناسبة ووضع توقعات واقعية.
شرح تفصيلي
تطوير تطبيقات الجوال هو عملية إنشاء برمجيات تعمل على الهواتف الذكية والأجهزة اللوحية. المنصتان الأكثر انتشارًا هما أندرويد من جوجل و iOS من آبل.
أندرويد مقابل iOS
- أندرويد مفتوح المصدر، ويعمل على مجموعة واسعة من الأجهزة، ويستخدم Java أو Kotlin كلغات أساسية.
- iOS مغلق المصدر، ويعمل فقط على أجهزة آبل، ويستخدم بشكل أساسي Swift (أو Objective-C).
الأصلي (Native) مقابل متعدد المنصات (Cross-Platform)
- التطوير الأصلي يعني كتابة قاعدتي كود منفصلتين لأندرويد و iOS. يمنح هذا عادةً أفضل أداء وأعمق تكامل مع المنصة، لكنه يضاعف تكلفة التطوير والصيانة.
- أُطر العمل متعددة المنصات تتيح لك كتابة معظم الكود مرة واحدة وتشغيله على منصات متعددة.
مكانة فلاتر
فلاتر هو مجموعة أدوات واجهة مستخدم متعددة المنصات أنشأتها جوجل. يستخدم لغة Dart ويُترجم إلى كود آلة أصلي، مما يوفر أداءً قريبًا من الأصلي مع السماح بقاعدة كود واحدة لأندرويد و iOS والويب و Windows و macOS و Linux.
متى يكون فلاتر هو الخيار الأفضل
- عندما تحتاج إلى الإطلاق على منصات متعددة بسرعة
- عندما تريد فريقًا واحدًا وقاعدة كود واحدة
- عندما تقدّر واجهات مستخدم قابلة للتخصيص بشكل كبير وجميلة
- عندما يكون الأداء مهمًا لكنك لا تحتاج إلى ميزات خاصة بمنصة معينة بعمق شديد قد تتطلب استخدامًا مكثفًا لقنوات المنصة (platform channels)
الأخطاء الشائعة
- افتراض أن فلاتر مخصص للجوال فقط (فهو يستهدف أيضًا الويب وسطح المكتب)
- اختيار فلاتر عندما يتطلب التطبيق استخدامًا مكثفًا لواجهات برمجة تطبيقات جديدة جدًا أو نادرة دون تقييم توفر الإضافات (plugins)
أفضل الممارسات
- قيّم دائمًا متطلبات المشروع المحددة قبل اختيار التقنية
- جرّب الميزات الحرجة مبكرًا
تمرين تطبيقي
اكتب فقرة قصيرة تقارن فيها مزايا التطوير الأصلي مقابل فلاتر لتطبيق تجارة إلكترونية بسيط.
مشروع مصغّر
ابحث عن ثلاثة تطبيقات حقيقية مبنية بفلاتر ودوّن المنصات التي تدعمها.
الدرس 1.2 — عمارة فلاتر والمفاهيم الأساسية
أهداف التعلّم
- فهم العمارة الطبقية لفلاتر
- تعلّم أدوار محرك فلاتر (Engine) والإطار (Framework) والمُضمِّن (Embedder)
- استيعاب مفهوم العناصر (widgets) وشجرة العناصر
شرح تفصيلي
يمتلك فلاتر عمارة طبقية:
- الإطار (Dart) — مكتبة واجهة المستخدم عالية المستوى المكتوبة بلغة Dart. هذا ما تتعامل معه يوميًا (Material، Cupertino، العناصر، الإيماءات، الرسوم المتحركة، إلخ).
- المحرك (++C) — مسؤول عن العرض منخفض المستوى (Skia أو Impeller)، وتخطيط النص، وإدخال/إخراج الملفات، وقنوات المنصة.
- المُضمِّن (Embedder) — كود خاص بالمنصة يستضيف محرك فلاتر على أندرويد و iOS و Windows، إلخ.
العناصر (Widgets)
في فلاتر، كل شيء هو عنصر (widget). العنصر هو وصف غير قابل للتغيير (immutable) لجزء من واجهة المستخدم. تشكّل العناصر شجرة. عندما تتغيّر حالة التطبيق، يعيد فلاتر بناء الأجزاء المتأثرة من شجرة العناصر بكفاءة.
إعادة التحميل السريع (Hot Reload) وإعادة التشغيل السريع (Hot Restart)
- إعادة التحميل السريع تحقن الكود المحدَّث في آلة Dart الافتراضية أثناء تشغيلها مع الحفاظ على حالة التطبيق الحالية. وهي سريعة جدًا ومثالية لتكرار تصميم الواجهة.
- إعادة التشغيل السريع تعيد تشغيل آلة Dart الافتراضية وتفقد حالة التطبيق. استخدمها عند تغيير ()main أو المتغيرات العامة.
نظام العرض (Rendering)
يرسم فلاتر كل بكسل بنفسه باستخدام محرك العرض الخاص به. لا يعتمد على عناصر واجهة نظام التشغيل الأصلية (OEM widgets). لهذا السبب تبدو واجهات فلاتر متسقة عبر المنصات، ولهذا السبب يكون التخصيص قويًا جدًا.
مزايا فلاتر
- قاعدة كود واحدة لستّ منصات
- أداء ممتاز (الترجمة المسبقة AOT)
- واجهة مستخدم معبّرة ومرنة للغاية
- دورة تطوير سريعة (إعادة التحميل السريع)
- نظام بيئي متنامٍ ودعم قوي من جوجل
القيود
- قد يكون حجم التطبيق أكبر من التطبيقات الأصلية الخالصة
- بعض الميزات الخاصة بالمنصة لا تزال تتطلب قنوات منصة أو إضافات
- دعم الويب، رغم كونه جاهزًا للإنتاج، لا يزال يحتوي على اختلافات عن الجوال
مقارنة فلاتر بتقنيات أخرى
تمرين تطبيقي
اشرح بأسلوبك الخاص لماذا تُبسّط فلسفة “كل شيء عنصر” في فلاتر عملية بناء واجهة المستخدم.
المستوى 2 — تثبيت وإعداد فلاتر
الدرس 2.1 — تثبيت فلاتر على Windows و macOS و Linux
أهداف التعلّم
- تثبيت حزمة تطوير فلاتر (SDK) بشكل صحيح على نظام التشغيل لديك
- إعداد متغيّر البيئة PATH
- التحقق من التثبيت باستخدام Flutter Doctor
خطوات التثبيت التفصيلية (مفاهيمية)
Windows
- نزّل أحدث حزمة فلاتر المستقرة بصيغة zip.
- استخرجها إلى موقع خالٍ من المسافات والرموز الخاصة (مثال: C:\src\flutter).
- أضف مجلد flutter\bin إلى متغيّر PATH في نظامك.
- افتح موجّه أوامر جديدًا وشغّل flutter doctor.
macOS
- نزّل الحزمة.
- استخرجها (عادةً إلى ~/development/flutter).
- أضف مجلد bin إلى ملف تعريف الصدفة لديك (.zshrc أو .bash_profile).
- شغّل flutter doctor.
Linux
- نزّل الحزمة واستخرجها.
- أضف مسار bin إلى ~/.bashrc أو ~/.zshrc.
- ثبّت التبعيات المطلوبة للنظام التي يذكرها flutter doctor.
الأوامر الأساسية ومعانيها
flutter doctor
يتحقق من بيئتك ويُبلغ عن التبعيات المفقودة (سلسلة أدوات أندرويد، Xcode، إضافات VS Code، إلخ).
flutter --version
يعرض إصدارَي فلاتر و Dart المثبَّتين حاليًا.
flutter create my_app
ينشئ مشروع فلاتر جديدًا بهيكل المجلدات القياسي.
flutter run
يبني التطبيق ويشغّله على جهاز متصل أو محاكي.
إعداد Android Studio / VS Code
- ثبّت Android Studio وحزمة تطوير أندرويد (SDK).
- أنشئ جهازًا افتراضيًا واحدًا على الأقل (محاكي).
- في VS Code، ثبّت الإضافتين الرسميتين Flutter و Dart.
- على macOS، ثبّت Xcode من App Store وشغّل sudo xcode-select --switch /Applications/Xcode.app.
مشكلات التثبيت الشائعة وحلولها
- “مكوّن cmdline-tools مفقود” ← ثبّت Android SDK Command-line Tools عبر مدير SDK في Android Studio.
- “لم تُقبل تراخيص أندرويد” ← شغّل flutter doctor --android-licenses واقبل الجميع.
- PATH غير موجود ← أعد تشغيل الطرفية أو الحاسوب بعد تعديل PATH.
- مشكلات Xcode على macOS ← افتح Xcode مرة واحدة واقبل اتفاقية الترخيص.
تمرين تطبيقي
شغّل flutter doctor -v وفسّر كل سطر من المُخرَجات.
المستوى 3 — برمجة Dart من الصفر
الدرس 3.1 — المتغيرات والأنواع والأمان من القيم الفارغة (Null Safety)
أهداف التعلّم
- إعلان المتغيرات بشكل صحيح
- فهم نظام الأنواع في Dart
- استخدام الأمان من القيم الفارغة بثقة
الشرح
Dart لغة ذات كتابة (typing) قوية مع استدلال النوع (type inference). منذ إصدار Dart 2.12، يكون الأمان من القيم الفارغة مفعّلًا افتراضيًا.
الصياغة
// نوع صريح
String name = 'Flutter';
// استدلال النوع
var age = 25;
final pi = 3.14159; // ثابت وقت التشغيل
const gravity = 9.81; // ثابت وقت الترجمة
// أنواع قابلة لأن تكون فارغة (nullable)
String? nickname; // يمكن أن تكون null
String nonNullable = 'Hello'; // لا يمكن أن تكون null
المُعامِلات الواعية بالقيم الفارغة (Null-aware Operators)
String? maybeName;
print(maybeName ?? 'Guest'); // إذا كانت null، استخدم 'Guest'
print(maybeName?.length); // وصول آمن
maybeName ??= 'Anonymous'; // إسناد فقط إذا كانت null
الأخطاء الشائعة
- نسيان علامة ? عندما يمكن أن يكون المتغيّر فارغًا (null)
- استخدام ! (تأكيد عدم الفراغ) دون تحقّق، مما قد يسبب خطأً وقت التشغيل
أفضل الممارسات
- فضّل الأنواع غير القابلة للفراغ (non-nullable) كلما أمكن
- استخدم late فقط عندما تضمن التهيئة قبل أول قراءة
- فضّل final على var عندما لا تتغيّر القيمة
تمرين تطبيقي
أنشئ برنامجًا صغيرًا يعلن عن نص قابل لأن يكون فارغًا (nullable string)، ويطبع طوله بأمان، ويوفّر قيمة افتراضية.
الدرس 3.2 — التجميعات والدوال والتحكّم في التدفّق
المواضيع الأساسية المشمولة بشكل شامل
- القوائم (Lists)، المجموعات (Sets)، الخرائط (Maps)
- for، while، do-while، for-in، forEach
- if / else، تعبيرات switch (Dart 3)
- الدوال ذات المعاملات الموضعية والمسمّاة والاختيارية
- دوال السهم (Arrow functions)
- الدوال ذات الرتبة الأعلى (Higher-order functions)
مثال كامل — تصفية قائمة
void main() {
final numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
final evenNumbers = numbers.where((n) => n.isEven).toList();
final doubled = numbers.map((n) => n * 2).toList();
print(evenNumbers); // [2, 4, 6, 8, 10]
print(doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
}
يتم تدريس الأصناف (Classes) والوراثة (Inheritance) والخلطات (Mixins) والأنواع العامّة (Generics) عبر أمثلة تدريجية تقود إلى نماذج واقعية (User، Product، Order، إلخ).
البرمجة غير المتزامنة (Async)
تُغطّى الـ Futures و async/await والتدفقات (Streams) ومعالجة الأخطاء باستخدام try/catch عبر أمثلة عملية شبيهة بالشبكات قبل الانتقال إلى فلاتر.
المستوى 4 — أول تطبيق فلاتر لك
الدرس 4.1 — هيكل المشروع وتطبيق “Hello Flutter”
أهداف التعلّم
- فهم هيكل مشروع فلاتر القياسي
- إنشاء وتشغيل تطبيق أساسي
- تعلّم أدوار main() و runApp() و MaterialApp و Scaffold
هيكل المشروع (المجلدات الرئيسية)
- lib/ — يوجد هنا كل كود Dart المصدري. main.dart هو نقطة الدخول.
- android/, ios/, web/, windows/, macos/, linux/ — كود خاص بكل منصة.
- pubspec.yaml — التبعيات والأصول (assets).
- test/ — اختبارات الوحدة والواجهة.
تطبيق Hello Flutter الكامل
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hello Flutter',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Hello Flutter'),
centerTitle: true,
),
body: const Center(
child: Text(
'Welcome to Flutter Development',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w500),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
شرح الكود
- تقوم ()runApp بتضخيم العنصر المُعطى وإرفاقه بالشاشة.
- يوفّر MaterialApp تنسيق Material Design والتنقّل والترجمة (localization).
- ينفّذ Scaffold الهيكل البصري الأساسي لتصميم Material (شريط التطبيق، المحتوى، الزر العائم، الدرج، إلخ).
التطوير التدريجي
يضيف الطلاب بعد ذلك Drawer و BottomNavigationBar و SafeArea، محوّلين التطبيق البسيط إلى قالب بداية يبدو احترافيًا.
المستوى 5 — عناصر فلاتر (Widgets)
نظرة عامة على المستوى
في المستويات السابقة تعلّمت ما هو فلاتر، وكيفية تثبيته، وكيف تعمل Dart، وكيفية إنشاء تطبيق أساسي باستخدام MaterialApp و Scaffold. الآن ستتقن اللبنات الأساسية لكل واجهة فلاتر: العناصر (widgets).
كل ما تراه على الشاشة هو عنصر. فهم العناصر بعمق — كيفية تهيئتها، وكيفية تلقّيها للقيود (constraints)، وكيفية تركيبها، وكيفية تحويل فلاتر لها إلى بكسلات — هو أساس التطوير الاحترافي بفلاتر.
أهداف التعلّم
- استخدام العناصر الأساسية بشكل صحيح وتنسيقها باحترافية
- تركيب تخطيطات معقدة باستخدام Row و Column و Stack و Expanded و Flexible وما يتصل بها
- بناء قوائم وشبكات تمرير فعّالة باستخدام ListView و GridView و Slivers
- فهم العلاقة بين أشجار Widget و Element و RenderObject
- تشخيص مشكلات التخطيط والأداء الشائعة المتعلقة بالعناصر
- إنشاء مكوّنات واجهة مستخدم قابلة لإعادة الاستخدام ومنظّمة جيدًا
المتطلبات الأساسية
- الارتياح مع أساسيات Dart (المتغيرات، الدوال، الأصناف، الأمان من القيم الفارغة)
- القدرة على إنشاء وتشغيل تطبيق فلاتر بسيط
- فهم StatelessWidget وطريقة build و MaterialApp / Scaffold
الوحدة 1 — العناصر الأساسية
الدرس 5.1 — عنصر Text
ما هو؟
Text هو العنصر الأساسي الذي يعرض سلسلة نصية على الشاشة. وهو غير قابل للتغيير (immutable) وقابل للتهيئة بدرجة كبيرة عبر خصائص التنسيق.
لماذا هو مهم؟
تقريبًا كل شاشة تحتوي على نص. إتقان Text و TextStyle أساسي لواجهات قابلة للقراءة، ويسهل الوصول إليها، ومتّسقة مع الهوية البصرية.
كيف يعمل
يستقبل عنصر Text سلسلة نصية و TextStyle اختياري. أثناء مرحلة التخطيط (layout) يقيس فلاتر النص باستخدام مقاييس الخط الحالية والقيود المُعطاة من العنصر الأب. ثم يُرسم النص على اللوحة (canvas).
الصياغة والخصائص المهمة
Text(
String data, {
Key? key,
TextStyle? style,
TextAlign? textAlign,
TextDirection? textDirection,
bool softWrap = true,
TextOverflow? overflow,
int? maxLines,
...
})
الخصائص الأساسية:
- style ← يتحكم في حجم الخط، الوزن، اللون، تباعد الأحرف، الارتفاع، إلخ.
- textAlign ← يسار، وسط، يمين، ضبط (justify)
- overflow ← قص، تلاشي، نقاط حذف، مرئي
- maxLines ← يحدد عدد الأسطر
مثال أساسي
Text('Hello Flutter')
مثال برمجي كامل
import 'package:flutter/material.dart';
class TextDemo extends StatelessWidget {
const TextDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Text Widget')),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Primary Heading',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 12),
Text(
'This is a longer paragraph that demonstrates soft wrapping, '
'letter spacing, and line height control for better readability.',
style: TextStyle(
fontSize: 16,
height: 1.5,
letterSpacing: 0.3,
color: Colors.grey[800],
),
),
const SizedBox(height: 24),
const Text(
'This text will be truncated with an ellipsis if it is too long for the available space.',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 16),
),
],
),
),
);
}
}
شرح الكود
- يستخدم Text الأول تنسيقًا كبيرًا وعريضًا لعنوان.
- يوضّح Text الثاني تنسيق الفقرات باستخدام height (ارتفاع السطر) و letterSpacing.
- يوضّح Text الثالث maxLines و overflow: TextOverflow.ellipsis — نمط شائع لعناوين القوائم.
مثال من الواقع
في بطاقة منتج للتجارة الإلكترونية، يستخدم اسم المنتج maxLines: 2 مع نقاط حذف، ويستخدم السعر لونًا مميزًا عريضًا، ويستخدم الوصف رماديًا أفتح مع ارتفاع سطر محكوم.
الأخطاء الشائعة
- نسيان const عندما لا يتغيّر النص أبدًا (يسبب إعادة بناء غير ضرورية).
- استخدام أحجام خط صغيرة جدًا دون مراعاة إمكانية الوصول.
- الاعتماد فقط على اللون لنقل المعنى (مشكلة في إمكانية الوصول).
أفضل الممارسات
- فضّل Theme.of(context).textTheme على التنسيقات المُرمَّزة يدويًا (hard-coded).
- وفّر دائمًا maxLines + overflow مناسبَين للمحتوى الديناميكي.
- استخدم const Text(...) كلما أمكن.
تمارين تطبيقية
- أنشئ ثلاثة أنماط نصية: عنوان، نص أساسي، وتعليق توضيحي.
- اعرض جملة طويلة تُقتطع بعد سطرين.
- وسّط عنوانًا أفقيًا وعموديًا باستخدام خصائص Text فقط وعنصر أب.
تحدٍّ
ابنِ بطاقة “اقتباس اليوم” تعرض اقتباسًا بخط مائل، والمؤلف بخط عريض، وتتعامل مع الاقتباسات الطويلة بسلاسة.
مشروع مصغّر
أنشئ رأس ملف شخصي بسيط يعرض الاسم الكامل للمستخدم (كبير)، واسم المستخدم (رمادي)، ونبذة قصيرة تُقتطع بعد ثلاثة أسطر.
الدرس 5.2 — عنصر Icon
ما هو؟
يعرض Icon رمزًا رسوميًا من خط (Material Icons، Cupertino Icons، أو خطوط أيقونات مخصصة).
لماذا هو مهم؟
تنقل الأيقونات الإجراءات والحالة أسرع من النص وهي ضرورية لواجهات الجوال الحديثة.
كيف يعمل
تُعرَض الأيقونات من ملف خط. يربط فلاتر IconData (نقطة الترميز) بحرف رسومي (glyph) ويرسمه بالحجم واللون المطلوبَين.
الصياغة
Icon(
IconData icon, {
Key? key,
double? size,
Color? color,
String? semanticLabel,
TextDirection? textDirection,
})
مثال برمجي كامل
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
Icon(Icons.home, size: 32, color: Colors.blue),
Icon(Icons.favorite, size: 32, color: Colors.red),
Icon(Icons.settings, size: 32, color: Colors.grey),
Icon(Icons.notifications_active, size: 32, color: Colors.orange),
],
)
مثال من الواقع
تعتمد أشرطة التنقّل السفلية، وأزرار الإجراءات في أشرطة التطبيقات، ومؤشرات الحالة (متصل/غير متصل، البطارية، الواي فاي) بشكل كبير على عنصر Icon.
الأخطاء الشائعة
- استخدام الأيقونات دون semanticLabel (يضرّ بإمكانية الوصول).
- ترميز الألوان يدويًا بدلًا من استخدام IconTheme أو ColorScheme.
أفضل الممارسات
- وفّر دائمًا semanticLabel للأيقونات المهمة.
- فضّل أيقونات Material أو أنشئ مجموعة أيقونات مخصصة متسقة.
- اضبط حجم الأيقونات نسبةً إلى النص المحيط (عادةً 20–28 بكسل منطقي لإجراءات المحتوى).
تمارين تطبيقية
- أنشئ صفًا من أربع أيقونات إجراء بألوان مختلفة.
- أضف تلميحًا (tooltip) لكل أيقونة.
- ابنِ صفّ حالة بسيط (واي فاي، بطارية، إشارة) باستخدام الأيقونات.
الدرس 5.3 — عنصر Image
ما هو؟
يعرض Image صورة من الأصول (assets)، أو الشبكة، أو الذاكرة، أو الملف.
لماذا هو مهم؟
الصور محورية في التطبيقات الحديثة — صور المنتجات، الصور الرمزية، اللافتات، الرسوم التوضيحية.
كيف يعمل
يحمّل فلاتر بيانات الصورة، ويفكّها، ويرسمها وفقًا لقواعد BoxFit والمحاذاة. تُخزَّن صور الشبكة مؤقتًا (cache) افتراضيًا.
المُنشِئات المهمة
- Image.asset
- Image.network
- Image.file
- Image.memory
مثال برمجي كامل
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'https://picsum.photos/400/300',
width: double.infinity,
height: 200,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) {
return const Center(child: Icon(Icons.broken_image, size: 48));
},
),
)
شرح الكود
- يقوم ClipRRect بتدوير الزوايا.
- يملأ BoxFit.cover المساحة مع الحفاظ على نسبة العرض إلى الارتفاع.
- يوفّر loadingBuilder و errorBuilder حالتَي تحميل وخطأ احترافيتين.
الأخطاء الشائعة
- عدم التعامل مع حالتَي التحميل والخطأ.
- استخدام صور كبيرة جدًا دون استراتيجية تصغير أو تخزين مؤقت.
- نسيان الإعلان عن الأصول في pubspec.yaml.
أفضل الممارسات
- وفّر دائمًا واجهات بناء (builders) للتحميل والخطأ لصور الشبكة.
- استخدم حزمة cached_network_image لصور الشبكة في الإنتاج.
- فضّل صيغة WebP أو أصولًا بأحجام مناسبة.
تمارين تطبيقية
- اعرض صورة أصل محلية.
- اعرض صورة شبكة مع مؤشر تحميل وبديل عند الخطأ.
- أنشئ صورة رمزية دائرية باستخدام ClipOval + Image.
الدرس 5.4 — عنصر Container
ما هو؟
Container هو عنصر مساعد يجمع بين الرسم والتموضع والتحجيم. يمكنه تطبيق اللون، والزخرفة (decoration)، والحشو (padding)، والهامش (margin)، والقيود، والتحويلات.
لماذا هو مهم؟
هو أحد أكثر العناصر استخدامًا لإنشاء البطاقات والأزرار والشارات والأشكال المخصصة.
كيف يعمل
يطبّق Container أولًا الحشو، ثم القيود، ثم الزخرفة/اللون، وأخيرًا الهامش. إذا لم يُعطَ عنصرًا فرعيًا فإنه يحاول أن يكون بأكبر حجم ممكن (وفقًا للقيود).
الخصائص المهمة
- width, height
- padding, margin
- color أو decoration
- alignment
- constraints
- transform
مثال برمجي كامل — هيكل بطاقة منتج
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Wireless Headphones', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('\$129.99', style: TextStyle(fontSize: 16, color: Colors.green)),
],
),
)
الأخطاء الشائعة
- استخدام color و decoration معًا (يتعارضان).
- وضع هوامش كبيرة داخل قيود ضيّقة، مما يسبب تجاوز الحدود (overflow).
- الإفراط في استخدام Container عندما يكفي عنصر أبسط (SizedBox، Padding، ColoredBox).
أفضل الممارسات
- فضّل DecoratedBox أو Padding أو SizedBox أو ColoredBox عندما تحتاج ميزة واحدة فقط.
- استخدم BoxDecoration للحدود والتدرجات والظلال.
- حافظ على منطق الزخرفة متسقًا عبر التطبيق (أنشئ أنماط بطاقات قابلة لإعادة الاستخدام).
الدرس 5.5 — SizedBox و Padding و Center
SizedBox
يفرض على العنصر الفرعي عرضًا و/أو ارتفاعًا محددًا، أو ينشئ مساحة فارغة.
const SizedBox(height: 16)
const SizedBox(width: 8)
SizedBox(width: 120, height: 40, child: ElevatedButton(...))
Padding
يضيف مساحة فارغة حول عنصر فرعي.
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Text('Content'),
)
Center
يوسّط عنصره الفرعي داخل نفسه (أفقيًا وعموديًا).
const Center(child: Text('Centered'))
متى تستخدم أيًّا منها
- استخدم SizedBox للفراغات الثابتة أو العناصر ذات الحجم الثابت.
- استخدم Padding عندما تحتاج فقط إلى تباعد حول عنصر.
- استخدم Center عندما يكون الهدف الأساسي هو التوسيط.
خطأ شائع
تداخل عدة عناصر Padding بدلًا من دمج القيم في EdgeInsets واحد.
الدرس 5.6 — Card و Divider و Placeholder
Card
حاوية بتصميم Material مع ارتفاع (elevation) وزوايا مدوّرة. فضّل Card على Container العادي مع ظل عندما تريد تنسيق Material القياسي.
Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Card content'),
),
)
Divider
يرسم خطًا رفيعًا أفقيًا (أو عموديًا).
const Divider(height: 32, thickness: 1)
const VerticalDivider(width: 24, thickness: 1)
Placeholder
صندوق بسيط مفيد أثناء تطوير الواجهة قبل جاهزية المحتوى الحقيقي.
const Placeholder(fallbackHeight: 120)
الوحدة 2 — عناصر التخطيط (Layout Widgets)
الدرس 5.7 — Row و Column
ما هما؟
يرتّب Row العناصر الفرعية أفقيًا. يرتّب Column العناصر الفرعية عموديًا. هما العمود الفقري لمعظم تخطيطات فلاتر.
كيف تعمل القيود
يستقبل Row أو Column القيود من العنصر الأب ويمرّر قيودًا صارمة أو مرنة إلى عناصره الفرعية حسب mainAxisSize ووجود Expanded/Flexible.
الخصائص المهمة
- mainAxisAlignment
- crossAxisAlignment
- mainAxisSize
- children
مثال كامل — رأس ملف شخصي
Row(
children: [
const CircleAvatar(radius: 32, child: Icon(Icons.person)),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Sarah Johnson', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Text('@sarah_j', style: TextStyle(color: Colors.grey)),
],
),
),
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
],
)
الأخطاء الشائعة
- نسيان Expanded أو Flexible عندما يحتاج عنصر فرعي إلى أخذ المساحة المتبقية ← أخطاء تجاوز الحدود.
- تداخل عدد كبير جدًا من Row/Column دون فهم انتشار القيود.
أفضل الممارسات
- فضّل Expanded عندما يجب أن يملأ العنصر الفرعي كل المساحة المتبقية.
- فضّل Flexible عندما يمكن أن يكون العنصر الفرعي أصغر من المساحة المتبقية.
- حافظ على شجرة العناصر ضحلة (shallow) قدر الإمكان.
الدرس 5.8 — Expanded و Flexible
يفرض Expanded على عنصر فرعي في Row/Column/Flex ملء المساحة المتبقية على المحور الرئيسي. يسمح Flexible للعنصر الفرعي بملء المساحة المتبقية لكن لا يفرض ذلك.
Row(
children: [
Container(width: 60, height: 60, color: Colors.red),
Expanded(child: Container(height: 60, color: Colors.blue)),
Flexible(child: Container(width: 80, height: 60, color: Colors.green)),
],
)
قاعدة عامة
- استخدم Expanded لمناطق المحتوى المرن (القوائم، النصوص التي يجب أن تنمو).
- استخدم Flexible عندما تريد أن يحافظ العنصر الفرعي على حجمه الجوهري إن أمكن.
الدرس 5.9 — Stack و Positioned
يضع Stack العناصر الفرعية فوق بعضها البعض. يعطي Positioned (صالح فقط داخل Stack) قيمًا صريحة top/right/bottom/left.
مثال كامل — صورة مع تراكب نصّي
Stack(
children: [
Image.network('https://picsum.photos/400/250', fit: BoxFit.cover, width: double.infinity, height: 250),
Positioned(
bottom: 16,
left: 16,
right: 16,
child: Text(
'Beautiful Landscape',
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, shadows: [
Shadow(blurRadius: 8, color: Colors.black54),
]),
),
),
Positioned(
top: 12,
right: 12,
child: IconButton(
icon: const Icon(Icons.favorite_border, color: Colors.white),
onPressed: () {},
),
),
],
)
الأخطاء الشائعة
- استخدام Positioned خارج Stack.
- نسيان أن العناصر غير المُموضَعة (non-positioned) تُحاذى وفقًا لـ alignment (الافتراضي أعلى-بداية).
الدرس 5.10 — Wrap و Align و AspectRatio و FittedBox و ConstrainedBox
- Wrap — يشبه Row لكنه ينتقل إلى سطر جديد عند نفاد المساحة (الوسوم، عوامل التصفية).
- Align — يموضع عنصرًا فرعيًا داخل نفسه باستخدام قيمة Alignment.
- AspectRatio — يفرض على عنصر فرعي الحفاظ على نسبة عرض إلى ارتفاع.
- FittedBox — يُحجّم ويموضع عنصره الفرعي داخل نفسه.
- ConstrainedBox — يفرض قيودًا إضافية على عنصره الفرعي.
تحلّ هذه العناصر مشكلات تخطيط محددة جدًا وهي ضرورية لواجهات مستخدم مصقولة.
الوحدة 3 — عناصر التمرير (Scrolling Widgets)
الدرس 5.11 — ListView و ListView.builder
ListView هو الطريقة الأكثر شيوعًا لعرض قائمة قابلة للتمرير من العناصر الفرعية. للقوائم الطويلة أو غير المحدودة، فضّل دائمًا مُنشئ builder:
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(items[index].title),
subtitle: Text(items[index].subtitle),
onTap: () {},
);
},
)
لماذا builder؟
يبني بكسل فقط العناصر المرئية (بالإضافة إلى هامش صغير)، وهو أمر بالغ الأهمية للأداء مع مئات أو آلاف العناصر.
يضيف ListView.separated فاصلًا بين العناصر — مثالي للفواصل (dividers).
الدرس 5.12 — GridView
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: products.length,
itemBuilder: (context, index) {
return ProductCard(product: products[index]);
},
)
خطأ شائع
استخدام GridView عادي مع قائمة كبيرة من العناصر الفرعية بدلًا من GridView.builder.
الدرس 5.13 — SingleChildScrollView والتمرير المتداخل
استخدم SingleChildScrollView عندما يكون لديك عنصر فرعي واحد قد يتجاوز الحدود (النماذج، الأعمدة الطويلة). تجنّب تداخل عناصر قابلة للتمرير تتمرّر في نفس الاتجاه دون معالجة خاصة (NestedScrollView أو استخدام دقيق لـ PrimaryScrollController).
الدرس 5.14 — CustomScrollView و Slivers
الـ Slivers هي البروتوكول منخفض المستوى للمناطق القابلة للتمرير. يتيح لك CustomScrollView تركيب عدة slivers:
- SliverAppBar (رأس قابل للطي)
- SliverList
- SliverGrid
- SliverToBoxAdapter
- SliverFillRemaining
مثال — رأس قابل للطي + قائمة
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('My App'),
background: Image.network('...', fit: BoxFit.cover),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item $index')),
childCount: 50,
),
),
],
)
يُستخدَم هذا النمط في العديد من التطبيقات الاحترافية (شاشات الملفات الشخصية، تفاصيل المنتجات، قارئات الأخبار).
الوحدة 4 — عمارة العرض في فلاتر (Rendering Architecture)
Widget ← Element ← RenderObject
شجرة Widget — كائنات تهيئة غير قابلة للتغيير. تنشئها في طريقة build. تصف كيف يجب أن تبدو الواجهة.
شجرة Element — كائنات قابلة للتغيير تحتفظ بتهيئة العنصر الحالية وتدير دورة حياته. تقرّر العناصر ما إذا كان يمكن تحديث عنصر موجود بعنصر جديد أو يجب استبداله.
شجرة RenderObject — الكائنات التي تنفّذ التخطيط والرسم. كل RenderObject يعرف حجمه وموضعه وكيفية رسم نفسه.
المراحل
- البناء (Build) — يتم إنشاء العناصر أو تحديثها.
- التخطيط (Layout) — القيود تتدفق للأسفل، الأحجام تتدفق للأعلى.
- الرسم (Paint) — ترسم RenderObjects في طبقات.
- التركيب (Compositing) — تُدمَج الطبقات وتُرسَل إلى وحدة معالجة الرسومات (GPU).
فهم هذا الأنبوب (pipeline) يساعدك على تشخيص سبب عدم ظهور عنصر، أو سبب خطأ التخطيط، أو سبب ضعف الأداء.
نصيحة عملية
عندما تستدعي setState، يضع فلاتر علامة على State المعني بأنه “قذر” (dirty). فقط الأشجار الفرعية “القذرة” تُعاد بناؤها. لهذا السبب يهم الحفاظ على الحالة منخفضة قدر الإمكان واستخدام عناصر const.
مشروع تطبيقي للمستوى 5 — شاشة عرض المنتجات
ابنِ شاشة عرض منتجات كاملة تتضمن:
- شريط تطبيق مخصص
- قائمة فئات أفقية (ListView بتمرير أفقي)
- شبكة منتجات متجاوبة (GridView.builder)
- كل بطاقة منتج تستخدم Container/Card و Image و Text و Icon وحشوًا مناسبًا
- محاكاة السحب للتحديث (pull-to-refresh)
- حالتَي فراغ وتحميل
يجمع هذا المشروع كل ما تم تدريسه في المستوى 5.
أسئلة مراجعة المستوى 5
- ما الفرق بين Expanded و Flexible؟
- لماذا يجب أن تفضّل ListView.builder على ListView للقوائم الطويلة؟
- ماذا يحدث إذا وضعت عنصر Positioned خارج Stack؟
- اشرح العلاقة بين Widget و Element و RenderObject في فقرة واحدة.
- متى تختار CustomScrollView على ListView بسيط؟
المستوى 6 — واجهة المستخدم والتصميم المتجاوب
نظرة عامة على المستوى
في المستوى 5 تعلّمت العناصر الفردية التي تشكّل اللبنات الأساسية لكل شاشة. في هذا المستوى ستتعلم كيفية تنسيق تلك العناصر بشكل متسق، وإنشاء أنظمة بصرية احترافية، وجعل الواجهات تتكيّف بسلاسة مع أحجام الشاشات المختلفة، والاتجاهات، والمنصات.
أهداف التعلّم
- إنشاء وتطبيق سمة (theme) كاملة بنظام Material 3 (فاتحة وداكنة)
- استخدام ColorScheme و TextTheme وسمات المكوّنات بشكل صحيح
- بناء أزرار وبطاقات ومربّعات حوار وأوراق سفلية (bottom sheets) وأشرطة إشعار (snack bars) متسقة
- التحكّم في التباعد والأشكال والارتفاع (elevation) بشكل منهجي
- جعل التخطيطات تستجيب لحجم الشاشة والاتجاه والمنصة
- استخدام MediaQuery و LayoutBuilder و OrientationBuilder بفعالية
- تصميم واجهات تعمل بشكل جيد على الهواتف والأجهزة اللوحية والأجهزة القابلة للطي وسطح المكتب والويب
الوحدة 1 — Material Design و Material 3
الدرس 6.1 — ما هو Material Design و Material 3؟
ما هو؟
Material Design هو نظام التصميم الخاص بجوجل. Material 3 (يُعرَف أيضًا بـ Material You) هو أحدث إصدار. يركّز على اللون الديناميكي، والأشكال الأكبر، وتحسين إمكانية الوصول، والتخصيص الشخصي.
كيف يعمل في فلاتر
ينفّذ فلاتر Material 3 عبر ThemeData و ColorScheme. عند ضبط useMaterial3: true، تتبنى العديد من العناصر تلقائيًا اللغة البصرية الجديدة.
إعداد سمة أساسي
MaterialApp(
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
home: const HomePage(),
)
شرح الكود
- يولّد ColorScheme.fromSeed لوحة ألوان متناسقة كاملة من لون بذرة واحد.
- يحدد theme و darkTheme الوضعين.
- يتبع themeMode: ThemeMode.system إعداد الجهاز.
الأخطاء الشائعة
- ضبط useMaterial3: true مع الاستمرار في استخدام خصائص ألوان Material 2 القديمة.
- ترميز الألوان يدويًا بدلًا من القراءة من Theme.of(context).colorScheme.
أفضل الممارسات
- ولّد دائمًا ColorScheme من بذرة (seed).
- فضّل الألوان الدلالية (primary، onPrimary، surface، onSurface، error، إلخ) على القيم اللونية الخام.
- حافظ على تزامن السمتين الفاتحة والداكنة باستخدام نفس البذرة.
تمرين تطبيقي
أنشئ MaterialApp يتبدّل بين السمتين الفاتحة والداكنة باستخدام مفتاح تبديل في شريط التطبيق.
الدرس 6.2 — ColorScheme بعمق
الخصائص الأساسية التي ستستخدمها يوميًا
- primary / onPrimary
- secondary / onSecondary
- surface / onSurface
- background / onBackground (استُبدلت الآن إلى حد كبير بـ surface)
- error / onError
- outline، shadow، scrim، إلخ.
كيفية الوصول إلى الألوان
final colorScheme = Theme.of(context).colorScheme;
Container(
color: colorScheme.primaryContainer,
child: Text(
'Hello',
style: TextStyle(color: colorScheme.onPrimaryContainer),
),
)
أفضل ممارسة
لا تُرمِّز أبدًا Colors.blue أو Color(0xFF...) يدويًا في العناصر عندما يوجد لون دلالي متاح. اسحب دائمًا من ColorScheme.
الدرس 6.3 — الطباعة و TextTheme
أدوار النص في Material 3
displayLarge ← displaySmall، headlineLarge ← headlineSmall، titleLarge ← titleSmall، bodyLarge ← bodySmall، labelLarge ← labelSmall
مثال كامل
Text(
'Welcome back',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
)
Text(
'Here is your dashboard summary for today.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
الخطوط المخصصة
- أضف ملفات الخط إلى مجلد assets/fonts.
- أعلن عنها في pubspec.yaml.
- طبّقها في ThemeData:
theme: ThemeData(
useMaterial3: true,
fontFamily: 'Inter',
textTheme: const TextTheme(
headlineMedium: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600),
bodyLarge: TextStyle(fontFamily: 'Inter'),
),
)
خطأ شائع
الإفراط في تجاوز (override) العديد من أنماط النص بتنسيقات مؤقتة بدلًا من توسيع السمة.
أفضل ممارسة
أنشئ مجموعة صغيرة من أنماط النص الدلالية وأعد استخدامها. استخدم copyWith فقط للتعديلات الطفيفة.
الوحدة 2 — مكوّنات واجهة المستخدم الشائعة
الدرس 6.4 — الأزرار
يوفّر فلاتر عدة أنواع أزرار من Material 3: ElevatedButton، FilledButton، FilledButton.tonal، OutlinedButton، TextButton، IconButton، FloatingActionButton.
مثال زر احترافي
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('Create New'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
)
أفضل الممارسات
- فضّل FilledButton للإجراء الأساسي في الشاشة.
- استخدم OutlinedButton أو TextButton للإجراءات الثانوية.
- حافظ على ارتفاع زر متسق (عادةً 40–48 بكسل منطقي).
- وفّر دائمًا onPressed بقيمة null عندما يجب أن يظهر الزر معطّلًا.
الدرس 6.5 — البطاقات، مربّعات الحوار، الأوراق السفلية، وأشرطة الإشعار
Card
Card(
elevation: 0,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Revenue', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text('\$24,500', style: Theme.of(context).textTheme.headlineSmall),
],
),
),
)
Dialog
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirm Delete'),
content: const Text('This action cannot be undone.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
],
),
);
Bottom Sheet
showModalBottomSheet(
context: context,
showDragHandle: true,
builder: (context) => Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Options', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 16),
ListTile(leading: const Icon(Icons.share), title: const Text('Share'), onTap: () {}),
ListTile(leading: const Icon(Icons.link), title: const Text('Copy Link'), onTap: () {}),
],
),
),
);
SnackBar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Item saved successfully'),
behavior: SnackBarBehavior.floating,
action: SnackBarAction(label: 'Undo', onPressed: () {}),
),
);
الأخطاء الشائعة
- استخدام ScaffoldMessenger الجذر بشكل غير صحيح عند وجود Scaffold متداخلة.
- جعل الأوراق السفلية طويلة جدًا دون جعلها قابلة للتمرير.
الدرس 6.6 — التباعد والأشكال والارتفاع
نظام التباعد — تستخدم التطبيقات الاحترافية مقياس تباعد متسقًا (عادةً مضاعفات 4 أو 8): 4، 8، 12، 16، 24، 32، 48، 64.
class AppSpacing {
static const double xs = 4;
static const double sm = 8;
static const double md = 16;
static const double lg = 24;
static const double xl = 32;
}
الأشكال — يشجّع Material 3 على نصف أقطار زوايا أكبر (12–28).
class AppShapes {
static final small = RoundedRectangleBorder(borderRadius: BorderRadius.circular(8));
static final medium = RoundedRectangleBorder(borderRadius: BorderRadius.circular(12));
static final large = RoundedRectangleBorder(borderRadius: BorderRadius.circular(16));
}
الوحدة 3 — التصميم المتجاوب والقابل للتكيف
الدرس 6.7 — MediaQuery
يعطيك MediaQuery معلومات عن الشاشة الحالية: الحجم، الاتجاه، الحشو (النتوءات، واجهة النظام)، عامل تحجيم النص، السطوع، إلخ.
final size = MediaQuery.sizeOf(context);
final padding = MediaQuery.paddingOf(context);
final orientation = MediaQuery.orientationOf(context);
final textScaler = MediaQuery.textScalerOf(context);
مثال — تجنّب النتوء (notch)
Padding(
padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top),
child: ...,
)
أو ببساطة لفّ المحتوى بـ SafeArea.
الدرس 6.8 — LayoutBuilder ونقاط التكسّر (Breakpoints)
يعطيك LayoutBuilder قيود العنصر الأب، وهو أكثر موثوقية من MediaQuery عندما لا يكون العنصر بملء الشاشة.
class Breakpoints {
static const double mobile = 600;
static const double tablet = 900;
static const double desktop = 1200;
}
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < Breakpoints.mobile) {
return const MobileLayout();
} else if (constraints.maxWidth < Breakpoints.tablet) {
return const TabletLayout();
} else {
return const DesktopLayout();
}
},
);
}
نمط من الواقع
- الجوال: تنقّل سفلي + عمود واحد
- الجهاز اللوحي: شريط تنقّل جانبي (NavigationRail) + عمودان
- سطح المكتب: شريط تنقّل جانبي أو درج جانبي + محتوى متعدد الأعمدة
الدرس 6.9 — OrientationBuilder
OrientationBuilder(
builder: (context, orientation) {
if (orientation == Orientation.portrait) {
return const PortraitLayout();
} else {
return const LandscapeLayout();
}
},
)
الدرس 6.10 — التنقّل القابل للتكيف والشبكات المتجاوبة
أنماط التنقّل
- الهاتف ← NavigationBar (سفلي)
- الجهاز اللوحي / سطح المكتب ← NavigationRail أو NavigationDrawer
مثال شبكة متجاوبة
GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 300,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 0.75,
),
itemBuilder: ...,
)
مشروع مصغّر للمستوى 6 — لوحة تحكّم متجاوبة
ابنِ لوحة تحكّم كاملة تتضمن:
- سمة Material 3 كاملة (فاتحة + داكنة) بلون بذرة للعلامة التجارية.
- شريط تطبيق مع مفتاح تبديل السمة.
- محتوى متجاوب (الجوال: قائمة عمودية + تنقّل سفلي؛ الجهاز اللوحي/سطح المكتب: NavigationRail + شبكة بطاقات متعددة الأعمدة).
- أربع بطاقات ملخّص على الأقل باستخدام تباعد ونمط طباعة وألوان ColorScheme متسقة.
- زر عائم يعرض ورقة سفلية منبثقة بإجراءات سريعة.
- تأكيد SnackBar عند تنفيذ إجراء.
- معالجة آمنة للنتوءات وواجهة النظام باستخدام SafeArea.
أسئلة مراجعة المستوى 6
- لماذا يجب أن تفضّل ColorScheme.fromSeed على تحديد كل لون يدويًا؟
- ما الفرق بين MediaQuery.sizeOf(context) والقيود المُعطاة من LayoutBuilder؟
- متى تختار NavigationRail بدلًا من NavigationBar؟
- اشرح لماذا يجعل ترميز الألوان والأنماط النصية يدويًا الوضع الداكن صعبًا.
- ما الطريقة الموصى بها للتعامل مع شريط الحالة ومنطقة النتوء؟
المستوى 7 — التنقّل والتوجيه (Navigation & Routing)
نظرة عامة على المستوى
تحتوي التطبيقات الحقيقية على شاشات كثيرة. يعلّمك هذا المستوى كيفية تعامل فلاتر مع التنقّل — من واجهة Navigator التوجيهية الكلاسيكية إلى التوجيه التعريفي الحديث.
أهداف التعلّم
- التنقّل بين الشاشات باستخدام Navigator
- تحديد واستخدام المسارات المسمّاة (named routes)
- تمرير معطيات إلى شاشة واستقبال نتائج منها
- تنفيذ التنقّل المتداخل
- بناء تنقّل سفلي مع الحفاظ على الحالة باستخدام IndexedStack
- حماية المسارات التي تتطلب تسجيل دخول المستخدم
- فهم الفرق بين التوجيه التوجيهي (imperative) والتعريفي (declarative)
- بناء تطبيق متعدد الشاشات كامل بتدفّق تنقّل نظيف
الوحدة 1 — مفاهيم التنقّل الأساسية
الدرس 7.1 — Navigator والتنقّل الأساسي
ما هو؟
Navigator هو عنصر فلاتر الذي يدير مكدسًا (stack) من المسارات (الشاشات). عندما تدفع (push) مسارًا جديدًا، يُضاف فوق المكدس. عندما تسحب (pop)، يُزال المسار العلوي وتظهر الشاشة السابقة مجددًا.
الصياغة الأساسية
// دفع شاشة جديدة
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SecondScreen(),
),
);
// سحب الشاشة الحالية
Navigator.of(context).pop();
مثال كامل — شاشتان
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: FilledButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DetailsScreen(),
),
);
},
child: const Text('Go to Details'),
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
const DetailsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details')),
body: Center(
child: FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Go Back'),
),
),
);
}
}
شرح الكود
- ينشئ MaterialPageRoute انتقالًا متكيّفًا مع المنصة (انزلاق على iOS، تلاشي/انزلاق على أندرويد).
- تعيد دالة builder العنصر الذي يجب عرضه لهذا المسار.
- تزيل ()pop المسار الحالي وتعود إلى السابق.
الأخطاء الشائعة
- استدعاء Navigator.pop(context) عندما لا يوجد شيء لسحبه (قد يسبب خطأً).
- استخدام BuildContext بعد فجوة غير متزامنة (async gap) دون التحقق من mounted.
أفضل الممارسات
- فضّل Navigator.of(context).push على الصيغة الأقصر Navigator.push(context, ...) من أجل الوضوح.
- تحقّق دائمًا من if (context.mounted) قبل استخدام السياق بعد await.
الدرس 7.2 — تمرير المعطيات إلى شاشة
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ProductDetailsScreen(productId: 42),
),
);
class ProductDetailsScreen extends StatelessWidget {
const ProductDetailsScreen({super.key, required this.productId});
final int productId;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product $productId')),
body: Center(child: Text('Showing product #$productId')),
);
}
}
الدرس 7.3 — إرجاع بيانات من شاشة
// من الشاشة الأولى
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
if (result != null && context.mounted) {
print('User selected: $result');
}
// داخل SelectionScreen
Navigator.of(context).pop('Selected Value');
خطأ شائع
نسيان تحديد النوع العام (push<String>) ثم مواجهة صعوبة مع الأنواع الديناميكية (dynamic).
الوحدة 2 — المسارات المسمّاة (Named Routes)
الدرس 7.4 — المسارات المسمّاة
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/details': (context) => const DetailsScreen(),
'/profile': (context) => const ProfileScreen(),
'/settings': (context) => const SettingsScreen(),
},
)
Navigator.of(context).pushNamed('/details');
Navigator.of(context).pushNamed('/profile');
Navigator.of(context).pop();
تمرير المعطيات مع المسارات المسمّاة
Navigator.of(context).pushNamed(
'/details',
arguments: 42,
);
// داخل DetailsScreen
final productId = ModalRoute.of(context)!.settings.arguments as int;
متى تصبح المسارات المسمّاة محدودة
تعمل المسارات المسمّاة بشكل جيد للتطبيقات البسيطة. عندما تحتاج إلى معطيات مضبوطة النوع بدقة، أو حراسة المسارات (route guards)، أو الروابط العميقة (deep linking)، أو التنقّل المتداخل، ينتقل معظم الفرق الاحترافية إلى حل تعريفي مثل go_router.
الوحدة 3 — التنقّل السفلي والحفاظ على الحالة
الدرس 7.5 — BottomNavigationBar + IndexedStack
المشكلة
إذا استبدلت ببساطة محتوى Scaffold عند نقر المستخدم على عنصر تنقّل سفلي، تُفقد حالة كل تبويب (موضع التمرير، بيانات النموذج، إلخ).
الحل الاحترافي
أبقِ كل شاشات التبويبات حيّة وغيّر فقط أيها ظاهر باستخدام IndexedStack.
class MainShell extends StatefulWidget {
const MainShell({super.key});
@override
State<MainShell> createState() => _MainShellState();
}
class _MainShellState extends State<MainShell> {
int _currentIndex = 0;
final List<Widget> _pages = const [
HomeTab(),
SearchTab(),
OrdersTab(),
ProfileTab(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() => _currentIndex = index);
},
destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.receipt_long_outlined), selectedIcon: Icon(Icons.receipt_long), label: 'Orders'),
NavigationDestination(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
لماذا IndexedStack؟
تبقى كل العناصر الفرعية في شجرة العناصر. فقط الرؤية تتغيّر. تُحفَظ مواضع التمرير والحالة الداخلية.
خطأ شائع
إعادة بناء قائمة الصفحات بالكامل عند كل تغيير تبويب بدلًا من إبقائها ثابتة.
الوحدة 4 — التنقّل المتداخل والمسارات المحمية
الدرس 7.6 — التنقّل المتداخل
يمكن أن يمتلك كل تبويب مكدس تنقّل خاصًا به. هذا شائع في التطبيقات حيث يمكن لتبويب الرئيسية دفع شاشات تفاصيل بينما يمتلك تبويب الملف الشخصي مكدسه الخاص. تحقّق ذلك بإعطاء كل تبويب Navigator خاصًا به (أو باستخدام موجّه تعريفي يدعم المسارات المتداخلة).
الدرس 7.7 — المسارات المحمية (حارس المصادقة)
المفهوم
يجب أن تكون بعض الشاشات متاحة فقط عندما يكون المستخدم مسجّلًا دخوله.
void openProfile(BuildContext context, bool isLoggedIn) {
if (isLoggedIn) {
Navigator.of(context).pushNamed('/profile');
} else {
Navigator.of(context).pushNamed('/login');
}
}
نهج أفضل
مركزة القرار في طبقة التوجيه الخاصة بك بحيث لا تحتاج الأزرار الفردية إلى معرفة منطق المصادقة.
الوحدة 5 — التوجيه التعريفي الحديث (مفاهيم go_router)
لماذا التوجيه التعريفي؟
يصبح التنقّل التوجيهي (push، pop) صعب الصيانة في التطبيقات الكبيرة. يصف التوجيه التعريفي حالة التنقّل بأكملها كدالة لحالة التطبيق. تصبح الروابط العميقة، ودعم الويب، وإعادة توجيه المصادقة أنظف بكثير.
الأفكار الأساسية
- تُعرَّف المسارات في مكان واحد.
- يمكن تنفيذ إعادة التوجيه بناءً على حالة المصادقة.
- معطيات المسار (/product/:id) عناصر من الدرجة الأولى.
- يُدعَم التنقّل المتداخل بشكل طبيعي.
مشروع مصغّر للمستوى 7 — تطبيق متعدد الشاشات كامل
ابنِ تطبيقًا بالشاشات والتدفّقات التالية:
- شاشة البداية (Splash) — تظهر لثانيتين، ثم تعيد التوجيه إلى تسجيل الدخول أو الرئيسية حسب حالة المصادقة.
- شاشة تسجيل الدخول — حقول البريد الإلكتروني وكلمة المرور. عند النجاح ← الرئيسية. رابط للتسجيل.
- شاشة التسجيل — الاسم، البريد الإلكتروني، كلمة المرور. عند النجاح ← الرئيسية.
- الهيكل الرئيسي (بعد تسجيل الدخول) — تنقّل سفلي بأربعة تبويبات: الرئيسية، البحث، الطلبات، الملف الشخصي. استخدم IndexedStack للحفاظ على الحالة.
- تبويب الرئيسية — قائمة عناصر. النقر على عنصر يدفع شاشة تفاصيل (تنقّل متداخل).
- تبويب الملف الشخصي — يعرض معلومات المستخدم + زر تسجيل خروج. تسجيل الخروج يمسح الجلسة ويعود إلى تسجيل الدخول (مع مسح مكدس التنقّل).
- شاشة الإعدادات — يمكن الوصول إليها من الملف الشخصي.
المتطلبات
- استخدم مسارات مسمّاة أو هيكل Navigator نظيف.
- مرّر معطى واحدًا على الأقل بين الشاشات.
- أرجع نتيجة من شاشة واحدة.
- احمِ الهيكل الرئيسي بحيث لا يمكن فتحه دون “تسجيل دخول”.
- حافظ على حالة التبويبات باستخدام IndexedStack.
- تعامل مع زر الرجوع بشكل صحيح على أندرويد.
أسئلة مراجعة المستوى 7
- ما الفرق بين push و pushReplacement؟
- لماذا يُفضَّل IndexedStack على مجرد استبدال العناصر للتنقّل السفلي؟
- كيف تُرجع بيانات بأمان من شاشة فُتحت باستخدام push؟
- ما المشكلة التي تحلّها المسارات المحمية؟
- متى تبدأ واجهة Navigator الكلاسيكية بالشعور بأنها محدودة؟
المستوى 8 — إدخال المستخدم والنماذج
نظرة عامة على المستوى
يعلّمك هذا المستوى كيفية بناء نماذج احترافية في فلاتر: إدخال النص المُتحكَّم فيه، والتحقق من الصحة (validation)، وإدارة التركيز (focus)، وأنواع إدخال مختلفة، واختيار التاريخ والوقت، وكيفية توفير ملاحظات واضحة أثناء إرسال النموذج.
أهداف التعلّم
- استخدام TextField و TextFormField بشكل صحيح
- إدارة النص باستخدام TextEditingController
- التحكّم في التركيز باستخدام FocusNode
- بناء نماذج بالتحقق من الصحة باستخدام Form و FormState
- إنشاء أدوات تحقق (validators) مخصصة
- استخدام القوائم المنسدلة، وخانات الاختيار، والأزرار الدائرية، والمفاتيح
- عرض منتقيات التاريخ والوقت
- التعامل مع حالتَي التحميل والخطأ أثناء إرسال النموذج
الوحدة 1 — أساسيات إدخال النص
الدرس 8.1 — TextField و TextFormField
ما هو؟
TextField هو إدخال نصي أساسي بتصميم Material. TextFormField هو نفس الإدخال لكنه مصمَّم للعمل داخل Form. يدعم التحقق من الصحة ويتكامل مع FormState.
الصياغة الأساسية
TextField(
decoration: InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
)
المُفضَّل للنماذج — TextFormField
TextFormField(
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
if (!value.contains('@')) {
return 'Enter a valid email';
}
return null;
},
)
الأخطاء الشائعة
- استخدام TextField داخل Form ثم محاولة التحقق منه (لن يشارك في التحقق الخاص بالنموذج).
- نسيان التخلّص (dispose) من المُتحكِّمات (تسريب ذاكرة).
أفضل الممارسات
- فضّل TextFormField متى ما كان الحقل ينتمي إلى نموذج.
- وفّر دائمًا labelText أو hintText واضحين.
- استخدم keyboardType و textInputAction المناسبين.
الدرس 8.2 — TextEditingController
مثال كامل
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
),
],
);
}
}
قواعد مهمة
- أنشئ المُتحكِّم في State.
- استدعِ دائمًا ()dispose على كل مُتحكِّم.
- لا تنشئ مُتحكِّمًا أبدًا داخل طريقة build.
الدرس 8.3 — FocusNode ومعالجة لوحة المفاتيح
مثال — نقل التركيز
final _emailFocus = FocusNode();
final _passwordFocus = FocusNode();
TextFormField(
focusNode: _emailFocus,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
_passwordFocus.requestFocus();
},
)
إخفاء لوحة المفاتيح
GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(...),
)
أفضل ممارسة
تخلّص من كل FocusNode تنشئه، تمامًا مثل المُتحكِّمات.
الوحدة 2 — النماذج والتحقق من الصحة
الدرس 8.4 — Form و FormState
نمط كامل
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
// محاكاة طلب شبكة
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Registration successful')),
);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Full Name'),
validator: (value) =>
value == null || value.trim().isEmpty ? 'Name is required' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) return 'Email is required';
if (!value.contains('@')) return 'Enter a valid email';
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value == null || value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
},
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _isLoading ? null : _submit,
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Create Account'),
),
),
],
),
);
}
}
شرح الكود
- يعطيك GlobalKey<FormState> وصولًا إلى ()validate و ()save.
- تشغّل ()validate أداة التحقق (validator) الخاصة بكل حقل.
- يُعطَّل الزر ويعرض مؤشر تحميل أثناء تنفيذ الطلب.
- يُتحقَّق من mounted بعد الفجوة غير المتزامنة.
الأخطاء الشائعة
- إنشاء GlobalKey داخل build.
- نسيان تعطيل الزر أثناء التحميل (يمكن للمستخدم الإرسال عدة مرات).
- عدم التحقق من mounted بعد await.
الدرس 8.5 — أدوات تحقق مخصصة وقابلة لإعادة الاستخدام
String? requiredValidator(String? value) {
if (value == null || value.trim().isEmpty) return 'This field is required';
return null;
}
String? emailValidator(String? value) {
if (value == null || value.isEmpty) return 'Email is required';
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) return 'Enter a valid email address';
return null;
}
String? passwordValidator(String? value) {
if (value == null || value.length < 8) {
return 'Password must be at least 8 characters';
}
return null;
}
الوحدة 3 — عناصر تحكم النماذج الأخرى الشائعة
الدرس 8.6 — القائمة المنسدلة، خانة الاختيار، الزر الدائري، المفتاح
DropdownButtonFormField
String? _selectedRole;
DropdownButtonFormField<String>(
value: _selectedRole,
decoration: const InputDecoration(labelText: 'Role'),
items: const [
DropdownMenuItem(value: 'user', child: Text('User')),
DropdownMenuItem(value: 'editor', child: Text('Editor')),
DropdownMenuItem(value: 'admin', child: Text('Admin')),
],
onChanged: (value) => setState(() => _selectedRole = value),
validator: (value) => value == null ? 'Please select a role' : null,
)
Checkbox
bool _agreeToTerms = false;
CheckboxListTile(
title: const Text('I agree to the Terms of Service'),
value: _agreeToTerms,
onChanged: (value) => setState(() => _agreeToTerms = value ?? false),
controlAffinity: ListTileControlAffinity.leading,
)
Switch
SwitchListTile(
title: const Text('Enable notifications'),
value: _notificationsEnabled,
onChanged: (value) => setState(() => _notificationsEnabled = value),
)
الدرس 8.7 — منتقيات التاريخ والوقت
منتقي التاريخ
DateTime? _selectedDate;
Future<void> _pickDate() async {
final now = DateTime.now();
final picked = await showDatePicker(
context: context,
initialDate: _selectedDate ?? now,
firstDate: DateTime(1900),
lastDate: now,
);
if (picked != null) {
setState(() => _selectedDate = picked);
}
}
منتقي الوقت
TimeOfDay? _selectedTime;
Future<void> _pickTime() async {
final picked = await showTimePicker(
context: context,
initialTime: _selectedTime ?? TimeOfDay.now(),
);
if (picked != null) {
setState(() => _selectedTime = picked);
}
}
مشروع مصغّر للمستوى 8 — نظام تسجيل ودخول احترافي
1. شاشة تسجيل الدخول
- حقول البريد الإلكتروني وكلمة المرور مع التحقق من الصحة
- زر نصي “نسيت كلمة المرور؟”
- حالة تحميل على زر الدخول
- رابط لشاشة التسجيل
- منطقة رسالة خطأ (لبيانات الاعتماد غير الصحيحة)
2. شاشة التسجيل
- الاسم الكامل، البريد الإلكتروني، كلمة المرور، تأكيد كلمة المرور
- مفتاح إظهار/إخفاء كلمة المرور
- خانة اختيار الشروط والأحكام (يجب تحديدها)
- تحقق قوي من الصحة (صيغة البريد، طول كلمة المرور، تطابق كلمتَي المرور)
- حالة تحميل
- عند النجاح: انتقل إلى الرئيسية وأزل شاشات المصادقة من المكدس
أسئلة مراجعة المستوى 8
- لماذا يجب أن تتخلّص دائمًا من TextEditingController و FocusNode؟
- ما الفرق بين TextField و TextFormField؟
- كيف تقرر FormState.validate() ما إذا كان النموذج صالحًا؟
- لماذا من المهم تعطيل زر الإرسال أثناء تنفيذ الطلب؟
- كيف تنقل التركيز من حقل إلى التالي عندما يضغط المستخدم مفتاح “التالي”؟
المستوى 9 — إدارة الحالة (State Management)
نظرة عامة على المستوى
يعلّمك هذا المستوى إدارة الحالة من المبادئ الأولى. ستبدأ بالأدوات المدمجة (StatelessWidget، StatefulWidget، setState)، وتتعلم متى تصبح غير كافية، ثم تتقن الحلول الاحترافية الأكثر استخدامًا: Provider و Riverpod و BLoC/Cubit.
الوحدة 1 — إدارة الحالة المدمجة
الدرس 9.1 — StatelessWidget مقابل StatefulWidget
StatelessWidget — عنصر يعتمد فقط على تهيئته (معطيات المُنشئ) والعناصر الموروثة (inherited) المحيطة به. لا يمتلك حالة داخلية قابلة للتغيير.
StatefulWidget — عنصر يمكن أن يتغيّر بمرور الوقت. يُقسَّم إلى صنفين: العنصر نفسه (غير قابل للتغيير) وكائن State يحمل البيانات القابلة للتغيير وطريقة setState.
متى تستخدم كل واحد
- استخدم StatelessWidget كلما أمكن.
- استخدم StatefulWidget فقط عندما يحتاج العنصر إلى تحديث نفسه استجابة لتفاعل المستخدم أو أحداث داخلية.
الدرس 9.2 — setState ودورة حياة العنصر
كيف تعمل setState
استدعاء setState يضع علامة على كائن State بأنه “قذر” (dirty). يجدول فلاتر إعادة بناء ذلك العنصر وأحفاده.
مثال أساسي
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Text('Count: $_counter', style: Theme.of(context).textTheme.headlineMedium),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
}
طرق دورة الحياة المهمة
- initState — تُستدعى مرة واحدة عند إنشاء State. مثالية للمُتحكِّمات والمستمِعين والتحميل الأولي للبيانات.
- didChangeDependencies — تُستدعى عند تغيّر العناصر الموروثة.
- dispose — تُستدعى عند إزالة State. ألغِ دائمًا المؤقتات، وتخلّص من المُتحكِّمات، وأزل المستمِعين هنا.
الأخطاء الشائعة
- استدعاء setState بعد التخلّص من العنصر.
- وضع عمل ثقيل داخل setState.
- استخدام setState لبيانات تحتاجها عناصر بعيدة كثيرة (يؤدي إلى “prop drilling”).
أفضل ممارسة
حافظ على الحالة منخفضة قدر الإمكان في الشجرة. ارفعها فقط عندما تحتاج عدة عناصر شقيقة نفس البيانات.
الوحدة 2 — Provider و ChangeNotifier
الدرس 9.3 — أساسيات Provider
المكوّنات الأساسية
- ChangeNotifier — صنف يحمل الحالة ويُخطر المستمِعين عند تغيّر شيء ما.
- ChangeNotifierProvider — يجعل المُخطِر (notifier) متاحًا لشجرة العناصر.
- Consumer / context.watch / context.read — طرق للاستماع إلى الحالة أو قراءتها.
مثال كامل — عدّاد بسيط باستخدام Provider
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// في main.dart
ChangeNotifierProvider(
create: (_) => CounterNotifier(),
child: const MyApp(),
)
// في عنصر
final counter = context.watch<CounterNotifier>();
Text('Count: ${counter.count}')
// لاستدعاء طريقة دون إعادة بناء
context.read<CounterNotifier>().increment();
الأخطاء الشائعة
- استخدام context.watch داخل معالجات الأحداث (يسبب إعادة بناء غير ضرورية أو أخطاء).
- نسيان استدعاء ()notifyListeners.
- إنشاء المزوّد (provider) منخفضًا جدًا في الشجرة.
الوحدة 3 — Riverpod (النهج الحديث الموصى به)
الدرس 9.4 — لماذا Riverpod؟
- لا يتطلب BuildContext لقراءة المزوّدات
- آمن وقت الترجمة (compile-safe)
- دعم أفضل للحالة غير المتزامنة
- اختبار أسهل
- دعم عدة مزوّدات من نفس النوع
الدرس 9.5 — مزوّدات Riverpod الأساسية
- Provider — للقيم أو الخدمات غير القابلة للتغيير
- StateProvider — للحالة البسيطة القابلة للتغيير
- FutureProvider — للعمل غير المتزامن لمرة واحدة
- StreamProvider — للتدفقات (streams)
- Notifier / AsyncNotifier — للمنطق المعقّد (المفضَّل في الكود الحديث)
إعداد أساسي
final counterProvider = StateProvider<int>((ref) => 0);
class CounterView extends ConsumerWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
// للتحديث
ref.read(counterProvider.notifier).state++;
مثال Notifier حديث
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
مثال غير متزامن (تحميل البيانات)
final productsProvider = FutureProvider<List<Product>>((ref) async {
return await ref.watch(productRepositoryProvider).fetchProducts();
});
أفضل الممارسات مع Riverpod
- فضّل Notifier / AsyncNotifier على StateProvider لأي شيء يتجاوز الحالة البسيطة.
- احتفظ بمنطق العمل داخل المُخطِرات (notifiers)، وليس داخل العناصر.
- استخدم ref.watch فقط داخل طرق build. استخدم ref.read داخل معالجات الأحداث.
الوحدة 4 — BLoC و Cubit
الدرس 9.6 — Cubit (نسخة مبسّطة من BLoC)
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
BlocProvider(
create: (_) => CounterCubit(),
child: ...,
)
// الاستماع
BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('$count'),
)
الدرس 9.7 — BLoC الكامل (أحداث + حالات)
استخدم BLoC الكامل عندما تريد فصلًا واضحًا بين الأحداث (نوايا المستخدم) والحالات (تمثيل الواجهة)، أو عندما يكون المنطق معقدًا.
جدول مقارنة
التوصية
- التطبيقات الصغيرة أو النماذج الأولية ← setState + Provider
- معظم تطبيقات فلاتر الاحترافية اليوم ← Riverpod
- الفرق التي تستخدم BLoC بالفعل أو تحتاج بنية معمارية صارمة قائمة على الأحداث ← Cubit/BLoC
مشروع مصغّر للمستوى 9
ابنِ سلة تسوّق بسيطة باستخدام Riverpod:
- قائمة منتجات (FutureProvider)
- حالة السلة (Notifier)
- القدرة على إضافة/إزالة العناصر
- حساب السعر الإجمالي
- إجراء إفراغ السلة
- واجهة تستجيب لحالات التحميل والخطأ والبيانات
المستوى 10 — العمل مع واجهات برمجة التطبيقات (APIs)
نظرة عامة على المستوى
يعلّمك هذا المستوى كيفية العمل مع واجهات برمجة التطبيقات باحترافية في فلاتر: طلبات HTTP، JSON، نماذج Dart نظيفة، حالات التحميل والخطأ، المصادقة، التصفّح بالصفحات (pagination)، والبحث.
الوحدة 1 — أساسيات واجهات برمجة التطبيقات
الدرس 10.1 — ما هي واجهة برمجة التطبيقات (API)؟
ما هي؟
API تعني “واجهة برمجة التطبيقات”. في سياق تطوير الجوال، تعني عادةً واجهة ويب: مجموعة من العناوين (endpoints) التي يعرضها الخادم بحيث يمكن للعملاء (تطبيق فلاتر لديك) طلب البيانات أو إرسالها.
لماذا هي مهمة؟
بدون واجهات برمجة التطبيقات، سيحتاج كل تطبيق إلى قاعدة بيانات ومنطق أعمال خاص به يعمل فقط على الجهاز. تتيح واجهات برمجة التطبيقات لخادم خلفي واحد خدمة عملاء متعددين والحفاظ على البيانات مركزية وآمنة.
شرح تفصيلي
عندما يحتاج تطبيق فلاتر لديك إلى بيانات، فهو لا يفتح اتصالًا بقاعدة بيانات مباشرة. بدلًا من ذلك يرسل طلب HTTP إلى خادم، والذي يعالجه ويعيد استجابة (غالبًا بصيغة JSON دائمًا تقريبًا). ثم يحوّل تطبيقك تلك الاستجابة إلى كائنات Dart ويعرضها.
الأخطاء الشائعة
- الاعتقاد بأن تطبيق فلاتر يمكنه الوصول المباشر إلى قاعدة بيانات بعيدة (لا ينبغي ذلك).
- ترميز استجابات API يدويًا بدلًا من تعلّم استهلاك عناوين حقيقية.
أفضل الممارسات
- تعامل مع API كعقد. يجب أن يعتمد تطبيقك على الشكل الموثَّق للبيانات، وليس على تفاصيل تنفيذ الخادم.
- افترض دائمًا أن الشبكة يمكن أن تفشل.
الدرس 10.2 — عمارة العميل/الخادم
كيف تعمل
- ينشئ العميل طلب HTTP.
- يسافر الطلب عبر الشبكة إلى الخادم.
- يوثّق الخادم الطلب (إن لزم)، وينفّذ منطق الأعمال، وغالبًا يستعلم قاعدة بيانات.
- يرسل الخادم استجابة HTTP رجوعًا (رمز الحالة + المحتوى).
- يفحص العميل رمز الحالة ويحلّل المحتوى.
الدرس 10.3 — أساسيات HTTP
الأجزاء المهمة لطلب HTTP
- الطريقة (GET، POST، PUT، PATCH، DELETE، إلخ)
- العنوان (URL)
- الترويسات (headers) (بيانات وصفية مثل نوع المحتوى والمصادقة)
- المحتوى (body) (البيانات المرسَلة مع POST/PUT/PATCH)
الأجزاء المهمة لاستجابة HTTP
- رمز الحالة (200، 201، 400، 401، 404، 500، …)
- الترويسات
- المحتوى (غالبًا JSON)
الدرس 10.4 — REST وواجهات RESTful
المبادئ الأساسية
- تُعرَّف الموارد بعناوين URL (/users، /products/42)
- تُستخدم الطرق القياسية (GET للقراءة، POST للإنشاء، إلخ)
- الاتصال عديم الحالة (stateless)
- الاستجابات عادةً JSON
مثال تصميم مورد
- GET /products ← قائمة المنتجات
- GET /products/15 ← منتج واحد
- POST /products ← إنشاء منتج جديد
- PUT /products/15 ← استبدال المنتج 15
- PATCH /products/15 ← تحديث جزئي للمنتج 15
- DELETE /products/15 ← حذف المنتج 15
الوحدة 2 — تنفيذ طلبات HTTP في فلاتر
الدرس 10.5 — حزمة http
dependencies:
http: ^1.2.0
طلب GET أساسي
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> fetchProducts() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
print(data);
} else {
throw Exception('Failed to load data: ${response.statusCode}');
}
}
الدرس 10.6 — طلبات GET عمليًا
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
);
}
}
Future<List<Post>> fetchPosts() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('Failed to load posts');
}
}
الدرس 10.7 — طلبات POST
Future<Post> createPost(String title, String body) async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode({
'title': title,
'body': body,
'userId': 1,
}),
);
if (response.statusCode == 201) {
return Post.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create post');
}
}
الدرس 10.8 — PUT و PATCH و DELETE
// مثال DELETE
Future<void> deletePost(int id) async {
final response = await http.delete(
Uri.parse('https://jsonplaceholder.typicode.com/posts/$id'),
);
if (response.statusCode != 200 && response.statusCode != 204) {
throw Exception('Failed to delete post');
}
}
الدرس 10.9 — الترويسات، معطيات الاستعلام، ومعطيات المسار
final uri = Uri.https('api.example.com', '/products', {
'category': 'phones',
'page': '1',
'limit': '20',
});
الدرس 10.10 — رموز حالة HTTP
- 2xx ← نجاح (200 OK، 201 Created، 204 No Content)
- 4xx ← خطأ من العميل (400 Bad Request، 401 Unauthorized، 403 Forbidden، 404 Not Found)
- 5xx ← خطأ من الخادم (500 Internal Server Error، 503 Service Unavailable)
الوحدة 3 — JSON والنماذج
الدرس 10.11 — تسلسل وإلغاء تسلسل JSON
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
متى تنتقل إلى توليد الكود
عندما يكون لديك نماذج كثيرة أو كائنات متداخلة، يصبح fromJson اليدوي مملًا وعرضة للأخطاء. عندها أدخل json_serializable أو freezed.
الدرس 10.12 — معالجة الأخطاء وحالات التحميل
enum DataState { initial, loading, success, error }
class PostsState {
final DataState state;
final List<Post> posts;
final String? errorMessage;
PostsState({
this.state = DataState.initial,
this.posts = const [],
this.errorMessage,
});
}
مهلات الشبكة (Timeouts)
final response = await http
.get(uri)
.timeout(const Duration(seconds: 10));
الدرس 10.13 — المصادقة برموز Bearer
Future<List<Post>> fetchProtectedPosts(String token) async {
final response = await http.get(
Uri.parse('https://api.example.com/posts'),
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
},
);
}
الدرس 10.14 — التصفّح بالصفحات والتمرير اللانهائي
يعيد API صفحة من النتائج بالإضافة إلى بيانات وصفية (page، totalPages، nextPage). في فلاتر تحتفظ برقم الصفحة الحالية، وتُلحق العناصر الجديدة بالقائمة الموجودة، وتحمّل الصفحة التالية عندما يقترب المستخدم من الأسفل (ScrollController).
مشروع كامل للمستوى 10 — تطبيق أخبار / منشورات
ابنِ تطبيقًا كاملًا:
- يجلب قائمة منشورات من API عامة.
- يعرض مؤشر تحميل أثناء الجلب.
- يعرض المنشورات في ListView.
- يتعامل مع الأخطاء برسالة واضحة للمستخدم وزر إعادة محاولة.
- يدعم السحب للتحديث.
- يسمح بالنقر على منشور لفتح شاشة تفاصيل.
- ينفّذ البحث (التصفية من جانب العميل مقبولة للنسخة الأولى).
- يستخدم فصلًا نظيفًا: النموذج ← خدمة API ← المستودع (Repository) ← الواجهة.
أسئلة مراجعة المستوى 10
- ما الفرق بين PUT و PATCH؟
- لماذا يجب أن تتحقق دائمًا من response.statusCode قبل تحليل JSON؟
- ما الغرض من مُنشئ fromJson المصنعي (factory)؟
- كيف ترسل رمز Bearer مع طلب؟
- ما هي حالات الواجهة الأساسية الثلاث التي يجب التعامل معها عند تحميل البيانات من API؟
المستوى 11 — التخزين المحلي وقواعد البيانات
نظرة عامة على المستوى
يعلّمك هذا المستوى خيارات التخزين المحلي الرئيسية المتاحة في فلاتر، من التخزين البسيط بالمفتاح-القيمة إلى قواعد البيانات العلائقية الكاملة.
الوحدة 1 — التخزين المحلي البسيط
الدرس 11.1 — مفاهيم التخزين المحلي
الفئات الرئيسية
- التخزين بالمفتاح-القيمة (SharedPreferences، التخزين الآمن)
- قواعد البيانات العلائقية (SQLite / Drift)
- الحلول الشبيهة بـ NoSQL/المستندات (أقل شيوعًا في فلاتر الخالص)
- تخزين الملفات (للصور، ملفات PDF، إلخ)
أفضل ممارسة
اختر أبسط أداة تلبّي احتياجاتك. لا تستخدم قاعدة بيانات كاملة لعلامة منطقية واحدة.
الدرس 11.2 — SharedPreferences
متى تستخدمها
وضع السمة (فاتح/داكن)، تفضيل اللغة، علامة اكتمال الإعداد الأولي، آخر تبويب مختار، إعدادات بسيطة غير حساسة.
dependencies:
shared_preferences: ^2.2.0
import 'package:shared_preferences/shared_preferences.dart';
Future<void> saveThemeMode(bool isDark) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isDarkMode', isDark);
}
Future<bool> loadThemeMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('isDarkMode') ?? false;
}
الأخطاء الشائعة
- تخزين كائنات كبيرة أو بيانات حساسة (رموز، كلمات مرور).
- استدعاء getInstance() في كل قراءة بدلًا من تخزين النسخة عند الحاجة.
أفضل الممارسات
- أنشئ صنف غلاف (wrapper) صغير (مثل SettingsService) بحيث لا يعتمد باقي التطبيق مباشرة على SharedPreferences.
- استخدم أسماء مفاتيح واضحة ومتسقة.
الدرس 11.3 — flutter_secure_storage
متى تستخدمها
رموز المصادقة، رموز التحديث (refresh tokens)، مفاتيح API، أي بيانات مستخدم حساسة.
dependencies:
flutter_secure_storage: ^9.0.0
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
const storage = FlutterSecureStorage();
Future<void> saveToken(String token) async {
await storage.write(key: 'access_token', value: token);
}
Future<String?> readToken() async {
return await storage.read(key: 'access_token');
}
Future<void> deleteToken() async {
await storage.delete(key: 'access_token');
}
الوحدة 2 — SQLite و sqflite
الدرس 11.4 — قواعد البيانات العلائقية و SQLite
المفاهيم الأساسية
- الجدول (Table) ← مجموعة من الصفوف
- العمود (Column) ← حقل بنوع
- المفتاح الأساسي (Primary key) ← معرّف فريد (عادةً id INTEGER PRIMARY KEY AUTOINCREMENT)
- المفتاح الأجنبي (Foreign key) ← إشارة إلى جدول آخر
- الاستعلام (Query) ← عبارة SQL تقرأ أو تعدّل البيانات
الدرس 11.5 — استخدام sqflite
dependencies:
sqflite: ^2.3.0
path: ^1.8.0
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
Future<Database> openNotesDatabase() async {
final databasePath = await getDatabasesPath();
final path = join(databasePath, 'notes.db');
return openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE notes(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
category TEXT,
created_at TEXT NOT NULL,
is_deleted INTEGER DEFAULT 0
)
''');
},
);
}
الدرس 11.6 — عمليات CRUD مع sqflite
// إنشاء
Future<int> insertNote(Database db, Map<String, dynamic> note) async {
return await db.insert('notes', note);
}
// قراءة
Future<List<Map<String, dynamic>>> getNotes(Database db) async {
return await db.query(
'notes',
where: 'is_deleted = ?',
whereArgs: [0],
orderBy: 'created_at DESC',
);
}
// تحديث
Future<int> updateNote(Database db, int id, Map<String, dynamic> values) async {
return await db.update(
'notes',
values,
where: 'id = ?',
whereArgs: [id],
);
}
// حذف (حذف ناعم)
Future<int> softDeleteNote(Database db, int id) async {
return await db.update(
'notes',
{'is_deleted': 1},
where: 'id = ?',
whereArgs: [id],
);
}
أفضل ممارسة
فضّل الحذف الناعم (soft delete) عندما قد يرغب المستخدمون في استعادة البيانات. وفّر خيار حذف دائم منفصلًا.
الوحدة 3 — النهج الحديث مع Drift
الدرس 11.7 — مقدمة إلى Drift
Drift (كان يُسمى Moor سابقًا) هي مكتبة تخزين تفاعلية آمنة النوع مبنية فوق SQLite. تعرّف الجداول في Dart ويولّد Drift الكود المقابل.
class Notes extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 200)();
TextColumn get content => text()();
TextColumn get category => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
}
الدرس 11.8 — ترحيلات قاعدة البيانات (Migrations)
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await db.execute('ALTER TABLE notes ADD COLUMN color TEXT');
}
},
اختبر الترحيلات بعناية دائمًا. لا تدمّر بيانات المستخدم أبدًا.
الدرس 11.9 — مبادئ العمل دون اتصال أولًا (Offline-First)
الأفكار الأساسية
- اقرأ من التخزين المحلي أولًا (واجهة فورية).
- زامن مع الخادم عندما تتوفر الشبكة.
- ضع عمليات الكتابة المنفَّذة دون اتصال في قائمة انتظار.
- حلّ التعارضات (آخر كتابة تفوز last-write-wins أو استراتيجيات أكثر تطورًا).
مشروع كامل للمستوى 11 — تطبيق ملاحظات دون اتصال
ابنِ تطبيق ملاحظات دون اتصال يعمل بالكامل مع: الإنشاء، العرض، التعديل، الحذف الناعم، الاستعادة (سلة المهملات)، البحث، التصفية حسب الفئة، الاستمرارية، والعمل دون اتصال بالكامل.
أسئلة مراجعة المستوى 11
- متى يجب أن تستخدم SharedPreferences بدلًا من قاعدة بيانات؟
- لماذا يُفضَّل flutter_secure_storage للرموز؟
- ما هو الحذف الناعم ولماذا هو مفيد؟
- ما المشكلة التي تحلّها ترحيلات قاعدة البيانات؟
- اشرح الفكرة الأساسية لنهج العمل دون اتصال أولًا.
المستوى 12 — Firebase مع فلاتر
نظرة عامة على المستوى
Firebase هو منصّة “الخلفية كخدمة” (BaaS) من جوجل. يعلّمك هذا المستوى كيفية دمج Firebase في تطبيق فلاتر بشكل صحيح: إعداد المشروع، المصادقة، Cloud Firestore، التخزين، والإشعارات السحابية الأساسية.
الوحدة 1 — أساسيات Firebase والإعداد
الدرس 12.1 — ما هو Firebase؟
ما هو؟
Firebase هو منصّة طوّرتها جوجل توفّر مجموعة من الخدمات السحابية: Firebase Authentication، Cloud Firestore (قاعدة بيانات NoSQL)، Firebase Storage، Firebase Cloud Messaging، وFirebase Analytics/Crashlytics.
أفضل الممارسات
- اضبط دائمًا قواعد أمان مناسبة قبل إطلاق التطبيق.
- أبقِ الكود المتعلق بـ Firebase خلف مستودعات (repositories) بحيث يمكنك استبدال الخلفية لاحقًا إن لزم.
الدرس 12.2 — إنشاء مشروع Firebase وربطه بفلاتر
الخطوات (مستوى عالٍ)
- اذهب إلى Firebase Console وأنشئ مشروعًا جديدًا.
- سجّل تطبيق أندرويد (يجب أن يطابق اسم الحزمة تطبيق فلاتر لديك).
- سجّل تطبيق iOS (يجب أن يطابق معرّف الحزمة).
- نزّل ملفات التهيئة (google-services.json، GoogleService-Info.plist).
- ضعها في المواقع الصحيحة في مشروع فلاتر لديك.
- ثبّت أداة FlutterFire CLI وشغّل أمر التهيئة.
- أضف حزم FlutterFire المطلوبة إلى pubspec.yaml.
dependencies:
firebase_core: ^3.0.0
firebase_auth: ^5.0.0
cloud_firestore: ^5.0.0
firebase_storage: ^12.0.0
google_sign_in: ^6.2.0 # لتسجيل الدخول بجوجل
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
الأخطاء الشائعة
- نسيان استدعاء Firebase.initializeApp() قبل استخدام أي خدمة Firebase.
- عدم تطابق اسم الحزمة/معرّف الحزمة بين Firebase ومشروع فلاتر.
الوحدة 2 — مصادقة Firebase
الدرس 12.3 — المصادقة بالبريد الإلكتروني وكلمة المرور
// التسجيل
final credential = await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: email,
password: password,
);
// تسجيل الدخول
final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password,
);
// تسجيل الخروج
await FirebaseAuth.instance.signOut();
// المستخدم الحالي
final user = FirebaseAuth.instance.currentUser;
الاستماع لحالة المصادقة
StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return const HomeScreen();
}
return const LoginScreen();
},
)
شرح الكود
تُصدر authStateChanges() المستخدم الحالي كلما تغيّرت حالة المصادقة (تسجيل دخول، تسجيل خروج، تحديث الرمز). هذه هي الطريقة الموصى بها لحماية المسارات وتحديث الواجهة.
الدرس 12.4 — المصادقة بجوجل
final GoogleSignIn googleSignIn = GoogleSignIn();
Future<UserCredential?> signInWithGoogle() async {
final GoogleSignInAccount? googleUser = await googleSignIn.signIn();
if (googleUser == null) return null; // ألغى المستخدم العملية
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
return await FirebaseAuth.instance.signInWithCredential(credential);
}
الدرس 12.5 — إعادة تعيين كلمة المرور
await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
الوحدة 3 — Cloud Firestore
الدرس 12.6 — أساسيات Firestore
المفاهيم الأساسية
- المجموعة (Collection) ← تشبه الجدول (مثل users، posts)
- المستند (Document) ← يشبه الصف (له معرّف وحقول)
- الحقل (Field) ← زوج مفتاح-قيمة داخل مستند
- المجموعة الفرعية (Subcollection) ← مجموعة متداخلة داخل مستند
await FirebaseFirestore.instance.collection('notes').add({
'title': title,
'content': content,
'userId': currentUserId,
'createdAt': FieldValue.serverTimestamp(),
});
final doc = await FirebaseFirestore.instance.collection('notes').doc(noteId).get();
if (doc.exists) {
final data = doc.data()!;
}
final querySnapshot = await FirebaseFirestore.instance
.collection('notes')
.where('userId', isEqualTo: currentUserId)
.orderBy('createdAt', descending: true)
.get();
الدرس 12.7 — المستمِعون في الوقت الفعلي (Real-Time Listeners)
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('notes')
.where('userId', isEqualTo: currentUserId)
.orderBy('createdAt', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
final notes = snapshot.data!.docs;
return ListView.builder(
itemCount: notes.length,
itemBuilder: (context, index) {
final data = notes[index].data() as Map<String, dynamic>;
return ListTile(
title: Text(data['title'] ?? ''),
subtitle: Text(data['content'] ?? ''),
);
},
);
},
)
الدرس 12.8 — قواعد أمان Firestore (مقدمة)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /notes/{noteId} {
allow read, write: if request.auth != null && request.auth.uid == resource.data.userId;
}
}
}
أفضل ممارسة
لا تترك القواعد مفتوحة أبدًا (allow read, write: if true) في الإنتاج.
الوحدة 4 — Firebase Storage والإشعارات
الدرس 12.9 — Firebase Storage
final storageRef = FirebaseStorage.instance
.ref()
.child('user_uploads')
.child(userId)
.child('${DateTime.now().millisecondsSinceEpoch}.jpg');
final uploadTask = await storageRef.putFile(file);
final downloadUrl = await uploadTask.ref.getDownloadURL();
الدرس 12.10 — أساسيات Cloud Messaging
- أضف حزمة firebase_messaging.
- اطلب الإذن (خصوصًا على iOS).
- احصل على رمز جهاز FCM.
- أرسل الرمز إلى خادمك الخلفي أو خزّنه في Firestore.
- تعامل مع الرسائل في المقدمة والخلفية.
مشروع كامل للمستوى 12 — تطبيق ملاحظات/تطبيق اجتماعي مصغّر بـ Firebase
ابنِ تطبيقًا يتضمن تسجيلًا ودخولًا بالبريد الإلكتروني/كلمة المرور، والاستماع لحالة المصادقة، وعمليات CRUD في الوقت الفعلي للملاحظات المرتبطة بالمستخدم الحالي، ورفع صور اختياري، وحالتَي تحميل/خطأ مناسبتين، وتسجيل خروج.
أسئلة مراجعة المستوى 12
- لماذا يجب استدعاء Firebase.initializeApp() قبل استخدام أي خدمة Firebase؟
- ما ميزة استخدام authStateChanges() بدلًا من فحص currentUser مرة واحدة فقط؟
- ما الفرق بين .get() و .snapshots() في Firestore؟
- لماذا تُعد قواعد الأمان بالغة الأهمية عند استخدام Firestore؟
- متى تختار Firebase على واجهة REST API مخصصة؟
المستوى 13 — المصادقة والأمان
نظرة عامة على المستوى
يعلّمك هذا المستوى المفاهيم الأساسية للمصادقة والتفويض، والأنظمة القائمة على الرموز (tokens)، والتخزين الآمن، والمصادقة البيومترية، وتقنيات البرمجة الدفاعية العملية.
الوحدة 1 — مفاهيم المصادقة الأساسية
الدرس 13.1 — المصادقة مقابل التفويض
المصادقة (Authentication) تجيب عن “من أنت؟” — التحقق من الهوية. التفويض (Authorization) يجيب عن “بماذا يُسمح لك؟” — تحديد الصلاحيات.
خطأ شائع
فحص فقط if (currentUser != null) قبل السماح بعمليات حساسة.
أفضل ممارسة
تحقّق دائمًا من الهوية والصلاحيات معًا.
الدرس 13.2 — الجلسات، رموز الوصول، ورموز التحديث
رمز الوصول (Access Token) — بيانات اعتماد قصيرة الأجل تُرسَل مع كل طلب API. صالحة عادةً من 15 دقيقة إلى ساعة.
رمز التحديث (Refresh Token) — بيانات اعتماد أطول أجلًا تُستخدم فقط للحصول على رمز وصول جديد.
التدفّق النموذجي
- يسجّل المستخدم الدخول ← يعيد الخادم رمز وصول + رمز تحديث.
- يخزّن العميل الرمزين بأمان.
- يرسل العميل رمز الوصول في ترويسة Authorization مع كل طلب.
- عند انتهاء صلاحية رمز الوصول، يستخدم العميل رمز التحديث للحصول على رمز وصول جديد.
- إذا كان رمز التحديث غير صالح، يجب على المستخدم تسجيل الدخول مجددًا.
الدرس 13.3 — JWT (رموز JSON Web)
البنية: header.payload.signature. غالبًا ما يحتوي المحتوى (payload) على معرّف المستخدم، وقت انتهاء الصلاحية (exp)، وقت الإصدار (iat)، الأدوار أو الصلاحيات.
ملاحظات أمان مهمة
- لا تثق أبدًا بالمحتوى دون التحقق من التوقيع.
- لا تخزّن معلومات حساسة داخل JWT.
- تحقّق دائمًا من مطالبة انتهاء الصلاحية.
الوحدة 2 — التخزين الآمن والبيومترية
الدرس 13.4 — التخزين الآمن للرموز
لماذا ليس SharedPreferences؟
بيانات SharedPreferences ليست مشفَّرة افتراضيًا على جميع المنصات ويمكن استخراجها من جهاز مكسور الحماية (rooted/jailbroken) أو من النسخ الاحتياطية.
final storage = FlutterSecureStorage();
// حفظ
await storage.write(key: 'access_token', value: accessToken);
await storage.write(key: 'refresh_token', value: refreshToken);
// قراءة
final accessToken = await storage.read(key: 'access_token');
// حذف عند تسجيل الخروج
await storage.deleteAll();
الدرس 13.5 — المصادقة البيومترية
final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics;
final isDeviceSupported = await localAuth.isDeviceSupported();
if (canCheck || isDeviceSupported) {
final didAuthenticate = await localAuth.authenticate(
localizedReason: 'Please authenticate to continue',
options: const AuthenticationOptions(
biometricOnly: false,
stickyAuth: true,
),
);
}
الوحدة 3 — الاتصال الآمن ومعالجة الإدخال
الدرس 13.6 — HTTPS وأمان API
- استخدم دائمًا عناوين https://
- أرسل الرموز فقط في الترويسات، أبدًا في معطيات الاستعلام
- تحقّق من استجابات الخادم
- فكّر في تثبيت الشهادات (certificate pinning) للتطبيقات عالية الأمان
الدرس 13.7 — التحقق من صحة الإدخال وتعقيمه
يجب التعامل مع أي بيانات قادمة من المستخدم (نماذج، معطيات استعلام، روابط عميقة) كبيانات غير موثوقة. تحقّق من جانب العميل لتجربة المستخدم، وأعد التحقق والتعقيم دائمًا من جانب الخادم.
الوحدة 4 — الثغرات الشائعة في الجوال
الدرس 13.8 — المشكلات الأمنية المتكررة ودفاعاتها
- تخزين البيانات غير الآمن — استخدم التخزين الآمن؛ قلّل البيانات الحساسة على الجهاز.
- حماية غير كافية لطبقة النقل — فرض HTTPS في كل مكان.
- تحقق ضعيف من جانب الخادم — لا تعتمد أبدًا على التحقق من العميل فقط.
- مصادقة معطوبة — استخدم أنظمة مصادقة مُثبَتة واتبع أفضل ممارسات الرموز.
- مراجع كائنات مباشرة غير آمنة — تحقّق دائمًا على الخادم أن المستخدم الموثَّق يمتلك أو لديه صلاحية للمورد المطلوب.
- صلاحيات مفرطة — اطلب فقط الصلاحيات المطلوبة للميزة الحالية.
مشروع تطبيقي للمستوى 13 — تدفّق مصادقة آمن
ابنِ أو وسّع نظام مصادقة يوضّح ممارسات أمان احترافية بتحقق قوي من جانب العميل، ورموز مخزَّنة بأمان، واستعادة تلقائية للجلسة، وتسجيل خروج سليم، وفتح بصمة اختياري، ومسارات محمية، والتعامل مع الجلسات المنتهية.
أسئلة مراجعة المستوى 13
- ما الفرق بين المصادقة والتفويض؟
- لماذا يجب أن تكون رموز الوصول قصيرة الأجل؟
- لماذا لا يناسب SharedPreferences تخزين رموز التحديث؟
- ما الخطر الرئيسي للاعتماد فقط على التحقق من جانب العميل؟
- سمِّ ثلاث ثغرات أمان شائعة في الجوال ودفاعًا واحدًا لكل منها.
المستوى 14 — فلاتر المتقدم
نظرة عامة على المستوى
يغطّي هذا المستوى تحكّمًا أعمق في الإطار: دورة حياة العنصر، المفاتيح (keys)، الرسم المخصص، الرسوم المتحركة، الإيماءات، والعمليات المتوازية (isolates).
الوحدة 1 — دورة حياة العنصر و BuildContext
الدرس 14.1 — دورة حياة العنصر
الطرق الأساسية
- initState — تُستدعى مرة واحدة عند إدراج State في الشجرة.
- didChangeDependencies — تُستدعى عند تغيّر عنصر موروث.
- didUpdateWidget — تُستدعى عندما يعيد الأب بناء العنصر بتهيئة جديدة.
- build — تُستدعى كلما احتاج العنصر إلى العرض.
- deactivate / dispose — تُستدعى عند إزالة State.
مثال — إدارة صحيحة للمُتحكِّم
class TimerScreen extends StatefulWidget {
const TimerScreen({super.key});
@override
State<TimerScreen> createState() => _TimerScreenState();
}
class _TimerScreenState extends State<TimerScreen> {
late Timer _timer;
int _seconds = 0;
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted) {
setState(() => _seconds++);
}
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text('Seconds: $_seconds');
}
}
أفضل ممارسة
اقرن دائمًا كل مورد تنشئه (مُتحكِّمات، مؤقتات، اشتراكات تدفّق) بتنظيف في dispose. تحقّق من mounted قبل استدعاء setState بعد فجوة غير متزامنة.
الدرس 14.2 — قواعد BuildContext
القواعد المهمة
- لا تستخدم سياقًا عبر فجوات غير متزامنة دون التحقق من mounted.
- لا تستخدم سياق عنصر لم يُدرَج بعد بشكل كامل.
- سياق الأب مختلف عن سياق الابن.
onPressed: () async {
await someAsyncOperation();
if (!context.mounted) return;
Navigator.of(context).pop();
}
الوحدة 2 — المفاتيح (Keys)
الدرس 14.3 — لماذا توجد المفاتيح
الأنواع الشائعة
- ValueKey — تعتمد على قيمة بدائية (معرّف، نص، رقم)
- ObjectKey — تعتمد على هوية الكائن
- UniqueKey — تفرض هوية جديدة في كل مرة
- GlobalKey — تعطي مقبضًا عامًا (handle) لحالة عنصر أو سياقه
ListView(
children: items.map((item) {
return ListTile(
key: ValueKey(item.id),
title: Text(item.title),
);
}).toList(),
)
خطأ شائع
استخدام GlobalKey لكل شيء. GlobalKeys مكلفة ويجب استخدامها باقتصاد.
الوحدة 3 — InheritedWidget والعناصر المخصصة
الدرس 14.4 — InheritedWidget
class UserInherited extends InheritedWidget {
const UserInherited({
super.key,
required this.userName,
required super.child,
});
final String userName;
static UserInherited of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<UserInherited>();
assert(result != null, 'No UserInherited found in context');
return result!;
}
@override
bool updateShouldNotify(UserInherited oldWidget) {
return userName != oldWidget.userName;
}
}
الدرس 14.5 — بناء عناصر مخصصة قابلة لإعادة الاستخدام
استخرج أنماط الواجهة المتكررة إلى عناصر مركّزة وقابلة للتهيئة جيدة التسمية. فضّل التركيب على الوراثة العميقة.
الوحدة 4 — الرسم المخصص
الدرس 14.6 — CustomPainter و Canvas
class CirclePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
40,
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// الاستخدام
CustomPaint(
size: const Size(200, 200),
painter: CirclePainter(),
)
الوحدة 5 — الإيماءات
الدرس 14.7 — GestureDetector والإيماءات المتقدمة
GestureDetector(
onTap: () {},
onDoubleTap: () {},
onLongPress: () {},
onPanUpdate: (details) {
// details.delta يعطي الحركة
},
onScaleUpdate: (details) {
// details.scale للتكبير بالقرص
},
child: ...,
)
الوحدة 6 — الرسوم المتحركة
الدرس 14.8 — الرسوم المتحركة الضمنية
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: isExpanded ? 300 : 100,
height: 100,
color: isExpanded ? Colors.blue : Colors.grey,
)
الدرس 14.9 — الرسوم المتحركة الصريحة مع AnimationController
class FadeInDemo extends StatefulWidget {
const FadeInDemo({super.key});
@override
State<FadeInDemo> createState() => _FadeInDemoState();
}
class _FadeInDemoState extends State<FadeInDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeIn),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animation,
child: const FlutterLogo(size: 120),
);
}
}
الدرس 14.10 — الرسوم المتحركة Hero والانتقالات المخصصة بين الشاشات
// الشاشة الأولى
Hero(
tag: 'product-image-${product.id}',
child: Image.network(product.imageUrl),
)
// الشاشة الثانية
Hero(
tag: 'product-image-${product.id}',
child: Image.network(product.imageUrl),
)
الوحدة 7 — العمليات المتوازية (Isolates) والعمل الخلفي
الدرس 14.11 — Isolates
Future<List<Product>> parseProducts(String jsonString) async {
return await compute(_parse, jsonString);
}
List<Product> _parse(String jsonString) {
final list = jsonDecode(jsonString) as List;
return list.map((e) => Product.fromJson(e)).toList();
}
مشروع مصغّر للمستوى 14 — معرض بطاقات منتجات متحركة
ابنِ شاشة بشبكة من بطاقات المنتجات، ورسوم Hero متحركة إلى شاشة تفاصيل، ورسوم متحركة صريحة في شاشة التفاصيل، وشارة مرسومة مخصصة، وإيماءة سحب لإغلاق شاشة التفاصيل، وعملية ثقيلة واحدة على الأقل منفَّذة في عملية متوازية.
أسئلة مراجعة المستوى 14
- لماذا يجب إلغاء المؤقتات والتخلّص من المُتحكِّمات في dispose؟
- متى تستخدم ValueKey مقابل GlobalKey؟
- ما الميزة الرئيسية للرسوم المتحركة الضمنية على الصريحة؟
- لماذا نحتاج إلى Isolates في تطبيق فلاتر؟
- ما المشكلة التي يحلّها عنصر Hero؟
المستوى 15 — عمارة فلاتر (Architecture)
نظرة عامة على المستوى
يعلّمك هذا المستوى العمارة البرمجية الاحترافية المطبَّقة على فلاتر: MVC و MVVM والعمارة النظيفة (Clean Architecture) ومبادئ SOLID ونمط المستودع (Repository) وحقن التبعية (Dependency Injection) وهيكل المشروع القائم على الميزات.
الوحدة 1 — لماذا تهم العمارة
الدرس 15.1 — تكلفة الهيكل الضعيف
بدون عمارة، تعاني قواعد الكود من عناصر بمئات الأسطر، ومنطق مكرَّر، وصعوبة الاختبار، وخوف من التغيير، وصعوبة تأهيل الأعضاء الجدد.
الوحدة 2 — الأنماط المعمارية الشائعة
الدرس 15.2 — MVC و MVVM
MVC — النموذج (البيانات وقواعد الأعمال)، العرض (الواجهة)، المتحكِّم (ينسّق بين النموذج والعرض).
MVVM — النموذج (البيانات)، العرض (واجهة خالصة)، نموذج العرض (ViewModel) (يحمل حالة الواجهة ويعرض الطرق/الأوامر). يتناسب MVVM بشكل طبيعي مع فلاتر باستخدام Riverpod Notifiers أو Provider + ChangeNotifier أو ما شابه.
الدرس 15.3 — العمارة النظيفة (Clean Architecture)
الطبقات النموذجية في تطبيق فلاتر
- العرض (Presentation) — العناصر، الصفحات، إدارة الحالة. لا تعرف شيئًا عن مصادر البيانات.
- النطاق (Domain) — قواعد الأعمال والكيانات وحالات الاستخدام. Dart خالص. بدون استيرادات فلاتر.
- البيانات (Data) — المستودعات (التنفيذ)، مصادر البيانات (API، قاعدة بيانات، ذاكرة تخزين مؤقت)، DTOs والمحوّلات (mappers).
قاعدة التبعية: العرض ← النطاق ← البيانات. تقع طبقة النطاق في الوسط وتعرّف الواجهات (المستودعات المجرَّدة). تنفّذ طبقة البيانات تلك الواجهات.
الوحدة 3 — اللبنات الأساسية
الدرس 15.4 — نمط المستودع (Repository Pattern)
abstract class AuthRepository {
Future<User> login(String email, String password);
Future<void> logout();
Stream<User?> authStateChanges();
Future<User?> getCurrentUser();
}
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remote;
final AuthLocalDataSource local;
AuthRepositoryImpl({required this.remote, required this.local});
@override
Future<User> login(String email, String password) async {
final user = await remote.login(email, password);
await local.cacheUser(user);
return user;
}
}
الدرس 15.5 — حقن التبعية (Dependency Injection)
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepositoryImpl(
remote: ref.watch(authRemoteDataSourceProvider),
local: ref.watch(authLocalDataSourceProvider),
);
});
الدرس 15.6 — مبادئ SOLID في فلاتر
- مبدأ المسؤولية الواحدة (S) — يجب أن يكون للصنف سبب واحد للتغيير.
- مبدأ الفتح/الإغلاق (O) — مفتوح للتوسيع، مغلق للتعديل.
- مبدأ استبدال ليسكوف (L) — يجب أن تكون الأنواع الفرعية قابلة للاستبدال بأنواعها الأساسية.
- مبدأ فصل الواجهات (I) — فضّل الواجهات الصغيرة المركّزة.
- مبدأ عكس التبعية (D) — اعتمد على التجريدات، لا على التطبيقات الملموسة.
الوحدة 4 — هيكل المشروع
الدرس 15.7 — التنظيم القائم على الميزات (Feature-First)
lib/
├── core/
│ ├── error/
│ ├── network/
│ ├── theme/
│ ├── utils/
│ └── widgets/
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ ├── models/
│ │ │ └── repositories/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ ├── repositories/
│ │ │ └── usecases/
│ │ └── presentation/
│ │ ├── providers/
│ │ ├── screens/
│ │ └── widgets/
│ ├── home/
│ ├── profile/
│ └── settings/
├── shared/
└── main.dart
الدرس 15.8 — البنية الطبقية داخل الميزة
- يمكن للعرض استيراد النطاق
- يمكن للبيانات استيراد النطاق
- يجب ألا يستورد النطاق العرض أو البيانات
- يجب ألا يستورد العرض البيانات مباشرة
مشروع تطبيقي للمستوى 15 — إعادة هيكلة ميزة إلى عمارة نظيفة
خذ ميزة بسيطة وأعد هيكلتها: عرّف الكيانات وواجهات المستودعات في طبقة النطاق، ونفّذ المستودع في طبقة البيانات، وأنشئ حالات استخدام، واعرض الحالة عبر Notifier/Cubit/ViewModel، واربط كل شيء بحقن التبعية.
أسئلة مراجعة المستوى 15
- ما قاعدة التبعية الأساسية في العمارة النظيفة؟
- لماذا يجب ألا تحتوي طبقة النطاق على استيرادات فلاتر؟
- ما المشكلة التي يحلّها نمط المستودع؟
- لماذا يُفضَّل التنظيم القائم على الميزات على التنظيم التقني الخالص؟
- اذكر فائدة ملموسة واحدة لحقن التبعية من أجل الاختبار.
المستوى 16 — الاختبار (Testing)
نظرة عامة على المستوى
يعلّمك هذا المستوى الفئات الرئيسية الثلاث للاختبار في فلاتر: اختبارات الوحدة (unit)، واختبارات الواجهة (widget)، واختبارات التكامل (integration).
الوحدة 1 — أساسيات الاختبار
الدرس 16.1 — لماذا يهم الاختبار
هرم الاختبار
- اختبارات وحدة سريعة وكثيرة (منطق الأعمال، الدوال الخالصة، المستودعات)
- اختبارات واجهة أقل (مكوّنات الواجهة والشاشات)
- عدد صغير من اختبارات التكامل / من طرف إلى طرف (التدفقات الحرجة للمستخدم)
خطأ شائع
كتابة اختبارات من طرف إلى طرف فقط. فهي بطيئة وهشّة ومكلفة الصيانة.
الدرس 16.2 — تنظيم الاختبارات
test/
├── unit/
│ ├── models/
│ ├── repositories/
│ └── notifiers/
├── widget/
│ ├── screens/
│ └── widgets/
└── integration/
الوحدة 2 — اختبار الوحدة (Unit Testing)
الدرس 16.3 — كتابة أول اختبارات الوحدة
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/post.dart';
void main() {
group('Post', () {
test('fromJson creates a valid Post', () {
final json = {
'id': 1,
'title': 'Test Title',
'body': 'Test Body',
};
final post = Post.fromJson(json);
expect(post.id, 1);
expect(post.title, 'Test Title');
expect(post.body, 'Test Body');
});
});
}
الدرس 16.4 — اختبار المستودعات باستخدام المحاكاة (Mocks)
dev_dependencies:
mocktail: ^1.0.0
import 'package:mocktail/mocktail.dart';
import 'package:flutter_test/flutter_test.dart';
class MockAuthRemoteDataSource extends Mock implements AuthRemoteDataSource {}
void main() {
late AuthRepositoryImpl repository;
late MockAuthRemoteDataSource mockRemote;
setUp(() {
mockRemote = MockAuthRemoteDataSource();
repository = AuthRepositoryImpl(remote: mockRemote);
});
test('login returns user when remote succeeds', () async {
final user = User(id: '1', email: 'test@example.com');
when(() => mockRemote.login(any(), any())).thenAnswer((_) async => user);
final result = await repository.login('test@example.com', 'password');
expect(result, user);
verify(() => mockRemote.login('test@example.com', 'password')).called(1);
});
test('login throws when remote fails', () async {
when(() => mockRemote.login(any(), any()))
.thenThrow(Exception('Network error'));
expect(
() => repository.login('test@example.com', 'password'),
throwsException,
);
});
}
الدرس 16.5 — اختبار إدارة الحالة (Notifier / Cubit)
test('increment increases the count', () {
final container = ProviderContainer();
addTearDown(container.dispose);
final notifier = container.read(counterProvider.notifier);
expect(container.read(counterProvider), 0);
notifier.increment();
expect(container.read(counterProvider), 1);
});
blocTest<CounterCubit, int>(
'emits [1] when increment is called',
build: () => CounterCubit(),
act: (cubit) => cubit.increment(),
expect: () => [1],
);
الوحدة 3 — اختبار الواجهة (Widget Testing)
الدرس 16.6 — أساسيات اختبارات الواجهة
testWidgets('Counter increments when button is tapped', (tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterPage()),
);
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
pump مقابل pumpAndSettle
- تتقدّم pump بالإطار لمدة قصيرة.
- تكرر pumpAndSettle عملية pump حتى تنتهي كل الرسوم المتحركة والمهام الصغيرة.
الدرس 16.7 — اختبار الشاشات ذات التبعيات
await tester.pumpWidget(
ProviderScope(
overrides: [
postRepositoryProvider.overrideWithValue(mockRepository),
],
child: const MaterialApp(home: PostListScreen()),
),
);
الوحدة 4 — اختبار التكامل والتغطية
الدرس 16.8 — اختبارات التكامل
تعمل اختبارات التكامل على جهاز حقيقي أو محاكي وتفحص تدفقات مستخدم كاملة باستخدام حزمة integration_test.
الدرس 16.9 — تغطية الاختبار (Test Coverage)
flutter test --coverage
مشروع تطبيقي للمستوى 16 — اختبار ميزة كاملة
اختر ميزة بنيتها بالفعل وأضف اختبارات وحدة للنماذج، والمستودع (بمحاكاة)، والمُخطِر/Cubit (نجاح، تحميل، خطأ)، وما لا يقل عن اختبارَي واجهة للشاشة الرئيسية.
أسئلة مراجعة المستوى 16
- ما الفرق بين اختبار الوحدة واختبار الواجهة؟
- لماذا نحاكي (mock) التبعيات في اختبارات الوحدة؟
- متى تستخدم pump مقابل pumpAndSettle؟
- لماذا من المهم اختبار حالات الخطأ إلى جانب حالات النجاح؟
- أين يجب أن يقع معظم اختباراتك في هرم الاختبار؟
المستوى 17 — تحسين الأداء
نظرة عامة على المستوى
يعلّمك هذا المستوى كيفية قياس الأداء، وتحديد الاختناقات، وتطبيق تحسينات عملية.
الوحدة 1 — فهم الأداء في فلاتر
الدرس 17.1 — كيف يرسم فلاتر الإطارات
الأنبوب (Pipeline): البناء ← التخطيط ← الرسم ← التركيب. يجب أن يكتمل الإطار عادةً في أقل من 16 مللي ثانية (لـ 60 إطارًا في الثانية).
أسباب التقطّع (Jank)
- إعادة بناء مفرطة لأجزاء كبيرة من الشجرة
- طرق build مكلفة
- عمل متزامن كبير على العملية الرئيسية
- بناء غير فعّال للقوائم
- فك تشفير صور كبيرة على خيط الواجهة
الدرس 17.2 — طبقة الأداء التراكبية و DevTools
MaterialApp(
showPerformanceOverlay: true,
...
)
لوحات Flutter DevTools الأساسية: الأداء (الخط الزمني، مخطط الإطارات)، محلّل المعالج (CPU Profiler)، الذاكرة، إحصاءات إعادة بناء العناصر، الشبكة.
الوحدة 2 — التحكّم في إعادة البناء
الدرس 17.3 — لماذا تهم إعادة البناء
التقنيات
- حافظ على الحالة منخفضة قدر الإمكان في الشجرة.
- قسّم العناصر الكبيرة إلى عناصر أصغر.
- استخدم مُنشِئات const كلما أمكن.
- استخدم المُحدِّدات (selectors) / select في Riverpod أو Consumer بعناية.
- تجنّب إنشاء كائنات جديدة داخل build عند استخدامها كتبعيات.
الدرس 17.4 — قوة const
// جيد
const Text('Hello')
const SizedBox(height: 16)
const Icon(Icons.star)
// جيد أيضًا
const ProductCard(product: someProduct) // إذا كان لـ ProductCard مُنشئ const
الدرس 17.5 — تقليل نطاق إعادة البناء
// يعيد البناء فقط عند تغيّر العدد، وليس عند تغيّر حالة أخرى
final count = ref.watch(counterProvider.select((state) => state.count));
الوحدة 3 — أداء القوائم والشبكات
الدرس 17.6 — ListView.builder والبناء الكسول
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ItemTile(item: items[index]);
},
)
تحسينات إضافية
- وفّر itemExtent عندما يكون لكل العناصر نفس الارتفاع.
- استخدم ListView.separated عند الحاجة إلى فواصل.
- تجنّب العمل الثقيل داخل itemBuilder.
الدرس 17.7 — أداء GridView
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: products.length,
itemBuilder: (context, index) => ProductCard(product: products[index]),
)
الوحدة 4 — الصور والذاكرة
الدرس 17.8 — تحسين الصور
أفضل الممارسات
- غيّر حجم الصور على الخادم أو مسبقًا كلما أمكن.
- استخدم cacheWidth و cacheHeight مع Image.network / Image.asset.
- فضّل حزمة تخزين مؤقت (مثل cached_network_image).
- وفّر عناصر بديلة للتحميل والخطأ.
Image.network(
imageUrl,
width: 120,
height: 120,
fit: BoxFit.cover,
cacheWidth: 240, // ضعف الحجم للشاشات عالية الكثافة
cacheHeight: 240,
)
الوحدة 5 — العمل الثقيل والعمليات المتوازية
الدرس 17.9 — نقل العمل خارج العملية الرئيسية
final products = await compute(parseProducts, largeJsonString);
الوحدة 6 — الأنماط المضادة الشائعة وإصلاحاتها
الدرس 17.10 — كود سيّئ ← المشكلة ← كود محسَّن
- إعادة بناء شاشة كبيرة لتغيير صغير ← استخرج الجزء المتغيّر إلى عنصر منفصل أو استخدم محدِّدًا دقيقًا.
- إنشاء قائمة جديدة من العناصر في كل بناء ← استخدم ListView.builder / GridView.builder.
- عناصر غير const لا تتغيّر أبدًا ← أضف مُنشِئات const.
- تحميل صور كاملة الحجم للصور المصغّرة ← استخدم cacheWidth/cacheHeight أو تغيير الحجم من جانب الخادم.
- عمل متزامن ثقيل على خيط الواجهة ← انقله إلى عملية متوازية باستخدام compute.
- الاستماع إلى كائن حالة كبير بالكامل ← استخدم select (Riverpod) أو ما يعادله.
مشروع تطبيقي للمستوى 17 — تحسين شاشة بطيئة
قِس خط الأساس باستخدام DevTools، وحدّد إعادة البناء غير الضرورية، وحوّل القوائم إلى نسخ builder، وأضف const، وحسّن تحميل الصور، وانقل التحليل الثقيل إلى عملية متوازية، ووثّق التحسّن.
أسئلة مراجعة المستوى 17
- لماذا تساعد const في تحسين الأداء؟
- ما الميزة الرئيسية لـ ListView.builder على ListView العادي؟
- متى يجب أن تفكّر في استخدام عملية متوازية (isolate)؟
- ما الأداة التي تستخدمها لفحص الإطارات الطويلة وإعادة البناء؟
- لماذا يُعد تحميل صورة كاملة الدقة في عنصر مصغّر صغير أمرًا خطيرًا؟
المستوى 18 — الحزم والإضافات (Packages & Plugins)
نظرة عامة على المستوى
يعلّمك هذا المستوى كيفية عمل نظام pub البيئي، وكيفية إدارة التبعيات، وتقييم جودة الحزم، وإنشاء حزمك وإضافاتك القابلة لإعادة الاستخدام.
الوحدة 1 — النظام البيئي للحزم
الدرس 18.1 — ما هي الحزم ومن أين تأتي؟
أنواع الحزم
- حزم Dart خالصة (تعمل في أي مكان يعمل فيه Dart)
- حزم فلاتر (تعتمد على واجهات برمجة تطبيقات فلاتر)
- الإضافات (Plugins) (حزم فلاتر تتضمن كود أندرويد/iOS/ويب/سطح مكتب أصلي)
الدرس 18.2 — pubspec.yaml بعمق
name: my_app
description: A professional Flutter application
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
flutter_riverpod: ^2.5.0
shared_preferences: ^2.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
mocktail: ^1.0.0
build_runner: ^2.4.0
flutter:
uses-material-design: true
assets:
- assets/images/
fonts:
- family: Inter
fonts:
- asset: assets/fonts/Inter-Regular.ttf
الوحدة 2 — إدارة الإصدارات والتبعيات
الدرس 18.3 — الإصدارات الدلالية (Semantic Versioning)
الصيغة: MAJOR.MINOR.PATCH (تغييرات كاسرة / ميزات جديدة / إصلاح أخطاء).
http: ^1.2.0 # >=1.2.0 <2.0.0
http: >=1.2.0 <1.4.0
http: 1.2.1 # إصدار محدد بدقة (نادرًا ما يُنصح به)
الدرس 18.4 — حل تعارضات التبعيات
flutter pub get
flutter pub outdated
flutter pub deps
استخدم dependency_overrides فقط كحل أخير مؤقت.
الدرس 18.5 — تقييم جودة الحزمة
قبل إضافة حزمة، افحص: الشعبية، نقاط pub، الصيانة، التوثيق، الأمان من القيم الفارغة، دعم المنصات، الترخيص، والأمان.
الوحدة 3 — إنشاء حزمك الخاصة
الدرس 18.6 — إنشاء حزمة Dart / فلاتر قابلة لإعادة الاستخدام
flutter create --template=package my_utils
my_utils/
├── lib/
│ └── my_utils.dart # ملف التصدير الرئيسي
├── test/
├── pubspec.yaml
└── README.md
// lib/my_utils.dart
library my_utils;
export 'src/string_extensions.dart';
export 'src/date_helpers.dart';
export 'src/validators.dart';
الدرس 18.7 — التبعيات بالمسار المحلي
dependencies:
my_utils:
path: ../my_utils
الدرس 18.8 — مقدمة إلى الإضافات وقنوات المنصة
الإضافة (Plugin) هي حزمة فلاتر تتضمن كودًا أصليًا وتتواصل مع Dart عبر قنوات المنصة: Dart ↔ MethodChannel / EventChannel ↔ الكود الأصلي.
مشروع تطبيقي للمستوى 18 — إنشاء حزمة أدوات صغيرة
أنشئ حزمة قابلة لإعادة الاستخدام باسم app_validators تحتوي على دوال تحقق شائعة مع اختبارات وملف README واضح.
أسئلة مراجعة المستوى 18
- ما الفرق بين dependencies و dev_dependencies؟
- ماذا يعني رمز الإقحام (^) في قيد الإصدار؟
- لماذا يجب أن تكون حذرًا مع dependency_overrides؟
- اذكر أربعة أشياء على الأقل يجب فحصها قبل إضافة حزمة إلى مشروع إنتاجي.
- متى يكون من المنطقي إنشاء حزمتك الخاصة بدلًا من نسخ الكود بين المشاريع؟
المستوى 19 — ميزات الجهاز
نظرة عامة على المستوى
يعلّمك هذا المستوى كيفية دمج ميزات الجهاز الشائعة: الصلاحيات، الكاميرا، الموقع، الخرائط، الإشعارات، البيومترية، والوصول إلى الملفات.
الوحدة 1 — أساس الصلاحيات
الدرس 19.1 — صلاحيات وقت التشغيل
أفضل الممارسات
- اطلب الصلاحيات قبيل الحاجة إليها مباشرة (وليس عند بدء التطبيق).
- اشرح سبب حاجتك للصلاحية قبل ظهور مربع حوار النظام.
- وفّر بديلًا واضحًا عند رفض الصلاحية.
- وجّه المستخدم إلى إعدادات التطبيق عندما تُرفض الصلاحية بشكل دائم.
حزمة شائعة: permission_handler.
الوحدة 2 — الكاميرا والصور
الدرس 19.2 — الكاميرا والمعرض
حزمة شائعة: image_picker.
final ImagePicker picker = ImagePicker();
// من الكاميرا
final XFile? photo = await picker.pickImage(
source: ImageSource.camera,
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 85,
);
// من المعرض
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
);
إعداد المنصة
- أندرويد: صلاحيات الكاميرا والتخزين في AndroidManifest.xml.
- iOS: NSCameraUsageDescription و NSPhotoLibraryUsageDescription في Info.plist.
الوحدة 3 — الموقع والخرائط
الدرس 19.3 — GPS والموقع الحالي
حزمة شائعة: geolocator.
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.whileInUse ||
permission == LocationPermission.always) {
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
print('${position.latitude}, ${position.longitude}');
}
الدرس 19.4 — عرض الخرائط
حزمة شائعة: google_maps_flutter (تتطلب مفاتيح API وإعداد المنصة).
الوحدة 4 — الإشعارات
الدرس 19.5 — الإشعارات المحلية
حزمة شائعة: flutter_local_notifications.
المواضيع الأساسية: التهيئة، طلب صلاحية الإشعارات، الجدولة، التعامل مع النقرات، قنوات الإشعارات على أندرويد.
الوحدة 5 — البيومترية والوصول الآمن
الدرس 19.6 — المصادقة البيومترية
حزمة شائعة: local_auth. وفّر دائمًا بديلًا (رمز PIN / كلمة مرور) عند فشل البيومترية أو عدم توفرها.
الوحدة 6 — الملفات والتخزين
الدرس 19.7 — اختيار الملفات وتخزين الجهاز
حزم شائعة: file_picker، path_provider، open_filex.
مشروع تطبيقي للمستوى 19 — تدفّق “إضافة ذكرى” غني بالميزات
ادمج معالجة صلاحيات الكاميرا/المعرض، والموقع، والملاحظات، والحفظ المحلي، وإشعار تأكيد في تدفّق واحد متماسك.
أسئلة مراجعة المستوى 19
- لماذا يجب طلب الصلاحيات قبيل الحاجة مباشرة بدلًا من بدء التطبيق؟
- ما الفرق بين ImageSource.camera و ImageSource.gallery؟
- اذكر ثلاث حالات لصلاحية الموقع يجب التعامل معها.
- لماذا من المهم الإعلان عن أوصاف الاستخدام في Info.plist على iOS؟
- متى تختار الإشعارات المحلية على إشعارات الدفع (push)؟
المستوى 20 — فلاتر على الويب وسطح المكتب
نظرة عامة على المستوى
يدعم فلاتر رسميًا الويب و Windows و macOS و Linux من قاعدة كود واحدة. يعلّمك هذا المستوى كيفية تكييف تطبيقات فلاتر لبيئتَي الويب وسطح المكتب.
الوحدة 1 — نظرة عامة على المنصات
الدرس 20.1 — دعم فلاتر متعدد المنصات
المنصات المدعومة: أندرويد، iOS، الويب (Chrome، Firefox، Safari، Edge)، Windows، macOS، Linux. يمكن للويب استخدام محرك عرض HTML أو CanvasKit (WebAssembly + Skia).
الدرس 20.2 — التفعيل والتشغيل على كل منصة
# الويب
flutter create . --platforms=web
flutter run -d chrome
flutter build web
# Windows
flutter create . --platforms=windows
flutter run -d windows
flutter build windows
# macOS
flutter create . --platforms=macos
flutter run -d macos
flutter build macos
# Linux
flutter create . --platforms=linux
flutter run -d linux
flutter build linux
الوحدة 2 — كشف المنصة والكود الشرطي
الدرس 20.3 — كشف المنصة الحالية
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io' show Platform;
if (kIsWeb) {
// كود خاص بالويب
}
if (!kIsWeb && Platform.isWindows) {
// كود خاص بـ Windows فقط
}
أفضل ممارسة
أنشئ طبقات تجريد صغيرة بدلًا من نثر فحوصات if (Platform.is...) في أنحاء الواجهة.
الوحدة 3 — تخطيطات متجاوبة لسطح المكتب والويب
الدرس 20.4 — التصميم للشاشات الكبيرة
if (constraints.maxWidth >= 1200) {
return DesktopLayout();
} else if (constraints.maxWidth >= 800) {
return TabletLayout();
} else {
return MobileLayout();
}
الدرس 20.5 — أنماط التنقّل حسب المنصة
الوحدة 4 — طرق الإدخال
الدرس 20.6 — لوحة المفاتيح والفأرة
ادعم اختصارات لوحة المفاتيح الشائعة، واستخدم عناصر Shortcuts/Actions/Focus، وتأكّد من حالات التحويم، وقوائم النقر بزر الفأرة الأيمن حيث يُتوقّع ذلك، وتمرير طبيعي بعجلة الفأرة.
الوحدة 5 — اعتبارات خاصة بكل منصة
الدرس 20.7 — خصائص فلاتر للويب
القيود: حجم تنزيل أولي أكبر، بعض الإضافات لا تدعم الويب، تحسين محركات البحث (SEO) محدود، قيود واجهات برمجة تطبيقات المتصفح، وصول مقيَّد لنظام الملفات.
الدرس 20.8 — خصائص سطح المكتب (Windows، macOS، Linux)
ضع في اعتبارك إدارة النوافذ، والمظهر الأصلي، وتكاملات نظام الملفات، والتوزيع (MSIX، تطبيق .app موثَّق، AppImage/Snap/Flatpak).
مشروع تطبيقي للمستوى 20 — تطبيق ملاحظات أو لوحة تحكّم متعدد المنصات
كيّف تطبيقًا موجودًا للويب وسطح المكتب بتنقّل متجاوب، واختصارات لوحة مفاتيح، واستخدام محمي لـ dart:io.
أسئلة مراجعة المستوى 20
- لماذا يجب حماية استخدام dart:io بـ kIsWeb؟
- ما نمط التنقّل المُفضَّل عمومًا على سطح المكتب بدلًا من شريط تنقّل سفلي؟
- اذكر قيدين يجب مراعاتهما عند استهداف الويب بفلاتر.
- لماذا تُعد اختصارات لوحة المفاتيح مهمة على سطح المكتب والويب؟
- ما فائدة استخدام نظام تخطيط قائم على نقاط التكسّر عبر الجوال والجهاز اللوحي وسطح المكتب؟
المستوى 21 — نشر التطبيقات
نظرة عامة على المستوى
يعلّمك هذا المستوى كيفية نشر تطبيقات فلاتر على أندرويد (Google Play)، و iOS (App Store)، والويب.
الوحدة 1 — إدارة الإصدارات والتحضير للنشر
الدرس 21.1 — إدارة الإصدارات
version: 1.2.3+45
1.2.3 هو اسم الإصدار؛ 45 هو رمز الإصدار/رقم البناء (يجب أن يزداد مع كل رفعة للمتجر).
الدرس 21.2 — بناء التصحيح مقابل بناء الإصدار
flutter build appbundle # أندرويد (مفضَّل)
flutter build apk --release # ملف APK لأندرويد
flutter build ipa # iOS (على macOS)
flutter build web # الويب
الوحدة 2 — نشر أندرويد
الدرس 21.3 — توقيع التطبيق ومخزن المفاتيح (Keystore)
keytool -genkey -v -keystore upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
أفضل الممارسات
- احتفظ بنسخ احتياطية آمنة لمخزن المفاتيح وكلمات المرور.
- فضّل توقيع تطبيق جوجل بلاي (Google Play App Signing).
- لا تشارك مخزن المفاتيح أبدًا في محادثة أو تودعه في نظام التحكم بالمصادر.
الدرس 21.4 — APK مقابل حزمة تطبيق أندرويد (AAB)
حزمة تطبيق أندرويد (AAB) هي الصيغة المطلوبة للتطبيقات الجديدة على جوجل بلاي.
flutter build appbundle
الدرس 21.5 — Google Play Console
الخطوات الرئيسية: إنشاء حساب مطوّر، إنشاء التطبيق، إكمال قائمة المتجر، ضبط الفئة/جهة الاتصال/سياسة الخصوصية، إكمال تصنيف المحتوى، رفع حزمة التطبيق على مسار إصدار، الطرح.
الأصول المهمة: أيقونة عالية الدقة، رسم مميّز (feature graphic)، لقطات شاشة للهاتف/الجهاز اللوحي، سياسة خصوصية.
الوحدة 3 — نشر iOS
الدرس 21.6 — الشهادات، المعرّفات، وملفات التزويد (Provisioning Profiles)
المفاهيم الأساسية: حساب مطوّر آبل، معرّف التطبيق/الحزمة، الشهادة، ملف التزويد.
التدفّق العام: سجّل معرّف الحزمة في بوابة مطوّري آبل، أنشئ الشهادات أو دع Xcode يديرها، افتح ios/Runner.xcworkspace، اختر الفريق، فعّل التوقيع التلقائي، اضبط هدف النشر وأرقام الإصدار/البناء.
الدرس 21.7 — TestFlight و App Store
ارفع بناءً عبر Xcode أو Transporter، وادعُ المختبِرين عبر TestFlight، ثم أنشئ سجلّ App Store Connect، واملأ البيانات الوصفية/لقطات الشاشة، وارفع البناء، وقدّمه للمراجعة.
أسباب الرفض الشائعة: بيانات وصفية ناقصة، سياسة خصوصية/تسميات خصوصية مفقودة، تعطّل عند الإطلاق، محتوى نائب، مخالفات الإرشادات.
الوحدة 4 — نشر الويب
الدرس 21.8 — بناء واستضافة فلاتر للويب
flutter build web
المخرجات في build/web/ — ملفات ثابتة قابلة للاستضافة في أي مكان (Firebase Hosting، Netlify، Vercel، GitHub Pages، S3 + CloudFront، إلخ).
قائمة تحقق تطبيقية للمستوى 21 — أول إصدار إنتاجي
أندرويد: الإصدار محدَّث، مخزن المفاتيح مُنسَخ احتياطيًا، التوقيع مضبوط، بناء appbundle ناجح، قائمة المتجر مكتملة، سياسة الخصوصية منشورة، تصنيف المحتوى منجَز، مسار الاختبار موثَّق، الإصدار الإنتاجي جاهز.
iOS: حساب المطوّر نشط، معرّف الحزمة مسجَّل، التوقيع يعمل، بناء الإصدار مُختبَر على جهاز، سجلّ App Store Connect منشأ، لقطات الشاشة/البيانات الوصفية جاهزة، تسميات الخصوصية مكتملة، بناء TestFlight مُختبَر، مقدَّم للمراجعة.
الويب: البناء ينجح، مستضاف على HTTPS، base href/التوجيه موثَّق، الأداء مفحوص، النطاق المخصص مضبوط.
أسئلة مراجعة المستوى 21
- ما الفرق بين اسم الإصدار ورمز الإصدار؟
- لماذا تُفضَّل حزمة تطبيق أندرويد على APK لجوجل بلاي؟
- ما الخطر الرئيسي لفقدان مخزن مفاتيح الرفع لأندرويد؟
- ما استخدام TestFlight؟
- اذكر ثلاثة عناصر مطلوبة عادةً في قائمة المتجر.
المستوى 22 — CI/CD
نظرة عامة على المستوى
يعلّمك هذا المستوى الجانب العملي لـ CI/CD في مشاريع فلاتر: عادات Git، التفريع، طلبات السحب، مراجعة الكود، وسير عمل GitHub Actions.
الوحدة 1 — سير عمل Git الاحترافي
الدرس 22.1 — أساسيات Git للفرق
git status
git add .
git commit -m "Add login validation and error handling"
git push origin feature/login-validation
git pull origin main
الدرس 22.2 — استراتيجية التفريع
- main — دائمًا مستقر وقابل للنشر
- develop (اختياري) — فرع التكامل
- feature/*، bugfix/*، release/*، hotfix/*
الدرس 22.3 — طلبات السحب ومراجعة الكود
يحتوي طلب السحب الجيد على عنوان ووصف واضحَين، ولقطات شاشة للتغييرات في الواجهة، وملاحظات عن الاختبار، وقضية مرتبطة. راجع من أجل الصحة، وقابلية القراءة، واتساق العمارة، وتغطية الاختبار.
الوحدة 2 — التكامل المستمر مع GitHub Actions
الدرس 22.4 — ما هو CI/CD؟
CI — بناء واختبار تلقائي عند كل دفعة/طلب سحب. CD — إنشاء أصول إصدار تلقائيًا ونشرها.
الدرس 22.5 — مفاهيم GitHub Actions الأساسية
سير العمل (YAML في .github/workflows/)، المُحفِّز (on:)، المهمة (Job)، الخطوة (Step)، المُشغِّل (Runner)، السرّ (Secret).
الدرس 22.6 — سير عمل CI أساسي لفلاتر
name: Flutter CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0' # استخدم إصدارًا مستقرًا
channel: 'stable'
- name: Install dependencies
run: flutter pub get
- name: Analyze code
run: flutter analyze
- name: Run tests
run: flutter test
الدرس 22.7 — إضافة عمليات البناء إلى CI
build-android:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
- run: flutter pub get
- run: flutter build appbundle
الوحدة 3 — الأسرار والإعدادات
الدرس 22.8 — إدارة الأسرار
خزّن مفاتيح API، وكلمات مرور مخزن المفاتيح، وملفات JSON لحساب الخدمة، والشهادات، والرموز في إعدادات المستودع ← الأسرار والمتغيرات ← Actions، وأشِر إليها بـ ${{ secrets.MY_SECRET_NAME }}. لا تطبع الأسرار في السجلات أبدًا ولا تودعها.
الدرس 22.9 — متغيرات البيئة والنكهات (Flavors)
مرّر متغيرات بيئة إلى سير عمل CI وادمجها مع نكهات فلاتر أو --dart-define لبناءات التطوير/التجهيز/الإنتاج.
مشروع تطبيقي للمستوى 22 — إضافة CI لتطبيقك
أنشئ .github/workflows/flutter-ci.yml، شغّله عند الدفع/طلب السحب إلى main، ثبّت فلاتر، شغّل flutter pub get و flutter analyze و flutter test، واختياريًا ابنِ أصول ويب/أندرويد.
أسئلة مراجعة المستوى 22
- ما الغرض الرئيسي من التكامل المستمر؟
- لماذا يجب أن يبقى main مستقرًا وقابلًا للنشر؟
- أين تخزّن القيم الحساسة مثل كلمات مرور مخزن المفاتيح عند استخدام GitHub Actions؟
- ماذا يخبرك فحص CI الفاشل عن طلب سحب؟
- لماذا من الأفضل تشغيل الاختبارات تلقائيًا عند كل طلب سحب بدلًا من الاختبار اليدوي فقط؟
المستوى 23 — الذكاء الاصطناعي + تطوير فلاتر
نظرة عامة على المستوى
يغطّي هذا المستوى كيف يمكنك استخدام الذكاء الاصطناعي لتصبح مطوّر فلاتر أكثر إنتاجية، وكيفية بناء تطبيقات فلاتر تدمج النماذج اللغوية الكبيرة (LLMs) وخدمات ذكاء اصطناعي أخرى.
الوحدة 1 — الذكاء الاصطناعي كمساعد تطوير
الدرس 23.1 — البرمجة والتصحيح بمساعدة الذكاء الاصطناعي
ما يمكن أن يساعد فيه الذكاء الاصطناعي: توليد الكود الروتيني، شرح الأخطاء، اقتراح إعادة الهيكلة، كتابة حالات الاختبار، صياغة التوثيق.
أفضل الممارسات: لا تلصق أبدًا أسرارًا في أدوات الذكاء الاصطناعي، فضّل الطلبات الصغيرة المركّزة، راجع واختبر الكود المولَّد دائمًا.
الدرس 23.2 — صياغة طلبات (Prompts) أفضل للكود
تتضمن الطلبات الجيدة هدفًا محددًا، وقيودًا ذات صلة، وسياق الكود الموجود، والسلوك المتوقَّع. مثال: “أنشئ نموذج تسجيل دخول بفلاتر باستخدام TextFormField و Form والتحقق من الصحة. استخدم Riverpod للحالة. أظهر حالتَي تحميل وخطأ. استهدف فلاتر 3.24 و Material 3.”
الوحدة 2 — دمج الذكاء الاصطناعي في تطبيقات فلاتر
الدرس 23.3 — استدعاء واجهات برمجة تطبيقات النماذج اللغوية
التدفّق النموذجي: يدخل المستخدم طلبًا ← يرسل التطبيق الطلب (مع السجلّ) إلى API ← تعيد API استجابة ← يعرض التطبيق ويخزّنها.
ملاحظة أمان: لا تُضمِّن أبدًا مفاتيح API السرية مباشرة في العميل عندما يمكن استخراج المفتاح. فضّل خادمًا وسيطًا (proxy) للتطبيقات الإنتاجية.
الدرس 23.4 — هندسة الطلبات للتطبيقات
طلب النظام (System Prompt) يحدد الدور/النبرة/الحدود؛ طلب المستخدم هو الطلب الفعلي؛ سجلّ المحادثة يحافظ على السياق (مع مراعاة حدود الرموز).
الدرس 23.5 — الاستجابات المتدفّقة (Streaming)
يعرض البث الرموز أثناء وصولها لتجربة أكثر استجابة. أظهر مؤشر كتابة، واسمح للمستخدم بإيقاف التوليد، وتعامل مع انقطاع الاتصال بسلاسة.
الوحدة 3 — بناء واجهة محادثة
الدرس 23.6 — عناصر واجهة المحادثة الأساسية
قائمة رسائل، حقل إدخال نص، زر إرسال، مؤشر تحميل/بث، إجراءات اختيارية للمسح/النسخ/إعادة التوليد.
الدرس 23.7 — نموذج الرسالة والسجلّ
enum MessageRole { user, assistant, system }
class ChatMessage {
final MessageRole role;
final String content;
final DateTime timestamp;
ChatMessage({
required this.role,
required this.content,
DateTime? timestamp,
}) : timestamp = timestamp ?? DateTime.now();
}
الوحدة 4 — الأمان واعتبارات الإنتاج
الدرس 23.8 — حماية مفاتيح API والبيانات
استخدم خادمًا وسيطًا يحمل مفتاح API الحقيقي، وطبّق المصادقة، وحدّد معدّل الطلبات، وراقب الاستخدام/التكاليف، وتجنّب إرسال بيانات حساسة غير ضرورية إلى واجهات الذكاء الاصطناعي.
مشروع تطبيقي للمستوى 23 — تطبيق محادثة بالذكاء الاصطناعي
ابنِ تطبيق محادثة كامل بذكاء اصطناعي بواجهة محادثة نظيفة، وسجلّ محادثة، ودمج مع API نموذج لغوي (مباشرة أو عبر وسيط)، وحالتَي تحميل/خطأ، وبث اختياري.
أسئلة مراجعة المستوى 23
- لماذا يجب ألا تُخزَّن مفاتيح API السرية مباشرة في عميل فلاتر للتطبيقات الإنتاجية؟
- ما فائدة الاستجابات المتدفّقة في واجهة محادثة؟
- ما المعلومات التي يجب أن يتضمنها طلب برمجي جيد؟
- لماذا يُعد سجلّ المحادثة مهمًا عند استدعاء نموذج لغوي؟
- اذكر اعتبارَي أمان أو خصوصية عند إضافة ميزات ذكاء اصطناعي توليدي إلى تطبيق جوال.
المستوى 24 — مشاريع احترافية
نظرة عامة على المستوى
يرشدك هذا المستوى عبر مشاريع تدريجية من واقع الحياة، من أساسيات المبتدئين إلى مشروع تخرّج بأسلوب إنتاجي.
الوحدة 1 — مشاريع المبتدئين
المشروع 24.1 — آلة حاسبة: عمليات حسابية أساسية، مسح/حذف، أزرار متجاوبة، عرض المجموع الجاري. التركيز: التخطيط، معالجة الإيماءات، الحالة البسيطة.
المشروع 24.2 — قائمة مهام: إضافة/تعديل/إكمال/حذف المهام، حفظ محلي، تصفية، حالة فارغة. التركيز: القوائم، النماذج، التخزين المحلي، إدارة الحالة.
المشروع 24.3 — تطبيق اختبارات (Quiz): أسئلة متعددة، تتبّع النتيجة، شاشة النتائج، إعادة البدء. التركيز: التنقّل، الحالة عبر الشاشات.
المشروع 24.4 — متتبّع مصروفات (أساسي): إضافة مصروفات، قائمة، حساب إجمالي بسيط، حفظ محلي. التركيز: النماذج، النماذج البيانية، القوائم، التجميع.
الوحدة 2 — مشاريع متوسطة
المشروع 24.5 — تطبيق طقس: جلب حسب المدينة/الموقع، حالات تحميل/خطأ/فراغ. التركيز: دمج API، معالجة الأخطاء.
المشروع 24.6 — تطبيق أخبار: قائمة مقالات، شاشة تفاصيل، سحب للتحديث، بحث/تصفية، تصفّح بالصفحات. التركيز: واجهات REST، نمط المستودع.
المشروع 24.7 — تطبيق وصفات: تصفّح، بحث/تصفية، شاشة تفاصيل بالمكونات والخطوات، مفضّلات محلية. التركيز: دمج بيانات بعيدة + محلية.
المشروع 24.8 — تطبيق ملاحظات (محسَّن): CRUD كامل، فئات، بحث، حذف ناعم/سلة مهملات، عمل دون اتصال أولًا. التركيز: قاعدة بيانات محلية، تجربة دون اتصال.
المشروع 24.9 — متصفّح منتجات تجارة إلكترونية: قائمة/تفاصيل منتجات، فئات/تصفية، سلة محلية، تدفّق دفع بسيط. التركيز: حالة معقّدة (السلة)، التنقّل، نماذج نظيفة.
الوحدة 3 — مشاريع متقدمة
المشروع 24.10 — تطبيق محادثة: مصادقة، قائمة محادثات، رسائل شبه فورية، حالات إرسال. التركيز: التدفقات، حالة واجهة معقّدة.
المشروع 24.11 — خلاصة على نمط وسائل التواصل: مصادقة، خلاصة منشورات، إنشاء منشور (نص + صورة اختيارية)، إعجاب/تعليق، ملف شخصي. التركيز: Firestore + Storage، التصفّح بالصفحات.
المشروع 24.12 — تطبيق توصيل طعام: تصفّح مطاعم/قوائم، سلة وملخّص طلب، تقديم الطلب، سجلّ الطلبات، تدفّق حالة. التركيز: عمارة متعددة الميزات، حالة معقّدة.
المشروع 24.13 — تطبيق حجوزات: تصفّح الخدمات/الغرف، اختيار تاريخ/وقت، نموذج حجز، سجلّ حجوزات، مصادقة. التركيز: النماذج، التحقق من الصحة، نمذجة النطاق.
المشروع 24.14 — نظام إدارة تعلّم (مبسّط): قائمة/تفاصيل دورات، دروس، تتبّع تقدّم، اختبار بسيط، مصادقة. التركيز: محتوى منظَّم، استمرارية التقدّم.
الوحدة 4 — مشروع التخرّج الاحترافي
المشروع 24.15 — مشروع تخرّج بأسلوب إنتاجي
أفكار منتج مقترحة: متتبّع مالي شخصي، تطبيق إدارة مهام/مشاريع، تطبيق محتوى، تطبيق سوق، متتبّع صحة أو عادات.
المتطلبات الوظيفية: مصادقة (بريد/كلمة مرور + مزوّد اجتماعي واحد)، ملف شخصي، ميزات CRUD أساسية + منطق أعمال، بحث/تصفية، تصفّح بالصفحات، رفع صور، تخزين مؤقت محلي/دعم دون اتصال، إشعارات، إعدادات.
المتطلبات التقنية: عمارة نظيفة، نمط مستودع، إدارة حالة احترافية، حالات خطأ/تحميل/فراغ مناسبة، تحقق من صحة النماذج، إدارة رموز/جلسات آمنة، اختبارات وحدة، اختبارات واجهة، واجهة متجاوبة، قوائم/صور واعية بالأداء.
توقعات العمارة: هيكل قائم على الميزات، كيانات النطاق/واجهات المستودع، طبقة بيانات (بعيدة + محلية)، طبقة عرض، حقن تبعية.
خطة التنفيذ
- حدّد المتطلبات وتدفقات المستخدم.
- صمّم نماذج البيانات والعمارة.
- أعدّ هيكل المشروع وحقن التبعية.
- نفّذ المصادقة.
- ابنِ شرائح الميزات الأساسية.
- أضف دعم التخزين المؤقت/دون اتصال.
- أضف البحث والتصفية والتصفّح بالصفحات.
- نفّذ الملف الشخصي والإعدادات.
- اكتب اختبارات للمسارات الحرجة.
- صقل الواجهة ومعالجة الأخطاء والأداء.
- جهّز بناءات الإصدار وتوثيق النشر.
أسئلة مراجعة المستوى 24
- لماذا من الأفضل بناء مشاريع تدريجية بدلًا من القفز مباشرة إلى تطبيق كبير؟
- ما الذي يجعل المشروع “بجودة معرض أعمال”؟
- لماذا يجب أن يتضمن مشروع التخرّج المصادقة ودعم العمل دون اتصال معًا؟
- ما قيمة تحديد العمارة قبل كتابة معظم كود الواجهة؟
- اذكر ثلاثة عناصر غير متعلقة بالواجهة ينظر إليها المراجعون غالبًا في مشروع فلاتر احترافي.
المستوى 25 — أفضل ممارسات فلاتر
نظرة عامة على المستوى
يجمع هذا المستوى أهم أفضل ممارسات فلاتر، مع شرح لكل واحدة مدعوم بالمنطق، والأخطاء الشائعة، وأمثلة جيدة مقابل سيئة.
الوحدة 1 — الكود النظيف والتسمية
الدرس 25.1 — مبادئ الكود النظيف
مثال سيّئ
Widget build(BuildContext c) {
return x ? Container(child: Text(a), color: b ? Colors.red : Colors.blue) : Container();
}
مثال جيد
Widget build(BuildContext context) {
if (!isVisible) return const SizedBox.shrink();
return ColoredBox(
color: isError ? Colors.red : Colors.blue,
child: Text(title),
);
}
الدرس 25.2 — قواعد التسمية
- الملفات والمجلدات: snake_case
- الأصناف، التعدادات (enums)، الأسماء البديلة (typedefs): PascalCase
- المتغيرات، الدوال، المعطيات: camelCase
- الثوابت: camelCase أو SCREAMING_SNAKE_CASE
- الأعضاء الخاصة: شرطة سفلية بادئة (_controller)
أفضل ممارسة: إذا احتجت تعليقًا لشرح الغرض من متغيّر، فمن المحتمل أن الاسم ليس جيدًا بما يكفي.
الوحدة 2 — العناصر وهيكل الواجهة
الدرس 25.3 — العناصر القابلة لإعادة الاستخدام
استخرج عنصرًا عندما: يتكرر نفس نمط الواجهة أكثر من مرة، أو تصبح طريقة build صعبة القراءة، أو يمتلك جزء من الواجهة حالته الخاصة.
الدرس 25.4 — حافظ على وضوح طرق build
استخرج أقسامًا مثل _Header() و _ProductInfo() و _ActionButtons() حتى لو استُخدمت مرة واحدة فقط.
الوحدة 3 — إدارة الحالة والعمارة
الدرس 25.5 — اختر إدارة الحالة بتروٍّ
الحالة المحلية العابرة ← setState كافٍ. الحالة المشتركة أو المعقّدة ← Riverpod أو BLoC أو ما يعادلهما. تجنّب خلط نُهُج متعددة بشكل عشوائي.
الدرس 25.6 — انضباط العمارة
يجب ألا تتحدث الواجهة مباشرة إلى عملاء HTTP الخام أو واجهات قواعد البيانات؛ قواعد الأعمال تنتمي خارج العناصر؛ المستودعات تخفي مصادر البيانات.
الوحدة 4 — الأخطاء، الأمان، والمرونة
الدرس 25.7 — معالجة الأخطاء
لا تترك أبدًا كتل catch فارغة. حوّل الاستثناءات منخفضة المستوى إلى رسائل سهلة الفهم. أظهر خيارات إعادة المحاولة. سجّل الأخطاء غير المتوقعة.
الدرس 25.8 — أساسيات الأمان
استخدم HTTPS في كل مكان، خزّن الرموز في تخزين آمن، تحقق من صحة الإدخال، لا تُضمِّن الأسرار في العميل، اطلب فقط الصلاحيات اللازمة.
الوحدة 5 — إمكانية الوصول، التجاوب، والأداء
الدرس 25.9 — إمكانية الوصول
وفّر تسميات دلالية، تباينًا كافيًا، دعم تحجيم النص على مستوى النظام، وأهدافًا تفاعلية كبيرة بما يكفي.
الدرس 25.10 — التصميم المتجاوب
صمّم لأحجام شاشات مختلفة منذ البداية باستخدام نقاط تكسّر وشبكات مرنة وتنقّل قابل للتكيف.
الدرس 25.11 — عادات الأداء
استخدم const كلما أمكن، فضّل مُنشِئات builder، تجنّب العمل الثقيل في build، غيّر حجم/خزّن الصور مؤقتًا، قِس قبل التحسين.
الوحدة 6 — التعاون وقابلية الصيانة
الدرس 25.12 — التوثيق
وثّق سبب اتخاذ قرار غير واضح، وواجهات برمجة التطبيقات العامة، وتعليمات الإعداد، وإعدادات البيئة.
الدرس 25.13 — عادات Git ومراجعة الكود
التزامات صغيرة ومركّزة؛ رسائل التزام واضحة؛ لا أسرار في السجلّ. راجع من أجل الصحة، وقابلية القراءة، والعمارة، والاختبارات.
تمرين تطبيقي للمستوى 25 — تدقيق أفضل الممارسات
دقّق مشروعًا موجودًا مقابل التسمية، حجم العناصر، اتساق إدارة الحالة، حدود العمارة، حالات الخطأ/الفراغ، أساسيات الأمان، إمكانية الوصول/التجاوب، نقاط ضعف الأداء، التوثيق، وتغطية الاختبار. طبّق خمسة تحسينات ملموسة على الأقل.
أسئلة مراجعة المستوى 25
- لماذا يُعد الاتساق في إدارة الحالة أهم من استخدام أحدث مكتبة دائمًا؟
- متى يجب استخراج عنصر جديد؟
- ما حالات الواجهة الأربع التي يجب أن تتعامل معها معظم الشاشات القائمة على البيانات؟
- لماذا يجب ألا تُخزَّن الرموز في SharedPreferences؟
- اذكر ثلاث عادات تحسّن قابلية الصيانة لزملاء الفريق المستقبليين.
المستوى 26 — الأخطاء الشائعة في فلاتر
نظرة عامة على المستوى
مرجع استكشاف أخطاء عملي. يتبع كل عنصر: المشكلة، لماذا تحدث، الكود غير الصحيح، الكود الصحيح، الحل، وكيفية الوقاية.
الوحدة 1 — أخطاء BuildContext ودورة الحياة
الخطأ 26.1 — استخدام BuildContext عبر فجوة غير متزامنة
غير صحيح
onPressed: () async {
await Future.delayed(const Duration(seconds: 2));
Navigator.of(context).pop(); // غير آمن إذا اختفى العنصر
}
صحيح
onPressed: () async {
await Future.delayed(const Duration(seconds: 2));
if (!context.mounted) return;
Navigator.of(context).pop();
}
الوقاية: تعامل مع كل await كنقطة محتملة قد يختفي فيها العنصر.
الخطأ 26.2 — استدعاء setState بعد dispose
صحيح
Timer.periodic(const Duration(seconds: 1), (_) {
if (!mounted) return;
setState(() => _seconds++);
});
الوقاية: اقرن كل مورد يُنشأ في initState بتنظيف في dispose.
الوحدة 2 — أخطاء البرمجة غير المتزامنة والأمان من القيم الفارغة
الخطأ 26.3 — نسيان معالجة أخطاء العمليات غير المتزامنة
صحيح
try {
final products = await api.fetchProducts();
// تحديث الحالة بالبيانات
} catch (e) {
// تحديث الحالة برسالة خطأ
}
الخطأ 26.4 — إساءة استخدام مُعامِل تأكيد عدم الفراغ (!)
صحيح
final name = user?.name ?? 'Guest';
// أو
if (user == null) return;
final name = user.name;
الوقاية: تعامل مع ! كرائحة كود (code smell) تحتاج تبريرًا.
الوحدة 3 — تسريبات الذاكرة والموارد
الخطأ 26.5 — نسيان dispose() للمُتحكِّمات
صحيح
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
الوحدة 4 — أخطاء متعلقة بالأداء
الخطأ 26.6 — استخدام ListView بقائمة أبناء كبيرة
صحيح
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemTile(item: items[index]),
)
الخطأ 26.7 — إعادة بناء غير ضرورية لأشجار فرعية كبيرة
الحل: انقل الحالة أقرب إلى مكان استخدامها، استخرج عناصر، استخدم مُنشِئات const، استخدم المُحدِّدات.
الوحدة 5 — أخطاء العمارة والحالة
الخطأ 26.8 — وضع منطق الأعمال واستدعاءات API مباشرة في العناصر
الحل: انقل المنطق إلى المُخطِرات/Cubits/حالات الاستخدام والوصول إلى البيانات إلى المستودعات.
الخطأ 26.9 — الإفراط في استخدام الحالة العامة/الشاملة
الحل: فضّل الحالة المحدَّدة النطاق حسب الميزة وحدود ملكية واضحة.
الوحدة 6 — أخطاء الشبكة والحزم
الخطأ 26.10 — تجاهل رموز حالة HTTP
الحل: تحقّق من رموز الحالة وتعامل مع 4xx/5xx صراحةً.
الخطأ 26.11 — تعارضات التبعيات والتجاوزات غير الحذرة
الحل: افحص flutter pub deps و flutter pub outdated، رقِّ بعناية، وتعامل مع التجاوزات كحل مؤقت.
الوحدة 7 — أخطاء البناء والإصدار
الخطأ 26.12 — مشكلات توقيع إصدار أندرويد
الحل: تحقّق من key.properties، وإعداد توقيع Gradle، وأرقام الإصدار. احتفظ بنسخ احتياطية آمنة لمخزن المفاتيح.
الخطأ 26.13 — أخطاء توقيع الكود والتزويد على iOS
الحل: طابِق معرّف الحزمة والفريق وإعدادات التوقيع. فضّل التوقيع التلقائي وجدّد الشهادات في الوقت المناسب.
الخطأ 26.14 — الاختبار في وضع التصحيح فقط
الحل: اختبر بانتظام بناءات الإصدار على أجهزة حقيقية.
تمرين تطبيقي للمستوى 26 — تدريب تصحيح
خذ مشروعًا موجودًا أو أدخل عمدًا 5 من الأخطاء أعلاه، أعد إنتاج الأعراض، أصلح كل مشكلة، واكتب ملاحظة موجزة لكل واحدة: السبب الجذري ← الإصلاح ← الوقاية.
أسئلة مراجعة المستوى 26
- لماذا يُعد استخدام BuildContext بعد await غير آمن؟
- ما أكثر أسباب “setState() called after dispose()” شيوعًا؟
- لماذا يُفضَّل ListView.builder للقوائم الطويلة؟
- ما خطر مُعامِل تأكيد عدم الفراغ (!)؟
- لماذا يجب اختبار بناءات الإصدار قبل تقديمها للمتجر؟
المستوى 27 — خارطة طريق مطوّر فلاتر
نظرة عامة على المستوى
يحدد هذا المستوى مسارًا عمليًا من مبتدئ تمامًا إلى مطوّر فلاتر أول (senior) عبر سبع مراحل.
المرحلة 1 — مبتدئ تمامًا
الهدف: أن تصبح مرتاحًا مع الأدوات الأساسية وبناء واجهات بالكود.
المشاريع الموصى بها: تنويعات Hello World، بطاقة ملف شخصي ثابتة، تطبيق عدّاد أساسي.
التقييم: يمكنك تشغيل تطبيق دون مساعدة وفهم ما تفعله main() و runApp() و MaterialApp.
المرحلة 2 — مبتدئ في Dart
الهدف: كتابة كود Dart صحيح وواثق دون تعقيد واجهة فلاتر.
المشاريع الموصى بها: تمارين طرفية (console) خالصة بـ Dart، نماذج بيانات بسيطة (User، Product، Note).
التقييم: يمكنك إنشاء نماذج بـ fromJson يدويًا واستخدام async/await دون ارتباك.
المرحلة 3 — مبتدئ في فلاتر
الهدف: بناء واجهات متعددة الشاشات بعناصر قياسية وتنقّل أساسي.
المشاريع الموصى بها: قائمة مهام، تطبيق اختبارات، تطبيق ملاحظات بسيط، متتبّع مصروفات أساسي.
التقييم: يمكنك تنفيذ تنقّل أساسي وتحقق من صحة نماذج، وحفظ بيانات بسيطة محليًا.
المرحلة 4 — مطوّر فلاتر متوسط
الهدف: بناء تطبيقات قائمة على البيانات تستهلك واجهات برمجة تطبيقات، وتدير الحالة باحترافية، وتتعامل مع حالات الواجهة الحقيقية.
المشاريع الموصى بها: تطبيق طقس، تطبيق أخبار/قائمة منتجات، تطبيق ملاحظات بقاعدة بيانات محلية، تدفّق مصادقة + ملف شخصي + رئيسية.
التقييم: يمكنك تنفيذ حالات تحميل/خطأ/فراغ بشكل متسق واستخدام حل حقيقي لإدارة الحالة يتجاوز setState.
المرحلة 5 — مطوّر فلاتر متقدم
الهدف: بناء ميزات قابلة للتوسّع بعمارة متينة، واختبارات، ووعي بالأداء، وواجهة متقدمة.
المشاريع الموصى بها: تطبيق محادثة أو شبيه بوسائل التواصل، تدفّق تجارة إلكترونية بسلة ومصادقة، تطبيق دون اتصال أولًا، مشروع تخرّج غني بالميزات.
التقييم: يمكنك شرح وتطبيق حدود العمارة، وكتابة اختبارات ذات معنى، وتشخيص مشكلات الأداء.
المرحلة 6 — مطوّر فلاتر محترف
الهدف: تسليم تطبيقات جاهزة للإنتاج والعمل بفعالية في بيئة فريق.
المشاريع الموصى بها: مشروع تخرّج كامل بأسلوب إنتاجي، مشروع عميل حقيقي أو محاكى، مساهمات في قاعدة كود فريق.
التقييم: قمت بشحن بناء إصدار حقيقي واحد على الأقل وكودك مفهوم لمطورين آخرين.
المرحلة 7 — مطوّر فلاتر أول (Senior)
الهدف: قيادة القرارات التقنية، رفع جودة الفريق، وتصميم أنظمة تبقى سليمة بمرور الوقت.
العمل الموصى به: قيادة تطبيق أو وحدة رئيسية غير تافهة، وضع إرشادات عمارة، الإرشاد (mentoring)، دفع تحسينات جودة قابلة للقياس.
التقييم: يثق الآخرون بحكمك التقني وتترك قواعد الكود أكثر صحّة مما وجدتها.
كيفية استخدام خارطة الطريق هذه
- حدّد مرحلتك الحالية بناءً على ما يمكنك بناؤه دون دروس تعليمية.
- ركّز على مهارات ومشاريع تلك المرحلة حتى تشعر معايير التقييم بأنها صادقة.
- انتقل للأمام عندما تستطيع أداء القدرات العملية للمرحلة باستمرار.
- راجع المراحل السابقة كلما ظهرت نقاط ضعف (هذا أمر طبيعي وصحّي).
تذكير بالواقع المهم: إكمال دورة أو نسخ مشاريع لا ينقلك تلقائيًا إلى المرحلة التالية. القدرة على البناء والتصحيح والشرح بشكل مستقل هي المقياس الحقيقي.
أسئلة مراجعة المستوى 27
- ما الفرق الرئيسي بين مطوّر فلاتر متوسط ومتقدم في خارطة الطريق هذه؟
- لماذا يُعد شحن بناء إصدار معلمًا مهنيًا مهمًا؟
- كيف تقرر ما إذا كنت مستعدًا للمرحلة التالية؟
- لماذا تكون العمارة أكثر أهمية في المراحل المتقدمة منها في مراحل المبتدئين؟
- ما الذي يميّز المطوّر الأول (senior) عن مساهم فردي قوي؟
المستوى 28 — التمارين والتقييمات
نظرة عامة على المستوى
يوفّر هذا المستوى تمارين وتقييمات ملموسة تغطي المجالات الرئيسية للدورة، بما في ذلك أسئلة اختيار من متعدد، وأسئلة إجابة قصيرة، وتمارين برمجية، ومشكلات تصحيح، ومهام تطبيقية.
القسم 28.1 — أسئلة اختيار من متعدد
- ما الغرض الرئيسي من StatefulWidget؟ أ. إنشاء عناصر لا تتغيّر أبدًا ب. حمل حالة قابلة للتغيير يمكنها تحديث الواجهة ج. استبدال MaterialApp د. إدارة التنقّل فقط
- أي عنصر يجب أن تفضّله للقوائم الديناميكية الطويلة؟ أ. Column ب. ListView ج. ListView.builder د. SingleChildScrollView
- ما الذي تساعد context.mounted على منعه؟ أ. أخطاء الأمان من القيم الفارغة ب. استخدام BuildContext بعد التخلّص من العنصر ج. تعارضات الحزم د. الرسوم المتحركة البطيئة
- أي خيار تخزين هو الأنسب لرموز التحديث؟ أ. SharedPreferences ب. ملف نصي عادي ج. flutter_secure_storage د. متغيرات عامة
- في العمارة النظيفة المطبَّقة على فلاتر، أي طبقة يجب أن تحتوي على قواعد الأعمال الخالصة دون استيرادات فلاتر؟ أ. العرض ب. البيانات ج. النطاق د. عناصر الواجهة
- ما الفائدة الأساسية لعناصر const؟ أ. تُفعّل إعادة التحميل السريع ب. يمكن أن تقلّل إعادة البناء غير الضرورية ج. تستبدل إدارة الحالة د. مطلوبة لاستدعاءات API
- أي نطاق من رموز حالة HTTP يشير عادةً إلى خطأ من العميل؟ أ. 1xx ب. 2xx ج. 4xx د. 5xx
- ما الغرض من المستودع (repository) في تطبيق فلاتر؟ أ. رسم رسومات مخصصة ب. العمل كمصدر وحيد للحقيقة للوصول إلى البيانات ج. استبدال السمات د. إدارة الرسوم المتحركة فقط
- أي أمر ينتج صيغة الرفع المفضّلة لجوجل بلاي؟ أ. flutter build apk ب. flutter build appbundle ج. flutter build ios د. flutter build web
- لماذا يجب نقل تحليل JSON الثقيل للبيانات الكبيرة إلى عملية متوازية غالبًا؟ أ. العمليات المتوازية مطلوبة لكل استدعاءات API ب. للحفاظ على استجابة خيط الواجهة ج. لأن JSON لا يمكن تحليله على العملية الرئيسية د. لتقليل حجم الحزمة
مفتاح الإجابات (28.1): 1-ب، 2-ج، 3-ب، 4-ج، 5-ج، 6-ب، 7-ج، 8-ب، 9-ب، 10-ب
القسم 28.2 — أسئلة إجابة قصيرة
أجب في 2–6 جمل.
- اشرح الفرق بين المصادقة والتفويض.
- لماذا يُفضَّل ListView.builder عمومًا على ListView(children: …) لمجموعات البيانات الكبيرة؟
- ما المشكلة التي يحلّها نمط المستودع؟
- لماذا من غير الآمن تخزين رموز الوصول في SharedPreferences؟
- ما الفرق بين الرسوم المتحركة الضمنية والصريحة في فلاتر؟
- لماذا يجب التحقق من context.mounted بعد await قبل استخدام BuildContext؟
- اذكر ثلاث حالات واجهة يجب أن تتعامل معها شاشة قائمة على البيانات عادةً.
- ما دور dispose() في StatefulWidget؟
القسم 28.3 — تمارين برمجية
تمرين 1 — نموذج + تحليل: أنشئ صنف Product بمعرّف وعنوان وسعر وصورة اختيارية. نفّذ fromJson/toJson واختبار وحدة.
تمرين 2 — التحقق من صحة النموذج: ابنِ نموذج تسجيل دخول بتحقق من صحة البريد الإلكتروني وكلمة المرور، مؤشر تحميل عند الإرسال، وزر معطَّل أثناء التحميل.
تمرين 3 — واجهة المستودع: عرّف NotesRepository مجرَّدًا بطرق getNotes/createNote/updateNote/deleteNote، ثم نفّذ نسخة مزيّفة في الذاكرة.
تمرين 4 — تخطيط متجاوب: أنشئ شاشة بتنقّل سفلي على الشاشات الضيقة و NavigationRail على الشاشات الأوسع.
تمرين 5 — معالجة الحالة غير المتزامنة: نفّذ مُحمِّل بيانات يعرض حالات تحميل/نجاح/خطأ لقائمة مجلوبة من مستودع مزيَّف.
القسم 28.4 — تمارين تصحيح
تصحيح 1
onPressed: () async {
final data = await api.fetchData();
setState(() => items = data);
}
تصحيح 2
ListView(
children: products.map((p) => ProductTile(product: p)).toList(),
)
تصحيح 3
final name = user!.name.toUpperCase();
تصحيح 4: شاشة تستخدم TextEditingController لكنها لا تتخلّص منه أبدًا — أظهر إدارة دورة الحياة الصحيحة.
القسم 28.5 — مهام تطبيقية
المهمة أ — ميزة API: جلب قائمة، إظهار حالات تحميل/خطأ/نجاح، فتح شاشة تفاصيل، استخدام مستودع وإدارة حالة.
المهمة ب — شريحة ملاحظات دون اتصال: إنشاء، عرض، تعديل، حذف ناعم، وبحث باستخدام تخزين محلي.
المهمة ج — تدفّق مصادقة: تسجيل دخول، تسجيل، استعادة جلسة، شاشة رئيسية محمية، تسجيل خروج، تخزين رموز آمن.
المهمة د — إعادة هيكلة بأفضل الممارسات: حسّن مشروعًا قديمًا في خمسة مجالات على الأقل: التسمية، استخراج العناصر، حدود الحالة، معالجة الأخطاء، استخدام const، استخراج المستودع، أو الاختبارات.
القسم 28.6 — تحديات متقدمة
- أضف تصفّحًا بالصفحات أو تمريرًا لانهائيًا لقائمة API دون كسر معالجة التحميل والخطأ.
- نفّذ مفتاح سمة (فاتح/داكن) يستمر عبر إعادة تشغيل التطبيق.
- اكتب اختبارات وحدة لمستودع يعتمد على مصدر بيانات بعيد وذاكرة تخزين مؤقت محلية. حاكِ التبعيات.
- حلّل شاشة متقطّعة (janky) باستخدام DevTools وطبّق تحسينَين قابلَين للقياس على الأقل.
- صمّم هيكل مجلدات قائم على الميزات لتطبيق يحتوي على Auth و Products و Cart و Profile. اشرح ما ينتمي إلى كل طبقة.
القسم 28.7 — قائمة التقييم الذاتي
قيّم نفسك بصدق (يحتاج عملًا / مقبول / قوي) في: الأمان من القيم الفارغة والبرمجة غير المتزامنة بلغة Dart؛ التخطيط والواجهة المتجاوبة؛ النماذج والتحقق من الصحة؛ إدارة الحالة بما يتجاوز setState؛ دمج API ومعالجة الأخطاء؛ التخزين المحلي/دعم العمل دون اتصال؛ تدفقات المصادقة؛ العمارة والمستودعات؛ اختبارات الوحدة والواجهة؛ أساسيات الأداء؛ أساسيات Git + CI؛ الوعي بالإصدار والنشر.
استخدم نقاط الضعف كخطة مراجعة مركّزة قبل المستوى 29.
المستوى 29 — اختبار الشهادة الاحترافية النهائي
نظرة عامة على المستوى
يقيّم هذا الاختبار النهائي ما إذا كان بإمكانك تطبيق المعرفة والمهارات من الدورة بأكملها على مستوى احترافي.
هيكل الاختبار
- الجزء 1 — أسئلة اختيار من متعدد (50)
- الجزء 2 — أسئلة تقنية (20)
- الجزء 3 — مشكلات تصحيح (10)
- الجزء 4 — تحديات برمجية (5)
- الجزء 5 — مشروع تخرّج احترافي + سلّم تقييم
إرشادات الوقت الموصى بها: الجزء 1: 60–75 دقيقة · الجزء 2: 60–90 دقيقة · الجزء 3: 60–90 دقيقة · الجزء 4: 3–6 ساعات · الجزء 5: عمل مشروع متعدد الأيام.
الجزء 1 — أسئلة اختيار من متعدد (50)
- ما الدور الأساسي لمحرك فلاتر (Flutter Engine)؟ أ. تعريف عناصر Material ب. التعامل مع العرض منخفض المستوى، وتخطيط النص، وقنوات المنصة ج. إدارة تبعيات pubspec د. استبدال بيئة تشغيل Dart
- أي عبارة عن StatelessWidget صحيحة؟ أ. يمكنه استدعاء setState ب. غير قابل للتغيير ويعتمد فقط على التهيئة والبيانات الموروثة ج. يجب أن يمتلك دائمًا مُتحكِّمًا د. لا يمكن استخدامه في تطبيقات الإنتاج
- ماذا تحفظ إعادة التحميل السريع (Hot Reload) ولا تحفظه إعادة التشغيل السريع (Hot Restart)؟ أ. حالة التطبيق في معظم الحالات ب. كود المنصة الأصلي ج. pubspec.lock د. أذونات AndroidManifest
- ما أكثر طريقة أمانًا لتخزين رموز المصادقة على الجهاز؟ أ. SharedPreferences ب. متغيرات مفردة عامة ج. flutter_secure_storage د. ثوابت مُرمَّزة يدويًا
- ما الميزة الرئيسية لـ ListView.builder؟ أ. يدعم فقط الأبناء الثابتين ب. يبني بكسل الأبناء المرئيين فقط ج. يستبدل GridView د. يعطّل التمرير
- في الأمان من القيم الفارغة، ماذا تعني String؟ أ. لا يمكن أن يكون المتغيّر null أبدًا ب. قد يكون المتغيّر null ج. المتغيّر قائمة من النصوص د. المتغيّر مُهمَل
- أي طريقة دورة حياة هي الأفضل لإنشاء المُتحكِّمات والإعداد لمرة واحدة؟ أ. build ب. dispose ج. initState د. deactivate
- ماذا يجب أن تفعل قبل استخدام BuildContext بعد await؟ أ. استدعاء setState ب. التحقق من context.mounted ج. فرض إعادة بناء د. تعطيل الأمان من القيم الفارغة
- أي طريقة HTTP تُستخدَم عادةً لإنشاء مورد جديد؟ أ. GET ب. POST ج. DELETE د. HEAD
- ماذا يخفي المستودع عادةً عن طبقة الواجهة؟ أ. ألوان السمة ب. تفاصيل مصدر البيانات (API، قاعدة بيانات، ذاكرة تخزين مؤقت) ج. مُتحكِّمات الرسوم المتحركة د. أسماء ملفات العناصر
- أي حل لإدارة الحالة مبني حول المزوّدات والأمان وقت الترجمة؟ أ. setState فقط ب. Riverpod ج. InheritedWidget الخام فقط د. CSS
- ما الغرض من dispose()؟ أ. إنشاء عناصر جديدة ب. تنظيف المُتحكِّمات والمؤقتات والاشتراكات ج. ترجمة التطبيق د. نشر الحزم
- أي عنصر هو الأنسب لتجميع حقول النموذج والتحقق منها معًا؟ أ. Container ب. Form ج. Stack د. Spacer
- ماذا يساعد ColorScheme.fromSeed على توليده؟ أ. جداول قاعدة بيانات ب. مجموعة متناسقة من ألوان السمة ج. عناوين API د. تقارير تغطية الاختبار
- أي ملف منصة يخزّن عادةً أوصاف استخدام صلاحيات iOS؟ أ. AndroidManifest.xml ب. Info.plist ج. pubspec.yaml د. build.gradle
- ما صيغة الرفع المفضّلة للتطبيقات الجديدة على جوجل بلاي؟ أ. APK فقط ب. حزمة تطبيق أندرويد (AAB) ج. IPA د. ملف ZIP لمصادر Dart
- ما السبب الشائع لـ “setState() called after dispose()”؟ أ. استخدام عناصر const ب. اكتمال استدعاءات غير متزامنة بعد إزالة العنصر ج. عدد كبير جدًا من الخطوط د. MaterialApp مفقود
- أي مما يلي هو مسؤولية طبقة النطاق في العمارة النظيفة؟ أ. رسم البكسلات ب. قواعد الأعمال والكيانات ج. تنسيق ترويسات HTTP فقط د. تخزين لقطات الشاشة
- ما الغرض الرئيسي من اختبارات الوحدة؟ أ. استبدال التصميم ب. التحقق من الأصناف/الدوال الفردية بمعزل ج. نشر التطبيق د. إنشاء الرسوم المتحركة
- لماذا ننقل تحليل JSON الثقيل للحمولات الكبيرة إلى عملية متوازية؟ أ. العمليات المتوازية مطلوبة من المترجم ب. لتجنّب حجب خيط الواجهة ج. لأن JSON غير قانوني على العملية الرئيسية د. لتقليل حجم الصورة
- أي عنصر ينشئ انتقالًا بعنصر مشترك بين المسارات؟ أ. AnimatedContainer ب. Hero ج. Placeholder د. Divider
- ماذا يسمح به الإصدار الدلالي 1.4.2^ عمومًا؟ أ. أي إصدار بما في ذلك 2.0.0 ب. إصدارات متوافقة >=1.4.2 <2.0.0 ج. فقط الإصدار 1.4.2 بالضبط د. فقط الإصدارات التجريبية المسبقة
- أي ممارسة تساعد على منع إعادة البناء غير الضرورية؟ أ. استخدام عناصر const وتضييق نطاق الحالة ب. استدعاء setState بأكبر قدر ممكن ج. وضع كل الحالة في كائن عام واحد د. تجنّب المفاتيح تمامًا
- ما دور قواعد أمان Firestore؟ أ. تنسيق حقول النص ب. التحكّم في من يمكنه قراءة/كتابة البيانات ج. إنشاء لقطات شاشة د. استبدال إدارة الإصدارات
- أي مما يلي مثال على التفويض بدلًا من المصادقة؟ أ. تسجيل الدخول بالبريد الإلكتروني وكلمة المرور ب. التحقق من بصمة الإصبع ج. فحص ما إذا كان بإمكان مستخدم مسجَّل دخوله حذف منشور مستخدم آخر د. إعادة تعيين كلمة مرور
- ما الافتراض الأكثر أمانًا بشأن طلبات الشبكة؟ أ. تنجح دائمًا ب. يمكن أن تفشل أو تنتهي مهلتها أو تُرجع أخطاء ج. لا تحتاج أبدًا حالات تحميل د. تستبدل التخزين المحلي بالكامل
- أي أداة هي الأكثر فائدة لتشخيص التقطّع ومشكلات إعادة البناء؟ أ. Flutter DevTools ب. عبارات print فقط ج. pubspec.yaml فقط د. App Store فقط
- ما هو الحذف الناعم؟ أ. إزالة صف بشكل دائم فورًا ب. وضع علامة على سجل بأنه محذوف مع الاحتفاظ بإمكانية الاستعادة ج. حذف مخزن المفاتيح د. إزالة حزمة
- أي نمط تنقّل هو عمومًا أفضل لعروض سطح المكتب من شريط تنقّل سفلي؟ أ. NavigationRail أو قائمة جانبية ب. PopupMenuButton فقط ج. SnackBar فقط د. CircularProgressIndicator فقط
- ما الذي يجب أن تتعامل معه عادةً شاشة احترافية قائمة على API؟ أ. بيانات النجاح فقط ب. حالات التحميل، النجاح، الفراغ، والخطأ ج. الوضع الداكن فقط د. الرسوم المتحركة فقط
- لماذا يُفضَّل هيكل المجلدات القائم على الميزات في التطبيقات الأكبر؟ أ. يخفي كل الكود في ملف واحد ب. الكود المرتبط بقدرة عمل معينة يعيش معًا ج. يلغي الحاجة إلى الاختبارات د. يعطّل إعادة التحميل السريع
- ما الخطر الرئيسي لتضمين مفتاح API سري مباشرة في عميل فلاتر؟ أ. بناء أسرع ب. يمكن استخراج المفتاح من التطبيق ج. رسوم متحركة أفضل د. موافقة تلقائية من المتجر
- أي حزمة تُستخدَم عادةً للتخزين الآمن بالمفتاح-القيمة على الجهاز؟ أ. shared_preferences ب. flutter_secure_storage ج. path_provider فقط د. url_launcher
- بماذا يساعد flutter analyze بشكل أساسي؟ أ. لقطات شاشة المتجر ب. التحليل الساكن لمشكلات الكود ج. توليد الأيقونات د. زيادة حجم التنزيل
- متى يكون GlobalKey هو الأنسب؟ أ. لكل عنصر في قائمة ب. عندما تحتاج مقبضًا عامًا لحالة عنصر أو سياقه ج. لاستبدال كل ValueKeys د. لتنسيق النص
- ما الغرض من verify(…) عند استخدام المحاكاة (mocks) في الاختبارات؟ أ. تنسيق العناصر ب. تأكيد أن طريقة استُدعيت كما هو متوقَّع ج. بناء ملفات APK للإصدار د. إنشاء سمات لونية
- أي مما يلي متطلب شائع لإصدار الإنتاج؟ أ. سياسة خصوصية عند جمع بيانات المستخدم ب. بناءات تصحيح فقط ج. بدون إدارة إصدارات د. كلمات مرور مُرمَّزة يدويًا
- ما الغرض الرئيسي من CI (التكامل المستمر)؟ أ. تصميم الواجهة تلقائيًا ب. تشغيل فحوصات/اختبارات تلقائيًا عند تغييرات الكود ج. استبدال المطورين د. إزالة الحاجة إلى العمارة
- أي نهج للرسوم المتحركة يُحرِّك تغييرات الخصائص تلقائيًا بأقل قدر من الكود الروتيني؟ أ. AnimationController الصريح فقط ب. الرسوم المتحركة الضمنية مثل AnimatedContainer ج. العمليات المتوازية د. MethodChannels
- ماذا يجب أن تفعل عندما تُرفض الصلاحية بشكل دائم؟ أ. تعطيل التطبيق ب. توجيه المستخدم إلى إعدادات التطبيق وتوفير بديل ج. تجاهل الميزة بصمت للأبد دون تفسير د. تخزين الرموز بنص عادي
- في تطبيق فلاتر بعمارة نظيفة، في أي اتجاه يجب أن تشير التبعيات؟ أ. النطاق يعتمد على العرض ب. النطاق يعتمد على البيانات والعرض ج. العرض والبيانات يعتمدان داخليًا على تجريدات النطاق د. كل شيء يعتمد على العناصر
- ما الغرض الأساسي من المفاتيح في القوائم القابلة لإعادة الترتيب؟ أ. تحسين تباين الألوان ب. مساعدة فلاتر على الحفاظ على هوية العنصر ومطابقتها بشكل صحيح ج. توليد مفاتيح API د. توقيع تطبيقات أندرويد
- أي مما يلي خاصية جيدة لرسالة التزام (commit)؟ أ. غامضة وغير ذات صلة ب. وصف واضح لسبب التغيير ج. تحتوي على أسرار د. كلمة واحدة فقط دائمًا
- ما الغرض من التصفّح بالصفحات أو التمرير اللانهائي؟ أ. تحميل كل السجلات الممكنة دفعة واحدة ب. تحميل البيانات في صفحات قابلة للإدارة من أجل الأداء وتجربة المستخدم ج. تعطيل البحث د. استبدال المصادقة
- أي مما يلي الأنسب لتفضيل منطقي بسيط لمرة واحدة؟ أ. مخطط SQLite كامل ب. SharedPreferences ج. Cloud Firestore فقط د. محرك ++C مخصص
- ما السبب المتكرر لمشكلات تثبيت/تحديث إصدار أندرويد؟ أ. استخدام عناصر const ب. عدم زيادة رمز الإصدار أو توقيع غير صحيح ج. استخدام Material 3 د. كتابة الاختبارات
- لماذا نكتب اختبارات الواجهة؟ أ. للتحقق من سلوك الواجهة والتفاعلات بطريقة آلية سريعة ب. لاستبدال كل اختبارات الوحدة ج. لتوليد مخازن المفاتيح د. لإنشاء أيقونات التطبيق
- ما الاستجابة الموصى بها عندما تُعيد API الحالة 401 Unauthorized؟ أ. تجاهلها ب. التعامل مع إعادة المصادقة/تدفّق تسجيل الخروج بشكل مناسب ج. تحليلها كـ JSON ناجح د. تعطيل كاميرا الجهاز
- أي مما يلي يحسّن إمكانية الوصول؟ أ. تسميات دلالية ذات معنى وتباين قابل للقراءة ب. أيقونات صغيرة جدًا بدون تسميات فقط ج. أحجام نص مُرمَّزة يدويًا تتجاهل تحجيم النظام د. إزالة ترتيب التركيز
- ما الذي يصف بشكل أفضل مشروع فلاتر بجودة معرض أعمال؟ أ. لقطات شاشة فقط دون هيكل كود ب. تطبيق كامل بعمارة، ومعالجة أخطاء، واختبارات، وملف README واضح ج. ملف main.dart واحد بكل شيء مختلطًا معًا د. تطبيق يعمل فقط في وضع التصحيح
مفتاح الإجابات (الجزء 1)
1-ب، 2-ب، 3-أ، 4-ج، 5-ب، 6-ب، 7-ج، 8-ب، 9-ب، 10-ب، 11-ب، 12-ب، 13-ب، 14-ب، 15-ب، 16-ب، 17-ب، 18-ب، 19-ب، 20-ب، 21-ب، 22-ب، 23-أ، 24-ب، 25-ج، 26-ب، 27-أ، 28-ب، 29-أ، 30-ب، 31-ب، 32-ب، 33-ب، 34-ب، 35-ب، 36-ب، 37-أ، 38-ب، 39-ب، 40-ب، 41-ج، 42-ب، 43-ب، 44-ب، 45-ب، 46-ب، 47-أ، 48-ب، 49-أ، 50-ب
الجزء 2 — أسئلة تقنية (20)
أجب بوضوح وبشكل كامل.
- اشرح العلاقة بين Widget و Element و RenderObject.
- متى تختار Riverpod على setState العادي؟
- صف الفرق بين رموز الوصول ورموز التحديث.
- كيف يحسّن ListView.builder الأداء مقارنة بـ ListView عادي؟
- ما المشكلة التي تحلّها ترحيلات قاعدة البيانات؟
- اشرح قاعدة التبعية في العمارة النظيفة.
- لماذا من المهم التخلّص من المُتحكِّمات وإلغاء الاشتراكات؟
- ما الفروق الرئيسية بين اختبارات الوحدة واختبارات الواجهة؟
- كيف تصمم حالات التحميل والفراغ والخطأ لشاشة قائمة منتجات؟
- لماذا يجب ألا يكون التحقق من جانب العميل أبدًا هو التحقق الوحيد؟
- اشرح متى تستخدم مُنشِئات const ولماذا هي مهمة.
- ما الغرض من واجهة المستودع في طبقة النطاق؟
- كيف تتعامل بأمان مع التنقّل بعد استدعاء تسجيل دخول غير متزامن؟
- صف استراتيجية أساسية للعمل دون اتصال أولًا لتطبيق ملاحظات.
- ما الأسباب الشائعة لفشل توقيع الكود في بناء iOS؟
- كيف يمكن لأدوات الذكاء الاصطناعي مساعدة مطوّر فلاتر دون استبدال حكمه الهندسي؟
- اشرح الفرق بين الرسوم المتحركة الضمنية والصريحة بأمثلة.
- لماذا يُعد التنظيم القائم على الميزات مفيدًا في قواعد كود فلاتر متوسطة إلى كبيرة؟
- ما الفحوصات التي يجب أن يشغّلها خط أنابيب CI لمشروع فلاتر نموذجي؟
- اذكر خمسة عناصر لجاهزية الإنتاج قبل التقديم إلى متجر تطبيقات.
الجزء 3 — مشكلات تصحيح (10)
لكل مشكلة: حدّد الخلل، اشرح السبب، وقدّم نهجًا مصحَّحًا.
- setState() called after dispose() بعد استدعاء شبكة.
- استخدام Navigator.of(context) بعد await دون فحوصات mounted.
- قائمة طويلة مبنية بـ ListView(children: products.map(…).toList()) وتتلعثم.
- رموز مُخزَّنة في SharedPreferences وتبدو قابلة للاستخراج.
- نموذج يتحقق من الصحة فقط من جانب العميل ولا يزال الخادم يستقبل بيانات غير صالحة.
- TextEditingController يُنشأ في State لكن لا يُتخلَّص منه أبدًا.
- تعارض حزمة يظهر بعد إضافة تبعية جديدة؛ أُضيف dependency_overrides بشكل دائم.
- بناء إصدار يتصرّف بشكل مختلف عن بناء التصحيح ويتعطّل عند البدء.
- مسار نجاح API يُفترض متى ما وردت أي استجابة، متجاهلًا رموز الحالة.
- شاشة كبيرة تُعاد بناؤها بالكامل عندما تتغيّر قيمة نصية صغيرة واحدة فقط.
الجزء 4 — تحديات برمجية (5)
- متوسط: ابنِ نموذج تسجيل مُتحقَّق منه بالاسم والبريد الإلكتروني وكلمة المرور وتأكيد كلمة المرور. أظهر حالتَي تحميل وخطأ.
- متوسط: نفّذ مستودعًا + مُخطِرًا/Cubit يجلب قائمة عناصر، ويعرض حالات تحميل/خطأ/بيانات، ويدعم إعادة المحاولة.
- متقدم: أنشئ ميزة ملاحظات بتخزين محلي، وحذف ناعم، واستعادة، وبحث.
- متقدم: نفّذ تدفّق مصادقة باستعادة جلسة آمنة وشاشة رئيسية محمية.
- متقدم: حسّن شبكة منتجات متقطّعة (janky): حوّلها إلى builder، أضف const حيث أمكن، وحسّن تحميل الصور. اشرح التحسينات.
الجزء 5 — مشروع التخرّج الاحترافي
موجز المشروع: صمّم وابنِ تطبيق فلاتر بأسلوب إنتاجي من اختيارك (مدير مهام/مشاريع، متتبّع مالي شخصي، قارئ محتوى بدعم دون اتصال، تطبيق إعلانات سوق، متتبّع عادات).
القدرات المطلوبة: المصادقة؛ ميزة/ميزات CRUD أساسية بمنطق أعمال ذي معنى؛ دمج API و/أو Firebase؛ تخزين محلي/دعم دون اتصال؛ بحث وتصفية؛ حالات تحميل/فراغ/خطأ مناسبة؛ إدارة جلسة آمنة؛ عمارة نظيفة أو ما يعادلها؛ إدارة حالة تتجاوز setState؛ واجهة متجاوبة؛ اختبارات وحدة واختبارَي واجهة على الأقل؛ ملف README بتعليمات الإعداد والعمارة والتشغيل؛ وعي ببناء الإصدار.
متطلبات العمارة: هيكل قائم على الميزات، نمط مستودع، حقن تبعية، حدود واضحة بين النطاق والبيانات والعرض.
متطلبات الأمان: تخزين آمن للرموز، HTTPS فقط، تحقق من صحة الإدخال، مسارات محمية.
متطلبات الاختبار: اختبارات وحدة للمستودعات/المُخطِرات/المنطق الأساسي، اختبارات واجهة للشاشات الرئيسية، تعليمات لتشغيل الاختبارات.
متطلبات الأداء: قوائم/شبكات فعّالة، معالجة صور مدروسة، لا تقطّع واضح في التدفقات الأساسية.
متطلبات النشر: إدارة إصدارات مضبوطة، تعليمات بناء الإصدار، خطة نشر للمتجر أو الويب، ملاحظة سياسة خصوصية إذا جُمعت بيانات المستخدم.
سلّم تقييم مشروع التخرّج (100 نقطة)
إرشادات النجاح
- 90–100: مستوى احترافي ممتاز
- 75–89: أساس احترافي قوي/متين
- 60–74: يحتاج تحسينًا في مجالات رئيسية
- أقل من 60: لم يصل بعد إلى معيار الشهادة
بيان الشهادة النهائي
إتمام هذا الاختبار بنجاح يُثبت أنه بإمكانك تصميم وتطوير واختبار وتحسين وتأمين والتحضير لنشر تطبيق فلاتر احترافي باستخدام ممارسات Dart وفلاتر الحديثة.
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |





