دليل Flutter الشامل

من البداية إلى نشر أول تطبيق - رحلة تعلم تفاعلية بالعربية

جدول المحتويات

🌟 مقدمة: ما هو Flutter؟

اكتشف قوة Flutter في تطوير التطبيقات عبر المنصات

Flutter هو إطار عمل مفتوح المصدر من Google يتيح لك تطوير تطبيقات جميلة وسريعة لأنظمة التشغيل المحمولة وسطح المكتب والويب من قاعدة كود واحدة.

تطوير سريع

Hot Reload يتيح رؤية التغييرات فورياً دون إعادة تشغيل التطبيق

🚀

أداء عالي

يُترجم إلى كود أصلي مما يضمن أداءً سريعاً ومتميزاً

🎨

تصميم جميل

مكتبة غنية من الويدجتس لإنشاء واجهات مستخدم رائعة

📱

منصة واحدة

كود واحد يعمل على Android وiOS وWeb وDesktop

نصيحة: Flutter يوفر عليك الوقت والجهد بدلاً من تعلم Swift لـ iOS وKotlin لـ Android، تحتاج فقط لتعلم Dart وFlutter!

لماذا Flutter؟

في عالم تطوير التطبيقات اليوم، المطورون يواجهون تحدي إنشاء تطبيقات لمنصات متعددة. Flutter يحل هذه المشكلة بطريقة أنيقة وفعالة.

إحصائية مهمة: أكثر من 150,000 تطبيق في متجر Google Play تم بناؤه بـ Flutter، بما في ذلك تطبيقات من Google وAlibaba وToyota.

🏗️ الهيكل المعماري لـ Flutter

فهم كيفية عمل Flutter من الداخل

Flutter مبني على بنية طبقية متطورة تضمن الأداء العالي والمرونة في التطوير.

مخطط البنية المعمارية

📱 تطبيقك (Your App)

الكود الذي تكتبه بـ Dart

⬇️

🎯 Flutter Framework

Widgets, Rendering, Animation

⬇️

⚙️ Flutter Engine

Skia, Dart Runtime, Platform Channels

⬇️

📟 Platform

Android, iOS, Web, Desktop

المكونات الأساسية:

🎯 Dart Platform

  • لغة البرمجة: Dart هي لغة محسّنة خصيصاً لبناء واجهات المستخدم
  • الآلة الافتراضية: Dart VM سريعة مع دعم JIT وAOT compilation
  • المكتبات: مجموعة شاملة من المكتبات الأساسية

⚙️ Flutter Engine

  • محرك الرسم: يستخدم مكتبة Skia للرسم عالي الأداء
  • Platform Channels: للتواصل مع APIs الأصلية
  • Plugin Architecture: نظام إضافات مرن وقابل للتوسع

🎨 Framework Layer

  • Widgets: كل شيء في Flutter هو Widget
  • Rendering: نظام تحويل Widgets إلى pixels
  • Animation: نظام رسوم متحركة قوي ومرن
مهم: فهم هذه البنية سيساعدك في كتابة كود أكثر كفاءة وحل المشاكل بسرعة.

🛠️ إعداد بيئة التطوير

تحضير أدوات العمل وبيئة التطوير

قبل أن نبدأ بالبرمجة، نحتاج لإعداد بيئة التطوير بالطريقة الصحيحة.

تحميل Flutter SDK

انتقل إلى الموقع الرسمي وحمّل SDK المناسب لنظام التشغيل:

flutter.dev → تحميل SDK → استخراج الملفات

أضف مسار flutter/bin إلى متغير البيئة PATH

إعداد المحرر

VS Code (مُوصى به للمبتدئين):

  • تثبيت إضافة Flutter
  • تثبيت إضافة Dart
  • تفعيل Hot Reload

Android Studio (للمحترفين):

  • تثبيت Flutter plugin
  • تثبيت Dart plugin
  • إعداد المُحاكيات

فحص الإعداد

تأكد من أن كل شيء يعمل بشكل صحيح:

flutter doctor

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

نصائح الإعداد:
  • تأكد من تحديث Android SDK إلى أحدث إصدار
  • لتطوير iOS تحتاج macOS وXcode
  • استخدم Git لإدارة إصدارات المشروع

متطلبات النظام:

🖥️

Windows

Windows 10 أو أحدث
8 GB RAM
2.8 GB مساحة فارغة

🍎

macOS

macOS 10.14 أو أحدث
8 GB RAM
2.8 GB مساحة فارغة

🐧

Linux

64-bit distribution
8 GB RAM
2.8 GB مساحة فارغة

🚀 أول تطبيق Flutter

إنشاء وتشغيل مشروعك الأول

الآن بعد إعداد البيئة، حان الوقت لإنشاء أول تطبيق Flutter!

إنشاء المشروع

flutter create my_first_app
cd my_first_app
flutter run

ستحصل على تطبيق عداد بسيط يعمل على جهازك أو المُحاكي.

🔧 هيكل المشروع:

