توثيق فلاتر

الدليل الشامل لتطوير تطبيقات فلاتر

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

مقدمة في فلاتر

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

مميزات فلاتر

  • تطوير موحد لعدة منصات (Android، iOS، Web، Desktop)
  • أداء ممتاز بفضل محرك الرسوم المضمن (Skia)
  • Hot Reload لتطوير سريع ومرن
  • مكتبة غنية من الويدجات
  • سهولة تخصيص واجهة المستخدم
  • دعم متزايد من المجتمع

تطبيقات فلاتر الشهيرة

  • Google Ads
  • Alibaba
  • eBay
  • Tencent
  • BMW
  • The New York Times

كيف يعمل فلاتر؟

على عكس معظم أطر عمل تطوير التطبيقات الأخرى، فلاتر لا يستخدم مكونات واجهة المستخدم الأصلية للمنصة، بل يرسم كل بكسل على شاشة الجهاز. يوفر هذا النهج الفريد عدة مزايا:

آلية عمل فلاتر
معمارية فلاتر

معمارية فلاتر بشكل مبسط - المصدر: توثيق فلاتر الرسمي

للحصول على أداء متميز، فلاتر يتجنب استخدام جسر JavaScript (كما في React Native) ويجمع التطبيق مباشرة إلى التعليمات البرمجية الأصلية.

تثبيت وإعداد فلاتر

قبل البدء في تطوير تطبيقات فلاتر، تحتاج إلى تثبيت مجموعة أدوات فلاتر SDK وإعداد بيئة التطوير الخاصة بك. اتبع الخطوات التالية:

1. متطلبات النظام

للعمل مع فلاتر، ستحتاج إلى:

  • نظام تشغيل: Windows (64-bit) أو macOS أو Linux
  • مساحة: 2.5 GB على الأقل (لا تشمل مساحة بيئة التطوير المتكاملة/المحرر)
  • أدوات: Git للتحكم بالإصدارات

2. تحميل فلاتر SDK

قم بتحميل أحدث إصدار من فلاتر SDK من الموقع الرسمي:

اختر نظام التشغيل المناسب واتبع التعليمات الخاصة بالتثبيت.

3. إعداد متغيرات البيئة

أضف مجلد flutter/bin إلى متغير PATH لنظام التشغيل:

  1. افتح لوحة التحكم > النظام والأمان > النظام > إعدادات النظام المتقدمة
  2. انقر على "متغيرات البيئة"
  3. في قسم "متغيرات النظام"، ابحث عن متغير PATH وقم بتحديده، ثم انقر على "تعديل"
  4. انقر على "جديد" وأضف المسار الكامل لمجلد flutter\bin
  5. انقر على "موافق" لحفظ التغييرات

أضف السطر التالي إلى ملف $HOME/.bash_profile أو $HOME/.zshrc:

export PATH="$PATH:[مسار مجلد فلاتر]/flutter/bin"

بعد ذلك، قم بتنفيذ الأمر التالي لتحديث المتغيرات:

source $HOME/.bash_profile

أضف السطر التالي إلى ملف $HOME/.bashrc:

export PATH="$PATH:[مسار مجلد فلاتر]/flutter/bin"

بعد ذلك، قم بتنفيذ الأمر التالي لتحديث المتغيرات:

source $HOME/.bashrc

4. التحقق من متطلبات فلاتر

قم بتشغيل الأمر التالي للتحقق من وجود أي متطلبات إضافية:

flutter doctor

سيقوم هذا الأمر بفحص النظام وإظهار قائمة بالبرامج المطلوبة لتطوير تطبيقات فلاتر. قم بتثبيت أي برامج مطلوبة مذكورة في النتائج.

5. إعداد محرر الأكواد

يمكنك استخدام أحد محررات الأكواد المدعومة:

Android Studio / IntelliJ IDEA

  1. قم بتثبيت Android Studio
  2. قم بتثبيت إضافة Dart و Flutter من Plugins
  3. أعد تشغيل IDE

Visual Studio Code

  1. قم بتثبيت VS Code
  2. قم بتثبيت إضافة Dart و Flutter
  3. اضغط F1 وابحث عن "Flutter: New Project"