my_first_app/
  ├── lib/ # الكود الرئيسي لتطبيق Flutter
     ├── main.dart # نقطة البداية - تستدعي runApp() وتحدد أول واجهة تظهر
     ├── controllers/ # ملفات منطق التحكم وإدارة الحالة (مثل GetX أو Provider)
        ├── auth_controller.dart # يتحكم بعملية تسجيل الدخول/الخروج
        └── theme_controller.dart # يتحكم في تغيير الثيم (فاتح/داكن)
     ├── models/ # نماذج البيانات المستخدمة في التطبيق
        └── user_model.dart # تمثيل كائن المستخدم (اسم، إيميل، ...)
     ├── services/ # الطبقة التي تتواصل مع API أو قواعد البيانات
        ├── api_service.dart # جلب البيانات من الإنترنت
        └── local_storage_service.dart # حفظ البيانات محلياً مثل SharedPreferences
     ├── utils/ # دوال وأدوات مساعدة عامة
        └── validators.dart # التحقق من صحة الحقول (مثل البريد، كلمة السر...)
     ├── widgets/ # عناصر واجهة قابلة لإعادة الاستخدام
        ├── custom_button.dart # زر مخصص يستخدم في أكثر من شاشة
        └── user_card.dart # بطاقة تعرض بيانات المستخدم بشكل مختصر
     ├── screens/ # الشاشات الرئيسية في التطبيق
        ├── login_screen.dart # شاشة تسجيل الدخول
        └── home_screen.dart # الشاشة الرئيسية بعد تسجيل الدخول
     └── views/ # واجهات العرض المرتبطة بكل شاشة (تصميم الواجهة فقط)
        └── home_view.dart # تصميم واجهة الشاشة الرئيسية دون منطق
  ├── android/ # كود النظام الأصلي لـ Android (إعدادات وصلاحيات وتخصيص)
  ├── ios/ # كود النظام الأصلي لـ iOS
  ├── test/ # اختبارات الوحدة
  └── pubspec.yaml # ملف إعدادات المشروع وإضافة الحزم الخارجية

مثال على main.dart

              
              // استيراد الحزمة الأساسية
              import 'package:flutter/material.dart';

              // نقطة البداية في التطبيق
              void main() {
                runApp(MyApp());
              }

              class MyApp extends StatelessWidget {
                @override
                Widget build(BuildContext context) {
                  return MaterialApp(
                    title: 'تطبيقي الأول',
                    home: Scaffold(
                      appBar: AppBar(
                        title: Text('مرحبا Flutter!'),
                      ),
                      body: Center(
                        child: Text('أهلاً بك في Flutter بالعربية!'),
                      ),
                    ),
                  );
                }
              }
              
            
نصيحة: جرب تعديل نص "أهلاً بك في Flutter بالعربية!" وشاهد النتيجة فوراً باستخدام Hot Reload!

🧩 فهم الـ Widgets

اللبنات الأساسية لبناء واجهات المستخدم في Flutter

في Flutter، كل شيء هو Widget. الـ Widgets هي اللبنات الأساسية لتكوين الواجهات.

مفهوم أساسي: فكّر في Widgets مثل قطع Lego - كل واحدة تمثل مكوناً بصرياً أو وظيفياً في التطبيق.

🖼️ Image (عرض الصور)

لعرض صورة من الإنترنت أو من الأصول المحلية.


      // صورة من الإنترنت
      Image.network(
        'https://example.com/image.jpg',
        width: 200,
        height: 150,
        fit: BoxFit.cover,
      )

      // صورة من الأصول
      Image.asset(
        'assets/images/logo.png',
        width: 150,
        height: 100,
        fit: BoxFit.contain,
      )
            

🌅 FadeInImage

لعرض صورة مؤقتة أثناء تحميل صورة من الإنترنت.


      FadeInImage.assetNetwork(
        placeholder: 'assets/placeholder.png',
        image: 'https://example.com/image.jpg',
        fit: BoxFit.cover,
      )
            

📐 Flexible و Expanded

للسماح للعناصر بالتوسع داخل صف أو عمود.


      Row(
        children: [
          Expanded(child: Text('نص يأخذ المساحة المتبقية')),
          Flexible(
            flex: 2,
            child: Image.asset('assets/image.png'),
          ),
        ],
      )
            

🖱️ InkWell & GestureDetector

لإضافة استجابة عند الضغط على العناصر.


      // InkWell مع تأثير الضغط (Ripple)
      InkWell(
        onTap: () => print('تم الضغط'),
        child: Container(
          padding: EdgeInsets.all(12),
          child: Text('اضغط هنا'),
        ),
      )

      // GestureDetector بدون تأثير بصري
      GestureDetector(
        onTap: () {
          print('تم النقر');
        },
        child: Icon(Icons.touch_app),
      )
            

📜 ListView

لعرض قائمة قابلة للتمرير عموديًا أو أفقيًا.


      ListView(
        children: [
          ListTile(title: Text('عنصر 1')),
          ListTile(title: Text('عنصر 2')),
        ],
      )

      // أو باستخدام builder
      ListView.builder(
        itemCount: 10,
        itemBuilder: (context, index) {
          return ListTile(title: Text('عنصر \$index'));
        },
      )
            

🔲 GridView

لعرض عناصر في شبكة متعددة الأعمدة.


      GridView.count(
        crossAxisCount: 2,
        children: [
          Container(color: Colors.red, height: 100),
          Container(color: Colors.green, height: 100),
        ],
      )
            

🔁 Wrap

لعرض العناصر بطريقة تتدفق تلقائيًا إلى السطر التالي.


      Wrap(
        spacing: 8,
        runSpacing: 4,
        children: [
          Chip(label: Text('Flutter')),
          Chip(label: Text('Widgets')),
          Chip(label: Text('Responsive')),
        ],
      )
            

📌 Align & Positioned

لضبط موضع العنصر داخل المساحة أو داخل Stack.


      // باستخدام Align
      Align(
        alignment: Alignment.bottomRight,
        child: Text('محاذاة إلى اليمين'),
      )

      // باستخدام Positioned داخل Stack
      Stack(
        children: [
          Container(color: Colors.blue, height: 200),
          Positioned(
            right: 10,
            top: 10,
            child: Icon(Icons.star),
          ),
        ],
      )
            
💡 تلميح: استخدم Flexible و Wrap مع ListView و GridView لبناء واجهات ديناميكية ومتجاوبة.

🎨 تخصيص المظهر (App Theme) في Flutter

كل ما تحتاجه لإنشاء مظهر احترافي لتطبيقك، يدعم الوضع الليلي والنهاري، مع إمكانية التبديل اليدوي وتخزين التفضيلات

في تطبيقات Flutter، الثيم (Theme) هو المسؤول عن تحديد الشكل العام للتطبيق من حيث الألوان والخطوط، وتصميم الأزرار، والنصوص، وخلفيات الشاشات، وغيرها. من خلال ويدجت ThemeData داخل MaterialApp، يمكنك إنشاء مظهر موحّد للتطبيق بالكامل والتحكم فيه بدقة.

🔧 ThemeData - تخصيص مظهر التطبيق

الثيم الأساسي يبدأ بإنشاء كائن من نوع ThemeData، والذي يحتوي على خصائص مثل: primarySwatch (لون أساسي)، textTheme (أنماط الخطوط)، buttonTheme (مظهر الأزرار)، وغيرها. عند وضعه داخل MaterialApp، فإنه يطبق على كامل التطبيق.


      MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.indigo,
          scaffoldBackgroundColor: Colors.grey[100],
          textTheme: const TextTheme(
            bodyMedium: TextStyle(fontSize: 16),
            titleLarge: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 20,
              color: Colors.indigo,
            ),
          ),
          elevatedButtonTheme: ElevatedButtonThemeData(
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.indigo,
              foregroundColor: Colors.white,
            ),
          ),
        ),
        home: const HomeScreen(),
      )
            

🌙 ThemeMode - الوضع الليلي والنهاري

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


      MaterialApp(
        theme: ThemeData.light(),        // الوضع الفاتح
        darkTheme: ThemeData.dark(),     // الوضع الليلي
        themeMode: ThemeMode.system,     // يتبع إعدادات النظام
      )
            

💡 تخصيص عالمي للألوان والخطوط

يمكنك استخدام ColorScheme و fontFamily لتطبيق مظهر متناسق وحديث، خصوصًا مع دعم Material 3 الذي يوفر تجربة تصميم أكثر مرونة وحداثة.


      ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
        fontFamily: 'Cairo',
      )
            

🌓 التبديل اليدوي بين الوضع الفاتح والداكن (GetX + GetStorage)

أحيانًا ترغب في إعطاء المستخدم حرية اختيار الثيم بنفسه، وليس فقط اتباع إعدادات النظام. لهذا نستخدم GetX لإدارة الحالة وتغيير الثيم في الوقت الحقيقي، وGetStorage لحفظ هذا التفضيل.

📦 1. إنشاء ThemeController

نقوم بإنشاء كلاس يحتوي على منطق التبديل والحفظ واسترجاع حالة الثيم.


      class ThemeController extends GetxController {
        final _box = GetStorage();
        final _key = 'isDarkMode';

        ThemeMode get theme => _loadTheme() ? ThemeMode.dark : ThemeMode.light;

        bool _loadTheme() => _box.read(_key) ?? false;

        void saveTheme(bool isDarkMode) => _box.write(_key, isDarkMode);

        void toggleTheme() {
          final isDark = !_loadTheme();
          saveTheme(isDark);
          Get.changeThemeMode(isDark ? ThemeMode.dark : ThemeMode.light);
        }
      }
            

🚀 2. استخدامه في main.dart

نقوم بتهيئة GetStorage، ثم نمرر الثيم إلى GetMaterialApp.


      void main() async {
        await GetStorage.init();
        runApp(MyApp());
      }

      class MyApp extends StatelessWidget {
        final ThemeController themeController = Get.put(ThemeController());

        @override
        Widget build(BuildContext context) {
          return GetMaterialApp(
            theme: ThemeData.light(),
            darkTheme: ThemeData.dark(),
            themeMode: themeController.theme,
            home: const HomeScreen(),
          );
        }
      }
            

🖱️ 3. واجهة التطبيق وزر التبديل