جاهز للبدء!

بمجرد إكمال هذه الخطوات، ستكون جاهزًا لإنشاء مشروع فلاتر الأول الخاص بك.

أساسيات فلاتر

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

إنشاء مشروع جديد

لإنشاء مشروع فلاتر جديد، قم بتنفيذ الأمر التالي في الطرفية:

flutter create my_app

سيقوم هذا الأمر بإنشاء مشروع جديد باسم "my_app". للدخول إلى المشروع وتشغيله:

cd my_app
flutter run

هيكل مشروع فلاتر

عند إنشاء مشروع فلاتر جديد، سيتم إنشاء بنية ملفات أساسية:

my_app/
  ├── .dart_tool/         # أدوات دارت
  ├── .idea/              # ملفات IntelliJ IDEA
  ├── android/            # مشروع Android
  ├── ios/                # مشروع iOS
  ├── lib/                # ملفات Dart الرئيسية
  │   └── main.dart       # نقطة دخول التطبيق
  ├── test/               # اختبارات الوحدة والتكامل
  ├── web/                # ملفات تطبيق الويب
  ├── .gitignore          # ملفات مستثناة من git
  ├── .metadata           # بيانات وصفية للمشروع
  ├── pubspec.yaml        # تبعيات المشروع والإعدادات
  └── README.md           # وثائق المشروع
                                

الملف pubspec.yaml يحتوي على معلومات المشروع والتبعيات (المكتبات الخارجية). الملف main.dart هو نقطة البداية للتطبيق.

البرنامج الأول - Hello World

إليك مثال بسيط لتطبيق "Hello World" في فلاتر:

main.dart
import 'package:flutter/material.dart';

                                void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'مرحباً بالعالم',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('تطبيق فلاتر الأول'),
        centerTitle: true,
      ),
      body: const Center(
        child: Text(
          'مرحباً بالعالم!',
          style: TextStyle(fontSize: 24),
        ),
      ),
    );
  }
}
مثال Hello World بسيط
Dart

لنشرح المثال السابق:

main()
نقطة الدخول للتطبيق، حيث يتم استدعاء runApp() مع الويدجت الرئيسي.
MaterialApp
ويدجت يوفر المكونات الأساسية لتطبيق يتبع تصميم Material Design من جوجل.
Scaffold
يوفر هيكل صفحة تطبيق Material الأساسي (شريط علوي، جسم، إلخ).
StatelessWidget
ويدجت لا يحتوي على حالة داخلية قابلة للتغيير (ثابت).

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

1. كل شيء ويدجت

في فلاتر، كل شيء هو ويدجت: الأزرار، النصوص، الصور، التخطيطات، وحتى التطبيق نفسه. تعمل الويدجات كلبنات بناء لواجهة المستخدم.

2. الويدجات الثابتة والمتغيرة

  • StatelessWidget: ويدجت لا تحتفظ بحالة داخلية ولا تتغير بمرور الوقت.
  • StatefulWidget: ويدجت يمكنها تغيير حالتها الداخلية أثناء عمر التطبيق.

3. بناء الواجهة (build)

كل ويدجت تحتوي على دالة build() تحدد كيفية عرض الويدجت على الشاشة. هذه الدالة يتم استدعاؤها عندما يحتاج فلاتر لإعادة رسم الويدجت.

4. Hot Reload و Hot Restart

فلاتر يوفر ميزتين مهمتين لتسريع دورة التطوير:

  • Hot Reload: يعيد بناء واجهة المستخدم مع الحفاظ على الحالة الحالية.
  • Hot Restart: يعيد تشغيل التطبيق بالكامل ويعيد تعيين الحالة.

الويدجات الأساسية

فلاتر يوفر مجموعة غنية من الويدجات المدمجة لبناء واجهات المستخدم. سنستعرض هنا أهم الويدجات الأساسية التي ستحتاجها في معظم التطبيقات.

ويدجات النص والإدخال

Text

لعرض سلسلة نصية بتنسيق معين.

Text(
  'مرحباً بالعالم',
  style: TextStyle(
    fontSize: 18,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
)

TextField

حقل إدخال للنصوص.

TextField(
  decoration: InputDecoration(
    labelText: 'أدخل اسمك',
    border: OutlineInputBorder(),
  ),
  onChanged: (value) {
    print('تم الإدخال: $value');
  },
)

RichText

لعرض نص بتنسيقات مختلفة ضمن نفس النص.

RichText(
  text: TextSpan(
    style: TextStyle(color: Colors.black),
    children: [
      TextSpan(text: 'مرحباً '),
      TextSpan(
        text: 'بالعالم',
        style: TextStyle(
          fontWeight: FontWeight.bold,
          color: Colors.blue,
        ),
      ),
    ],
  ),
)

Form

لتجميع حقول إدخال مع إمكانية التحقق من الصحة.

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        validator: (value) {
          if (value == null || value.isEmpty) {
            return 'يرجى إدخال نص';
          }
          return null;
        },
      ),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            // تنفيذ الإجراء
          }
        },
        child: Text('إرسال'),
      ),
    ],
  ),
)

ويدجات الأزرار والتفاعل

ElevatedButton

زر بارز مرتفع.

ElevatedButton(
  onPressed: () {
    print('تم النقر');
  },
  child: Text('زر'),
)

TextButton

زر نصي بدون خلفية.

TextButton(
  onPressed: () {},
  child: Text('زر نصي'),
)

IconButton

زر يحتوي على أيقونة فقط.

IconButton(
  icon: Icon(Icons.favorite),
  onPressed: () {},
)

GestureDetector

للكشف عن الإيماءات المختلفة (النقر، السحب، إلخ).

GestureDetector(
  onTap: () {
    print('تم النقر');
  },
  onDoubleTap: () {
    print('نقر مزدوج');
  },
  child: Container(
    height: 100,
    width: 100,
    color: Colors.blue,
    child: Center(
      child: Text('انقر هنا'),
    ),
  ),
)

FloatingActionButton

زر عائم دائري بارز.

FloatingActionButton(
  onPressed: () {},
  child: Icon(Icons.add),
  backgroundColor: Colors.green,
)

ويدجات العرض والصور

Image

لعرض الصور من مصادر مختلفة.

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

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

Icon

لعرض الأيقونات.

Icon(
  Icons.star,
  color: Colors.yellow,
  size: 24,
)

ويدجات المؤشرات والتحميل

CircularProgressIndicator

مؤشر تقدم دائري.

CircularProgressIndicator(
  value: 0.7, // قيمة التقدم (0.0 إلى 1.0)
  backgroundColor: Colors.grey[200],
  color: Colors.blue,
)

LinearProgressIndicator

مؤشر تقدم خطي.

LinearProgressIndicator(
  value: 0.5, // قيمة التقدم (0.0 إلى 1.0)
  backgroundColor: Colors.grey[200],
  color: Colors.green,
)

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

تخطيطات الواجهة

تخطيطات الواجهة (Layouts) هي ويدجات تساعد في تنظيم وترتيب الويدجات الأخرى داخلها وفقاً لأنماط معينة. تعتبر أساسية في بناء واجهات المستخدم.

التخطيطات الأساسية

Container

ويدجت متعدد الاستخدامات يمكن استخدامه لإضافة الأنماط والتنسيق والحشوات والهوامش.

Container(
  margin: EdgeInsets.all(10),
  padding: EdgeInsets.all(15),
  width: 200,
  height: 100,
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(10),
    boxShadow: [
      BoxShadow(
        color: Colors.black26,
        blurRadius: 5,
        offset: Offset(0, 2),
      ),
    ],
  ),
  child: Text(
    'مربع منسق',
    style: TextStyle(color: Colors.white),
  ),
)

Row

يرتب الويدجات أفقياً جنباً إلى جنب.

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Icon(Icons.star, size: 30),
    Text('نجمة'),
    ElevatedButton(
      onPressed: () {},
      child: Text('زر'),
    ),
  ],
)

Column

يرتب الويدجات عمودياً من أعلى إلى أسفل.

Column(
  mainAxisSize: MainAxisSize.min,
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Text('العنوان', style: TextStyle(fontSize: 20)),
    SizedBox(height: 10),
    Text('وصف تفصيلي هنا...'),
    SizedBox(height: 15),
    ElevatedButton(
      onPressed: () {},
      child: Text('تأكيد'),
    ),
  ],
)