نضيف زرًا بسيطًا في AppBar لتبديل الثيم في الوقت الحقيقي.


      class HomeScreen extends StatelessWidget {
        const HomeScreen({super.key});

        @override
        Widget build(BuildContext context) {
          final ThemeController themeController = Get.find();

          return Scaffold(
            appBar: AppBar(
              title: const Text('تبديل الثيم'),
              actions: [
                IconButton(
                  icon: const Icon(Icons.brightness_6),
                  onPressed: themeController.toggleTheme,
                ),
              ],
            ),
            body: const Center(
              child: Text('اضغط على الأيقونة لتغيير الثيم'),
            ),
          );
        }
      }
            
📌 ملخص:
  • استخدم ThemeData لتوحيد المظهر العام لتطبيقك.
  • اعتمد على ThemeMode.system لدعم إعدادات النظام.
  • استخدم GetX + GetStorage لتمكين المستخدم من اختيار الثيم وتخزينه.
  • التبديل يتم فورياً بدون إعادة تشغيل، بفضل Get.changeThemeMode.

📜 التمرير في Flutter: الحلول المختلفة

كيف تتعامل مع المحتوى الذي يتجاوز حجم الشاشة؟

في تطوير التطبيقات، غالباً ما تحتاج إلى عرض محتوى يتجاوز حجم الشاشة. Flutter يوفر لك عدة ويدجتس للتحكم في التمرير بمرونة وأداء عالي.

1. SingleChildScrollView

الويدجت الأبسط للتمرير، يسمح بتمرير محتوى واحد (طفل واحد) يمكن أن يكون عموداً أو صفاً أو أي عنصر آخر.

SingleChildScrollView(
                child: Column(
                  children: [
                    // عناصر متعددة
                  ],
                ),
              )

متى تستخدمه؟ إذا كان المحتوى غير كبير جداً وعدد العناصر محدود.

2. ListView

يستخدم لعرض قائمة قابلة للتمرير من العناصر، وهو الأكثر شيوعاً عند التعامل مع قوائم طويلة.

ListView(
                children: [
                  ListTile(title: Text('العنصر 1')),
                  ListTile(title: Text('العنصر 2')),
                  // المزيد من العناصر
                ],
              )

أو لإنشاء قائمة ديناميكية مع عناصر غير محدودة:

ListView.builder(
                itemCount: items.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(items[index]),
                  );
                },
              )

متى تستخدمه؟ عند وجود قائمة متغيرة الطول، أو تحتاج لأداء أفضل مع عدد كبير من العناصر.

3. GridView

لإنشاء شبكة قابلة للتمرير، مفيدة لعرض العناصر في شكل جدول (مثل الصور).

GridView.count(
                  crossAxisCount: 2, // عدد الأعمدة
                  children: [
                    // عناصر الشبكة
                  ],
                )

4. CustomScrollView و Slivers

لتحكم دقيق ومتقدم في التمرير باستخدام Slivers (وحدات بناء التمرير القابلة للتخصيص).

مثال بسيط:

CustomScrollView(
                slivers: [
                  SliverAppBar(
                    expandedHeight: 150,
                    flexibleSpace: FlexibleSpaceBar(title: Text('عنوان')),
                    pinned: true,
                  ),
                  SliverList(
                    delegate: SliverChildBuilderDelegate(
                      (context, index) => ListTile(title: Text('العنصر $index')),
                      childCount: 20,
                    ),
                  ),
                ],
              )

متى تستخدمه؟ عندما تحتاج إلى تصميمات معقدة أو دمج عدة أنواع من المحتوى مع تمرير موحد.

5. التمرير اللانهائي (Infinite Scroll)

التمرير اللانهائي يُستخدم لعرض قائمة طويلة من العناصر يتم تحميلها تدريجياً من مصدر خارجي مثل API. هذا يوفر أداء أفضل وتجربة مستخدم سلسة.

الفكرة الأساسية هي مراقبة نهاية القائمة، وعند الوصول إليها يتم طلب المزيد من البيانات وتحميلها.

class InfiniteListView extends StatefulWidget {
            @override
            _InfiniteListViewState createState() => _InfiniteListViewState();
          }

          class _InfiniteListViewState extends State<InfiniteListView> {
            final ScrollController _scrollController = ScrollController();
            List _products = [];
            bool _isLoading = false;
            int _page = 0;

            @override
            void initState() {
              super.initState();
              _fetchProducts();
              _scrollController.addListener(() {
                if (_scrollController.position.pixels ==
                    _scrollController.position.maxScrollExtent) {
                  _fetchProducts();
                }
              });
            }

            Future _fetchProducts() async {
              if (_isLoading) return;

              setState(() => _isLoading = true);

              // محاكاة جلب بيانات من API (استبدل بالكود الحقيقي)
              await Future.delayed(Duration(seconds: 2));
              List newProducts =
                  List.generate(20, (index) => 'منتج رقم ${_page * 20 + index + 1}');

              setState(() {
                _page++;
                _products.addAll(newProducts);
                _isLoading = false;
              });
            }

            @override
            Widget build(BuildContext context) {
              return ListView.builder(
                controller: _scrollController,
                itemCount: _products.length + 1,
                itemBuilder: (context, index) {
                  if (index == _products.length) {
                    return _isLoading
                        ? Padding(
                            padding: EdgeInsets.all(16),
                            child: Center(child: CircularProgressIndicator()),
                          )
                        : SizedBox.shrink();
                  }
                  return ListTile(title: Text(_products[index]));
                },
              );
            }

            @override
            void dispose() {
              _scrollController.dispose();
              super.dispose();
            }
          }
          

ملاحظات:

  • استخدم ScrollController لمراقبة التمرير.
  • عند الوصول إلى نهاية القائمة، استدعِ دالة لتحميل المزيد.
  • أظهر مؤشر تحميل أثناء جلب البيانات.
  • يمكن استبدال الجزء الخاص بمحاكاة تحميل البيانات بالنداء الحقيقي لـ API باستخدام http أو Dio.

نصائح مهمة:

  • تجنب وضع أكثر من ويدجت تمرير داخل بعضها (مثل SingleChildScrollView داخل ListView) لتفادي مشاكل الأداء والتمرير.
  • عند استخدام ListView داخل Column، ضعها داخل Expanded أو حدد لها ارتفاعاً ثابتاً.
  • استخدم physics: NeverScrollableScrollPhysics() لتعطيل التمرير لويدجت معينة في حالة التداخل.
معلومة تقنية: معظم ويدجتس التمرير في Flutter تستخدم ScrollController للتحكم في سلوك التمرير.

🔄 إدارة الحالة (State Management)

فهم شامل لكيفية التحكم في البيانات وتحديث واجهة المستخدم في Flutter

إدارة الحالة هي إحدى أهم ركائز تطوير تطبيقات Flutter الحديثة. فهي تحدد كيفية مشاركة البيانات بين الواجهات، وكيفية تحديث الشاشة عند حدوث تغييرات. Flutter يوفر أدوات متنوعة لإدارة الحالة، تختلف في التعقيد والاستخدام حسب حجم المشروع.

🟢 setState (الطريقة التقليدية)

تُستخدم setState داخل StatefulWidget لتحديث الواجهة عند تغيير البيانات. تصلح للمشاريع الصغيرة أو الشاشات ذات منطق بسيط.


      class MyWidget extends StatefulWidget {
        @override
        _MyWidgetState createState() => _MyWidgetState();
      }

      class _MyWidgetState extends State<MyWidget> {
        int count = 0;

        void increment() {
          setState(() {
            count++;
          });
        }

        @override
        Widget build(BuildContext context) {
          return Column(
            children: [
              Text('العدد: \$count'),
              ElevatedButton(
                onPressed: increment,
                child: Text('زيادة'),
              ),
            ],
          );
        }
      }
            

🟡 Provider (حل متوسط يعتمد على ChangeNotifier)

Provider هو من أوائل أدوات إدارة الحالة المدعومة رسميًا من Google. يستخدم نموذج البرمجة التفاعلية ChangeNotifier لإشعار الواجهات عند حدوث تغييرات في البيانات.


      class CounterModel with ChangeNotifier {
        int count = 0;

        void increment() {
          count++;
          notifyListeners(); // إشعار الواجهة بالتغيير
        }
      }
            

🟠 GetX (سريع، خفيف، يشمل التوجيه والتخزين)

GetX هي مكتبة قوية لإدارة الحالة، والتوجيه، والترجمة، والتخزين المحلي. توفر طريقة بسيطة وسريعة لتحديث الواجهة عبر استخدام متغيرات "مراقبة" (Rx).


      class CounterController extends GetxController {
        RxInt count = 0.obs;

        void increment() => count++;
      }
            

🧠 Riverpod (الجيل المتطور من Provider)

Riverpod هو بديل متطور لـ Provider. يتميز بمرونة أكثر وأمان في كتابة الكود. يسمح لك بفصل المنطق عن الواجهة بشكل واضح، مما يسهل الاختبار والصيانة.

📦 GetStorage (تخزين البيانات محليًا)

GetStorage يُستخدم لحفظ القيم مثل إعدادات الوضع الليلي، بدون حاجة لقواعد بيانات أو ملفات خارجية.


      final box = GetStorage();
      box.write('isDarkMode', true);
      bool isDark = box.read('isDarkMode') ?? false;
            

⏳ Futures و async/await (البرمجة غير المتزامنة)

تُستخدم للتعامل مع العمليات التي تستغرق وقتًا مثل الاتصال بالإنترنت أو انتظار قاعدة بيانات.


      Future<String> fetchData() async {
        await Future.delayed(Duration(seconds: 2));
        return 'تم تحميل البيانات';
      }
            

📡 Streams (بيانات مستمرة بمرور الوقت)

تُستخدم Streams لتتبع بيانات تحدث بشكل دوري مثل الرسائل الجديدة أو تغير الموقع.


      Stream<int> counterStream() async* {
        for (int i = 0; i < 5; i++) {
          await Future.delayed(Duration(seconds: 1));
          yield i;
        }
      }
            
📊 مقارنة شاملة لأدوات إدارة الحالة:
  • setState: مباشر وسهل، لكنه غير مناسب للمشاريع الكبيرة.
  • Provider: قوي وسهل، وموصى به للمشاريع المتوسطة.
  • GetX: شامل وسريع، ويدعم التوجيه والتخزين.
  • Riverpod: قوي ومرن، مثالي لفِرق التطوير والمشاريع المعقدة.
  • Bloc: الأكثر تنظيمًا، لكنه يتطلب مجهودًا أكبر وهيكلية معقدة.