Stack

يضع الويدجات فوق بعضها البعض، مثل طبقات.

Stack(
  children: [
    Container(
      width: 200,
      height: 200,
      color: Colors.blue,
    ),
    Positioned(
      top: 10,
      right: 10,
      child: Icon(
        Icons.close,
        color: Colors.white,
      ),
    ),
    Center(
      child: Text(
        'محتوى',
        style: TextStyle(color: Colors.white),
      ),
    ),
  ],
)

Expanded و Flexible

يسمحان للويدجات بملء المساحة المتبقية في Row أو Column.

Row(
  children: [
    Container(width: 50, color: Colors.red),
    Expanded(
      flex: 2,
      child: Container(color: Colors.green),
    ),
    Expanded(
      flex: 1,
      child: Container(color: Colors.blue),
    ),
  ],
)

Wrap

يشبه Row لكنه ينقل الويدجات إلى السطر التالي إذا لم تكن هناك مساحة كافية.

Wrap(
  spacing: 8.0, // المسافة بين العناصر أفقياً
  runSpacing: 8.0, // المسافة بين الصفوف
  children: [
    Chip(label: Text('فلاتر')),
    Chip(label: Text('دارت')),
    Chip(label: Text('واجهة المستخدم')),
    Chip(label: Text('تطوير')),
    Chip(label: Text('تطبيقات')),
  ],
)

مثال تطبيقي على التخطيطات

لنرى مثالاً على كيفية استخدام التخطيطات المختلفة معاً لبناء واجهة بطاقة منتج:

بطاقة منتج
Card(
  elevation: 4,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(12),
  ),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    mainAxisSize: MainAxisSize.min,
    children: [
      // صورة المنتج
      Stack(
        children: [
          Image.network(
            'https://example.com/product.jpg',
            height: 200,
            width: double.infinity,
            fit: BoxFit.cover,
          ),
          Positioned(
            top: 10,
            right: 10,
            child: Container(
              padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
              decoration: BoxDecoration(
                color: Colors.red,
                borderRadius: BorderRadius.circular(20),
              ),
              child: Text(
                'خصم 20%',
                style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
              ),
            ),
          ),
        ],
      ),
      
      // معلومات المنتج
      Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'عنوان المنتج هنا',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(height: 8),
            Text(
              'وصف مختصر للمنتج يظهر هنا ويمكن أن يمتد لعدة أسطر.',
              style: TextStyle(color: Colors.grey[600]),
            ),
            SizedBox(height: 16),
            
            // السعر والتقييم
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  '99 ريال',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.green,
                  ),
                ),
                Row(
                  children: [
                    Text('4.5'),
                    Icon(Icons.star, color: Colors.amber, size: 16),
                  ],
                ),
              ],
            ),
            
            Divider(height: 24),
            
            // أزرار التفاعل
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: () {},
                    style: ElevatedButton.styleFrom(
                      primary: Colors.green,
                    ),
                    child: Text('إضافة للسلة'),
                  ),
                ),
                SizedBox(width: 8),
                IconButton(
                  icon: Icon(Icons.favorite_border),
                  onPressed: () {},
                ),
              ],
            ),
          ],
        ),
      ),
    ],
  ),
)

عند تصميم واجهات في فلاتر، فكر دائماً بالطريقة التي تتداخل بها التخطيطات. يمكن وضع Row داخل Column، أو Stack داخل Container، وهكذا.

إدارة الحالة

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

أنواع الحالة في فلاتر

الحالة المحلية (Local State)

تخص ويدجت واحد فقط، مثل حالة زر أو حقل نص. تُدار عادة باستخدام StatefulWidget و setState().

الحالة العامة (Global State)

تُشارك بين عدة ويدجات في التطبيق، مثل بيانات المستخدم أو سلة التسوق. تحتاج إلى حلول إدارة حالة أكثر تقدماً.

إدارة الحالة المحلية باستخدام StatefulWidget

الطريقة الأساسية لإدارة الحالة المحلية في فلاتر هي استخدام StatefulWidget:

مثال بسيط على StatefulWidget
class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State {
  int _counter = 0;
  
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('عداد بسيط')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'لقد ضغطت على الزر هذا العدد من المرات:',
            ),
            Text(
              '$_counter',
              style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'زيادة',
        child: Icon(Icons.add),
      ),
    );
  }
}

في المثال السابق:

  • نقوم بإنشاء StatefulWidget يسمى CounterPage
  • له حالة _CounterPageState تحتوي على متغير _counter
  • عند النقر على الزر العائم، يتم استدعاء دالة _incrementCounter() التي تستخدم setState() لتغيير قيمة _counter
  • استدعاء setState() يخبر فلاتر بإعادة بناء الويدجت لتعكس التغييرات

حلول إدارة الحالة المتقدمة

بينما يعمل setState بشكل جيد للحالات البسيطة، تحتاج التطبيقات الأكبر إلى حلول أكثر تنظيمًا. إليك بعض أشهر الحلول:

Provider

حل بسيط لإدارة الحالة مدعوم من فريق فلاتر. يعتمد على نمط الوصاية (InheritedWidget) ويسهل مشاركة وتحديث الحالة.

// إنشاء نموذج البيانات
class CounterModel extends ChangeNotifier {
  int _counter = 0;
  int get counter => _counter;
  
  void increment() {
    _counter++;
    notifyListeners();
  }
}

// استخدام Provider في الشجرة
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterModel(),
      child: MyApp(),
    ),
  );
}

// الاستماع للتغييرات في أي ويدجت
class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      '${context.watch().counter}',
      style: TextStyle(fontSize: 36),
    );
  }
}

// تحديث الحالة من أي ويدجت
class IncrementButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        context.read().increment();
      },
      child: Text('زيادة'),
    );
  }
}

Bloc / Cubit

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

// تعريف Cubit (أبسط من Bloc)
class CounterCubit extends Cubit {
  CounterCubit() : super(0);
  
  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

// استخدام في التطبيق
BlocProvider(
  create: (context) => CounterCubit(),
  child: CounterScreen(),
);

// عرض الحالة
BlocBuilder(
  builder: (context, count) {
    return Text('$count',
      style: TextStyle(fontSize: 36),
    );
  },
);

// تحديث الحالة
ElevatedButton(
  onPressed: () {
    context.read().increment();
  },
  child: Text('زيادة'),
)

GetX

إطار عمل خفيف وقوي يوفر إدارة حالة، تنقل، وحقن اعتماديات وأدوات متنوعة.

// وحدة التحكم
class CounterController extends GetxController {
  var count = 0.obs;
  
  void increment() => count++;
}

// استخدام في الويدجت
class HomeView extends StatelessWidget {
  final CounterController c = Get.put(CounterController());
  