🧾 التعامل مع البيانات والخدمات في Flutter

من نموذج البيانات المحلي إلى استهلاك Web APIs والتخزين المحلي

في أي تطبيق حقيقي، يجب عليك تحديد **نموذج البيانات (Model)** الذي يعبّر عن الكائنات في تطبيقك—مثل المستخدمين أو المنتجات—ثم التفكير في كيفية تخزين هذه البيانات محليًا مؤقتًا أو دائمًا، وأخيرًا في كيفية جلبها أو إرسالها إلى الخادم عبر **Web APIs**. سنغطي في هذا القسم:

  1. إنشاء نموذج البيانات مع `fromJson` و `toJson`
  2. التخزين المحلي باستخدام SharedPreferences
  3. مفهوم الـ API وأنواع الطلبات (GET، POST، PUT، DELETE)
  4. جلب البيانات من API وعرضها في الواجهة باستخدام FutureBuilder
  5. إرسال البيانات وتحديثها عبر POST و PUT
  6. نصائح لاختبار الـ API باستخدام Postman

📁 1. نموذج البيانات (Data Model)

أول خطوة هي إنشاء نموذج Dart يعكس بنية البيانات القادمة من الخادم أو البيانات التي تريد إرسالها. هذا النموذج يجب أن يحتوي على دوال fromJson لتحويل JSON إلى كائن Dart، وtoJson لتحويل كائن Dart إلى JSON عند الإرسال.

📄 ملف: lib/models/user_model.dart

      class User {
        final int? id;
        final String name;
        final String email;

        User({
          this.id,
          required this.name,
          required this.email,
        });

        /// تحويل JSON إلى كائن Dart
        factory User.fromJson(Map<String, dynamic> json) {
          return User(
            id: json['id'],
            name: json['name'],
            email: json['email'],
          );
        }

        /// تحويل كائن Dart إلى JSON (للإرسال إلى الخادم)
        Map<String, dynamic> toJson() {
          return {
            'name': name,
            'email': email,
          };
        }
      }
            

💾 2. التخزين المحلي باستخدام SharedPreferences

في بعض الأحيان ترغب في حفظ تفضيلات المستخدم أو بيانات صغيرة مثل الثيم أو حالة تسجيل الدخول محليًا دون الحاجة لقاعدة بيانات كاملة. SharedPreferences يسمح بتخزين أزواج مفتاح-قيمة (String) بطرق متزامنة وبسيطة.

📄 ملف: lib/services/local_storage.dart

      import 'package:shared_preferences/shared_preferences.dart';

      class LocalStorage {
        /// حفظ قيمة نصية بالمفتاح المحدد
        static Future<void> saveData(String key, String value) async {
          final prefs = await SharedPreferences.getInstance();
          await prefs.setString(key, value);
        }

        /// استرجاع قيمة نصية بناءً على المفتاح
        static Future<String?> getData(String key) async {
          final prefs = await SharedPreferences.getInstance();
          return prefs.getString(key);
        }

        /// مسح جميع البيانات المخزنة
        static Future<void> clearAll() async {
          final prefs = await SharedPreferences.getInstance();
          await prefs.clear();
        }
      }
            

✅ مثال توضيحي لحفظ واسترجاع الثيم:


      // حفظ الثيم (مثلاً: 'dark' أو 'light')
      await LocalStorage.saveData('theme', 'dark');

      // استرجاع الثيم عند تشغيل التطبيق
      final storedTheme = await LocalStorage.getData('theme');
      print('الثيم المخزن هو: $storedTheme');
            

🌐 3. ما هو الـ API وكيفية استهلاكه

API (واجهة برمجة التطبيقات) هو واجهة تتيح لتطبيق Flutter الخاص بك التواصل مع خادم خارجي. غالبًا ما تكون الـ APIs مبنية كنقاط نهاية (Endpoints) على خادم يستخدم بروتوكولات HTTP، وتستقبل الطلبات في صورة GET أو POST أو PUT أو DELETE، وتُرجع استجابات بصيغة JSON.

📦 حالات استخدام (Use Cases)

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

🛠️ هيكلية نموذجية للـ API

عندما ترسل طلبًا GET إلى نقطة النهاية /products مثلاً:


      GET https://api.example.com/products

      Response:
      [
        { "id": 1, "name": "Laptop", "price": 1299.99 },
        { "id": 2, "name": "Mouse", "price": 19.99 }
      ]
            

📲 4. جلب البيانات من API (GET)

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

📄 ملف: lib/services/api_service.dart

      import 'package:http/http.dart' as http;
      import 'dart:convert';
      import '../models/user_model.dart';

      class ApiService {
        static const String _baseUrl = 'https://api.example.com';

        /// جلب قائمة المستخدمين من الخادم
        static Future<List<User>> fetchUsers() async {
          final response = await http.get(Uri.parse('$_baseUrl/users'));

          if (response.statusCode == 200) {
            List<dynamic> data = jsonDecode(response.body);
            return data.map((json) => User.fromJson(json)).toList();
          } else {
            throw Exception('فشل في تحميل المستخدمين: ${response.statusCode}');
          }
        }
      }
            