  @override
  Widget build(context) {
    return Scaffold(
      body: Center(
        child: Obx(() => Text("${c.count}")),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: c.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Riverpod

تطوير أكثر تقدمًا لمفهوم Provider، يعالج بعض القيود ويوفر إمكانات إضافية.

// تعريف Provider
final counterProvider = StateNotifierProvider((ref) {
  return Counter();
});

class Counter extends StateNotifier {
  Counter() : super(0);
  
  void increment() => state = state + 1;
}

// استخدام في الويدجت
class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    
    return Column(
      children: [
        Text('$count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: Text('زيادة'),
        ),
      ],
    );
  }
}

اختيار نظام إدارة الحالة المناسب يعتمد على حجم التطبيق ومتطلباته. للمشاريع الصغيرة، setState أو Provider يكون كافيًا. للمشاريع الكبيرة، قد تفضل Bloc أو Riverpod للحصول على هيكل أكثر صلابة.

التعامل مع البيانات

معظم التطبيقات تتعامل مع البيانات بطريقة أو بأخرى: قراءتها، حفظها، مزامنتها. سنلقي نظرة على طرق التعامل مع البيانات في فلاتر.

تخزين البيانات محليًا

Shared Preferences

تخزين بسيط للبيانات البدائية على شكل مفتاح-قيمة.

// تثبيت المكتبة في pubspec.yaml
// shared_preferences: ^2.0.13

// حفظ البيانات
Future saveUser(String username) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('username', username);
  await prefs.setBool('isLoggedIn', true);
}

// قراءة البيانات
Future loadUser() async {
  final prefs = await SharedPreferences.getInstance();
  final username = prefs.getString('username') ?? '';
  final isLoggedIn = prefs.getBool('isLoggedIn') ?? false;
}

Hive

قاعدة بيانات NoSQL خفيفة وسريعة مكتوبة بلغة دارت.

// تثبيت المكتبة في pubspec.yaml
// hive: ^2.0.5
// hive_flutter: ^1.1.0

// في main.dart
await Hive.initFlutter();
Hive.registerAdapter(PersonAdapter());
await Hive.openBox('people');

// حفظ البيانات
final peopleBox = Hive.box('people');
peopleBox.put('key', Person('أحمد', 25));
peopleBox.add(Person('محمد', 30)); // مفتاح تلقائي

// قراءة البيانات
final person = peopleBox.get('key');
final allPeople = peopleBox.values.toList();

SQLite (مع sqflite)

قاعدة بيانات علائقية كاملة المزايا.

// تثبيت المكتبة في pubspec.yaml
// sqflite: ^2.0.2
// path: ^1.8.0

// إنشاء قاعدة البيانات والجدول
Future getDatabase() async {
  final dbPath = await getDatabasesPath();
  final path = join(dbPath, 'my_app.db');
  
  return openDatabase(
    path,
    onCreate: (db, version) {
      return db.execute(
        'CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',
      );
    },
    version: 1,
  );
}

// إدخال بيانات
Future insertUser(User user) async {
  final db = await getDatabase();
  await db.insert(
    'users',
    user.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

// قراءة البيانات
Future> getUsers() async {
  final db = await getDatabase();
  final List> maps = await db.query('users');
  
  return List.generate(maps.length, (i) {
    return User(
      id: maps[i]['id'],
      name: maps[i]['name'],
      age: maps[i]['age'],
    );
  });
}

العمل مع الملفات

// تثبيت المكتبة في pubspec.yaml
// path_provider: ^2.0.9

import 'dart:io';
import 'package:path_provider/path_provider.dart';

// الحصول على مسار الملف
Future get _localPath async {
  final directory = await getApplicationDocumentsDirectory();
  return directory.path;
}

// كتابة بيانات إلى ملف
Future writeToFile(String data) async {
  final path = await _localPath;
  final file = File('$path/data.txt');
  await file.writeAsString(data);
}

// قراءة بيانات من ملف
Future readFromFile() async {
  try {
    final path = await _localPath;
    final file = File('$path/data.txt');
    return await file.readAsString();
  } catch (e) {
    return '';
  }
}

استخدام واجهات API

تتواصل معظم التطبيقات الحديثة مع خدمات ويب خارجية من خلال واجهات API. سنتعلم كيفية إجراء طلبات HTTP وإدارة البيانات المستلمة.

طلبات HTTP باستخدام مكتبة http

// تثبيت المكتبة في pubspec.yaml
// http: ^0.13.4

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

// طلب GET
Future> fetchPosts() async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
  
  if (response.statusCode == 200) {
    final List data = jsonDecode(response.body);
    return data.map((json) => Post.fromJson(json)).toList();
  } else {
    throw Exception('فشل في تحميل المنشورات');
  }
}

// طلب POST
Future createPost(Post post) async {
  final response = await http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(post.toJson()),
  );
  
  if (response.statusCode == 201) {
    return Post.fromJson(jsonDecode(response.body));
  } else {
    throw Exception('فشل في إنشاء المنشور');
  }
}

// نموذج البيانات
class Post {
  final int userId;
  final int? id;
  final String title;
  final String body;
  
  Post({required this.userId, this.id, required this.title, required this.body});
  