📦 عرض البيانات باستخدام FutureBuilder

بعد جلب البيانات، نستخدم FutureBuilder لعرضها في واجهة المستخدم:


      FutureBuilder<List<User>>(
        future: ApiService.fetchUsers(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('خطأ: \${snapshot.error}'));
          } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return Center(child: Text('لا توجد بيانات'));
          } else {
            final users = snapshot.data!;
            return ListView.builder(
              itemCount: users.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(users[index].name),
                  subtitle: Text(users[index].email),
                );
              },
            );
          }
        },
      )
            

📝 5. إرسال وتحديث البيانات (POST / PUT)

بعد أن أصبح نموذج البيانات جاهزًا، يمكننا إرسال كائن User جديد إلى الخادم أو تحديث أحد المستخدمين الموجودين باستخدام POST و PUT.

📤 5.1 إرسال مستخدم جديد (POST)

نستخدم دالة http.post مع تحويل الكائن إلى JSON عبر user.toJson():


      Future<void> createUser(User user) async {
        final response = await http.post(
          Uri.parse('https://api.example.com/users'),
          headers: {'Content-Type': 'application/json'},
          body: jsonEncode(user.toJson()),
        );

        if (response.statusCode == 201) {
          print('تمت إضافة المستخدم بنجاح');
        } else {
          throw Exception('فشل في إرسال المستخدم: ${response.statusCode}');
        }
      }
            

🔄 5.2 تعديل مستخدم موجود (PUT)

لتحديث بيانات مستخدم محدد، نمرر معرف المستخدم في الـ URL:


      Future<void> updateUser(User user) async {
        final response = await http.put(
          Uri.parse('https://api.example.com/users/\${user.id}'),
          headers: {'Content-Type': 'application/json'},
          body: jsonEncode(user.toJson()),
        );

        if (response.statusCode == 200) {
          print('تم تحديث بيانات المستخدم');
        } else {
          throw Exception('فشل في التحديث: ${response.statusCode}');
        }
      }
            

❌ 5.3 حذف مستخدم (DELETE)

إذا أردت حذف سجل مستخدم من الخادم، تستخدم http.delete مع معرف المستخدم:


      Future<void> deleteUser(int id) async {
        final response = await http.delete(
          Uri.parse('https://api.example.com/users/\$id'),
        );

        if (response.statusCode == 200) {
          print('تم حذف المستخدم');
        } else {
          throw Exception('فشل في الحذف: ${response.statusCode}');
        }
      }
            

🧪 6. اختبار الـ API عبر Postman

قبل ربط الـ API بتطبيقك، من الأفضل التأكد من عمله باستخدام Postman:

  1. افتح Postman وأنشئ طلب جديد (GET/POST/PUT/DELETE).
  2. أدخل رابط الـ API الصحيح، مثل https://api.example.com/users.
  3. إذا كان الطلب يتطلب Body (مثل POST/PUT)، انتقل إلى تبويب Body واختر raw ثم JSON وأدخل البيانات.
  4. اضغط زر "Send" وراقب الاستجابة (Response).
  5. تأكد من كود الحالة (Status Code) والرد JSON قبل دمج الكود في تطبيقك.

📌 ملاحظات هامة:
  • تأكد من إعداد الــheaders بالشكل الصحيح: 'Content-Type': 'application/json'.
  • استخدم print(response.body) في Flutter أثناء التطوير لرؤية JSON المستلم.
  • عند استخدام بيانات كبيرة أو متعددة الاستهلاك، فكر في إضافة التخزين المؤقت (caching).

📌 خلاصة واستنتاجات

  • ابدأ دائمًا بتعريف نموذج البيانات (Model) المناسب لكائناتك.
  • التخزين المحلي عبر SharedPreferences مناسب للبيانات البسيطة مثل الثيم وتفضيلات المستخدم.
  • استخدم مكتبة http لجلب البيانات (GET) وعرضها في الواجهة باستخدام FutureBuilder.
  • لإرسال أو تحديث البيانات، اعتمد على http.post و http.put مع تحويل النموذج إلى JSON.
  • قبل دمج أي نقطة نهاية (Endpoint)، اختبرها أولاً باستخدام Postman لضمان عملها بصورة صحيحة.
  • يمكنك إضافة مزيد من المرونة باستخدام Provider أو GetX لإدارة التحميل وحالة الخطأ وتنظيم الكود.

🎨 تصميم الواجهات (UI Design)

بناء واجهات جميلة، متجاوبة وسهلة الاستخدام

واجهة المستخدم هي الانطباع الأول الذي يحصل عليه المستخدم عن تطبيقك. Flutter يتيح تصميم واجهات مذهلة باستخدام مكونات مرنة مثل Column، Row، Stack، وContainer. ولكن الأهم من ذلك هو جعل التصميم متجاوبًا بحيث يعمل بشكل ممتاز على جميع الأجهزة سواء الهاتف أو التابلت أو الويب.

🖥️ التصميم المتجاوب باستخدام LayoutBuilder و MediaQuery

تُستخدم LayoutBuilder و MediaQuery لتحديد أبعاد الشاشة وتخصيص التصميم بناءً عليها.


      LayoutBuilder(
        builder: (context, constraints) {
          if (constraints.maxWidth > 600) {
            return DesktopLayout(); // تصميم خاص بالشاشات الواسعة
          } else {
            return MobileLayout(); // تصميم مخصص للموبايل
          }
        },
      )
            

📏 MediaQuery (اكتشاف حجم الشاشة)

مثال لاستخدام MediaQuery لتعديل حجم عنصر بناءً على عرض الجهاز:


      double screenWidth = MediaQuery.of(context).size.width;

      Container(
        width: screenWidth * 0.9, // 90% من عرض الشاشة
        height: 200,
        color: Colors.amber,
      )
            

📐 Flexible & Expanded

تُستخدم للتحكم في توزيع المساحة داخل Row وColumn بطريقة مرنة:


      Row(
        children: [
          Expanded(child: Container(color: Colors.red, height: 100)),
          Expanded(child: Container(color: Colors.blue, height: 100)),
        ],
      )
            
💡 تلميحة: لتجربة تصميم واجهتك على أكثر من حجم شاشة، استخدم محاكي الأجهزة في DevTools أو Web View.

🔍 اختبار التطبيق (Testing)

ضمان جودة وموثوقية الكود وسلوك البرنامج المتوقع

اختبار الكود يُعدّ خطوة مهمة جدًا في تطوير التطبيقات، فهو يساعدك على التأكد من أن الوظائف تعمل كما يجب ويمنع حدوث أعطال عند التحديثات. Flutter يدعم ثلاثة أنواع رئيسية من الاختبارات:

  • Unit Test: لاختبار وظائف صغيرة مثل العمليات الحسابية أو المعالجات.
  • Widget Test: لاختبار ويدجت معين (مثل زر أو نص) بطريقة منعزلة.
  • Integration Test: لاختبار سير المستخدم الكامل عبر عدة شاشات.

🧪 مثال على Unit Test (اختبار وظيفة)

نختبر إذا كانت دالة increment() تعمل كما هو متوقع:


      class Counter {
        int value = 0;

        void increment() => value++;
      }
            

💡 ملف الاختبار: test/counter_test.dart


      import 'package:flutter_test/flutter_test.dart';
      import '../lib/counter.dart';

      void main() {
        test('اختبار زيادة العداد', () {
          final counter = Counter();
          counter.increment();
          expect(counter.value, 1);
        });
      }
            
نصائح لاختبار فعّال:
  • اكتب اختبارات لكل حالة محتملة، بما فيها الأخطاء.
  • استخدم أدوات مثل mockito لمحاكاة API.
  • شغّل الأمر flutter test دوريًا أثناء التطوير.

🚀 نشر التطبيق

إيصال تطبيقك للمستخدمين على منصات متعددة

بعد الانتهاء من تطوير التطبيق واختباره، تأتي خطوة نشر التطبيق ليصبح متاحًا للمستخدمين. Flutter يوفر أدوات سهلة لبناء التطبيق وإعداده للنشر على Android وiOS، مع خطوات واضحة لضمان إصدار مستقر.

🤖 بناء التطبيق لنظام Android

لبناء حزمة نشر Android، نستخدم الأمر التالي:

flutter build appbundle

هذا الأمر ينشئ ملف .aab (Android App Bundle) وهو الشكل الموصى به للنشر على متجر Google Play. الحزمة تحتوي على جميع الموارد والكود الضروري ليتم تحميل التطبيق بشكل فعال حسب نوع جهاز المستخدم.

📋 خطوات مهمة بعد البناء لـ Android:

  • توقيع التطبيق باستخدام مفتاح التوقيع الخاص بك لضمان الأصالة.
  • رفع ملف .aab على Google Play Console.
  • إعداد صفحة التطبيق، الصور الترويجية، ووصف التطبيق في المتجر.
  • اختبار التطبيق باستخدام نسخة تجريبية قبل النشر النهائي.

🍎 بناء التطبيق لنظام iOS

لبناء ملف نشر iOS (ملف IPA) نستخدم الأمر:

flutter build ipa

هذا يقوم بإنشاء ملف IPA جاهز للنشر على متجر App Store. يجب أن تكون قد قمت بضبط ملف Signing & Capabilities في Xcode وربط التطبيق بـ Apple Developer Account.

📋 خطوات مهمة بعد البناء لـ iOS:

  • فتح مشروع iOS في Xcode لضبط إعدادات التوقيع.
  • رفع ملف IPA إلى App Store Connect باستخدام Xcode أو أداة Transporter.
  • إعداد معلومات التطبيق، الصور، والأسعار على App Store Connect.
  • مراجعة التطبيق من قِبل Apple قبل الموافقة على النشر.
💡 نصيحة: تأكد دائمًا من اختبار التطبيق على أجهزة فعلية قبل رفعه للمتاجر، وحاول استخدام النسخ التجريبية (Beta) مثل Google Play Beta و TestFlight للاختبار الجماعي.