  factory Post.fromJson(Map json) {
    return Post(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
  
  Map toJson() {
    return {
      'userId': userId,
      'title': title,
      'body': body,
    };
  }
}

استخدام Dio لطلبات HTTP متقدمة

// تثبيت المكتبة في pubspec.yaml
// dio: ^4.0.6

import 'package:dio/dio.dart';

class ApiService {
  final Dio _dio = Dio(BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: 5000,
    receiveTimeout: 3000,
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
  ));
  
  // إضافة معترض لتوكن التفويض
  void addAuthToken(String token) {
    _dio.interceptors.clear();
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        options.headers['Authorization'] = 'Bearer $token';
        return handler.next(options);
      },
      onError: (DioError e, handler) {
        if (e.response?.statusCode == 401) {
          // تحديث التوكن أو تسجيل الخروج
        }
        return handler.next(e);
      },
    ));
  }
  
  // طلب GET
  Future> getUsers() async {
    try {
      final response = await _dio.get('/users');
      final List data = response.data;
      return data.map((json) => User.fromJson(json)).toList();
    } catch (e) {
      throw Exception('حدث خطأ: $e');
    }
  }
  
  // طلب POST
  Future createUser(User user) async {
    try {
      final response = await _dio.post('/users', data: user.toJson());
      return User.fromJson(response.data);
    } catch (e) {
      throw Exception('حدث خطأ: $e');
    }
  }
}

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

class PostsScreen extends StatefulWidget {
  @override
  _PostsScreenState createState() => _PostsScreenState();
}

class _PostsScreenState extends State {
  late Future> futurePosts;
  
  @override
  void initState() {
    super.initState();
    futurePosts = fetchPosts();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('المنشورات')),
      body: FutureBuilder>(
        future: futurePosts,
        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 {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                final post = snapshot.data![index];
                return ListTile(
                  title: Text(post.title),
                  subtitle: Text(post.body),
                );
              },
            );
          }
        },
      ),
    );
  }
}

للتطبيقات الإنتاجية، ينصح بإنشاء طبقة خدمة منفصلة لإدارة طلبات API، مما يساعد على فصل منطق عرض البيانات عن منطق الحصول عليها، ويسهل اختبار التطبيق.

اختبار التطبيقات

الاختبار جزء أساسي من دورة تطوير البرمجيات. فلاتر يدعم ثلاثة أنواع من الاختبارات: اختبارات الوحدة، واختبارات الويدجت، واختبارات التكامل.

اختبارات الوحدة (Unit Tests)

تختبر وحدة واحدة من الكود، مثل دالة أو فئة، بعيداً عن باقي مكونات التطبيق.

// في ملف test/calculator_test.dart
import 'package:test/test.dart';
import 'package:my_app/calculator.dart';

void main() {
  group('Calculator', () {
    late Calculator calculator;
    
    setUp(() {
      calculator = Calculator();
    });
    
    test('يجب أن تجمع رقمين بشكل صحيح', () {
      expect(calculator.add(2, 3), 5);
    });
    
    test('يجب أن تطرح رقمين بشكل صحيح', () {
      expect(calculator.subtract(5, 2), 3);
    });
    
    test('يجب أن ترفع استثناء عند القسمة على صفر', () {
      expect(() => calculator.divide(10, 0), throwsA(isA()));
    });
  });
}

اختبارات الويدجت (Widget Tests)

تختبر ويدجت واحد أو مجموعة صغيرة من الويدجات.

// في ملف test/counter_widget_test.dart
                                import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter_widget.dart';

void main() {
  testWidgets('يجب أن يزيد العداد عند النقر', (WidgetTester tester) async {
    // بناء التطبيق وتشغيل دورة واحدة من الرسم
    await tester.pumpWidget(MaterialApp(
      home: CounterWidget(),
    ));

    // التحقق من وجود نص '0'
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // الضغط على زر الزيادة
    await tester.tap(find.byIcon(Icons.add));
    // انتظار انتهاء الرسوم المتحركة
    await tester.pump();

    // التحقق من تغير العداد إلى '1'
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}

اختبارات التكامل (Integration Tests)

تختبر تطبيقك بالكامل كما هو في بيئة حقيقية.

// في ملف integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('تطبيق كامل', () {
    testWidgets('التكامل المبسط', (WidgetTester tester) async {
      // تشغيل التطبيق
      app.main();
      await tester.pumpAndSettle();

      // التحقق من العناصر الأولية
      expect(find.text('العداد: 0'), findsOneWidget);

      // النقر على زر الزيادة
      await tester.tap(find.byIcon(Icons.add));
      await tester.pumpAndSettle();

      // التحقق من تحديث العداد
      expect(find.text('العداد: 1'), findsOneWidget);

      // الانتقال إلى صفحة جديدة
      await tester.tap(find.byType(ElevatedButton));
      await tester.pumpAndSettle();

      // التحقق من الصفحة الجديدة
      expect(find.text('الصفحة الثانية'), findsOneWidget);
    });
  });
}

تشغيل الاختبارات

# تشغيل اختبارات الوحدة
flutter test test/calculator_test.dart

# تشغيل اختبارات الويدجت
flutter test test/counter_widget_test.dart

# تشغيل اختبارات التكامل
flutter test integration_test

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

النشر والتوزيع

بعد الانتهاء من تطوير تطبيقك، حان الوقت لنشره وتوزيعه على المستخدمين. هناك عدة منصات وخطوات تحتاج إلى اتباعها.

إعداد التطبيق للإنتاج

تكوين معلومات التطبيق

  • تعديل اسم التطبيق والوصف في ملف pubspec.yaml
  • تحديث رمز التطبيق وشاشة البداية
  • تعديل معرف حزمة التطبيق لنظام Android وiOS
  • إضافة سياسة الخصوصية ومعلومات حقوق النشر

تحسين الأداء

  • إزالة الشيفرات غير المستخدمة
  • تقليل حجم الصور والموارد
  • تشغيل وضع الإصدار للتحقق من المشكلات المحتملة
  • اختبار التطبيق على مجموعة متنوعة من الأجهزة

بناء النسخة النهائية

# بناء ملف APK لنظام Android
flutter build apk --release

# بناء حزمة Android App Bundle (AAB)
flutter build appbundle --release

# بناء تطبيق iOS (يتطلب جهاز Mac)
flutter build ios --release

نشر التطبيق على المتاجر

Google Play Store

  1. إنشاء حساب مطور (تسجيل)
  2. دفع رسوم التسجيل (25 دولار أمريكي لمرة واحدة)
  3. إنشاء تطبيق جديد في لوحة التحكم
  4. تحميل ملف AAB أو APK
  5. إضافة لقطات شاشة، ورموز، ووصف
  6. ملء نموذج تصنيف المحتوى
  7. إعداد الاشتراك والأسعار (إن وجدت)
  8. مراجعة ونشر

Apple App Store

  1. إنشاء حساب Apple Developer (تسجيل)
  2. دفع رسوم الاشتراك السنوي (99 دولار أمريكي سنويًا)
  3. إنشاء معرِّف تطبيق في App Store Connect
  4. تكوين شهادات التوقيع والملفات التعريفية
  5. استخدام Xcode لتحميل IPA
  6. إضافة المعلومات والوسائط: لقطات شاشة، ورموز، ووصف
  7. إعداد الاشتراك والأسعار (إن وجدت)
  8. تقديم للمراجعة والانتظار للموافقة

بعد النشر

متابعة وتحسين التطبيق

  • جمع تعليقات المستخدمين والرد عليها
  • متابعة تقارير الأعطال وإصلاحها
  • دراسة تحليلات الاستخدام لتحسين تجربة المستخدم
  • تحديث التطبيق بانتظام بميزات جديدة وإصلاحات
  • تحسين تحسين متاجر التطبيقات (ASO) لزيادة الظهور

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

الخاتمة

لقد تعلمنا في هذا الدليل الشامل أساسيات تطوير تطبيقات فلاتر، بدءًا من التثبيت والإعداد، مرورًا بالويدجات والتخطيطات، وإدارة الحالة، والتنقل، والتعامل مع البيانات، وحتى اختبار ونشر التطبيق.

فلاتر هو إطار عمل متطور باستمرار، وتظهر ميزات وأدوات جديدة بانتظام. لذا، من المهم متابعة المصادر الرسمية والمجتمع للبقاء على اطلاع بأحدث التطورات والممارسات.

موارد إضافية للتعلم

نتمنى لك التوفيق في رحلة تطوير تطبيقات فلاتر الخاصة بك!