Logo

المعمل: أساسيات واجهات Flutter، التخطيطات، وتنظيم الكود

9 دقائق قراءة
شرائح الدرس
1 / 11

المعمل: أساسيات واجهة Flutter، والتخطيطات، وتنظيم الكود

عادات تصحيح الأخطاء، والصور، وبنية StatelessWidget، وعناصر التخطيط الأساسية

1. الهدف

تقدّم هذه الوثيقة المعملية دليلاً شاملاً لطلاب علوم الحاسب لفهم المفاهيم الأساسية لبناء واجهات المستخدم (UI) في Flutter. بنهاية هذا المعمل، سيكون الطالب على دراية بتقنيات تصحيح الأخطاء (Debugging)، وعرض الصور، وتنظيم الكود عبر StatelessWidget، والتخطيطات البنائية باستخدام Container، وRow، وColumn.


2. تصحيح الأخطاء والتعلّم الذاتي

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

  • تظليل الأخطاء في بيئة التطوير (IDE): تقوم بيئات التطوير الحديثة بتظليل الأخطاء النحوية أو البنائية. تمرير مؤشر الفأرة فوق الخطأ غالبًا ما يعرض تلميحًا (Tooltip) يوضّح المشكلة بدقة.
  • البحث عن الحلول: إذا لم يحل تلميح بيئة التطوير المشكلة، انسخ نص رسالة الخطأ وابحث عنها في محركات البحث أو منصات مثل StackOverflow. هذا الأسلوب يوفّر وقتًا كبيرًا ويساعدك على فهم الأخطاء الشائعة.

مثال

افترض أنك كتبت هذا الكود ورأيت خطًا أحمر تحته في بيئة التطوير:

// ERROR: Missing comma between properties
Container(
  color: Colors.blue
  width: 100, // IDE highlights this line
)

تمرير المؤشر فوق الخطأ يُظهر: Expected to find ','. الحل بسيط:

// FIXED: Added the missing comma
Container(
  color: Colors.blue,
  width: 100,
)

إذا واجهت خطأ أثناء التشغيل (Runtime Error) مثل RenderFlex overflowed by 42 pixels، انسخ هذه الرسالة وابحث عنها على الإنترنت. ستجد أنها تعني أن المحتوى أكبر من المساحة المتاحة، وعادة ما يكون الحل بلف المحتوى داخل SingleChildScrollView أو استخدام Expanded/Flexible.


3. عناصر الواجهة الأساسية (Essential UI Widgets)

3.1. الأغلفة الأساسية: MaterialApp و Scaffold

عند بناء تطبيق Flutter، تحتاج إلى عناصر واجهة أساسية لإعداد البنية المرئية:

  • MaterialApp: يُعد الغلاف الرئيسي للتطبيق بأكمله، ويوفّر التنسيق الافتراضي، والثيمات (Themes)، وإمكانيات التنقل (Navigation).
  • Scaffold: عنصر يوضع عادة داخل MaterialApp (غالبًا كشاشة رئيسية - home). يوفّر بنية قياسية للتطبيق، تتيح لك بسهولة إضافة مكونات واجهة مثل شريط التطبيق (App Bar)، وزر إجراء عائم (Floating Action Button)، والمحتوى الرئيسي (body).

مثال

import 'package:flutter/material.dart';
 
void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: Text('My First App'),
          backgroundColor: Colors.teal,
        ),
        body: Center(
          child: Text(
            'Hello, Flutter!',
            style: TextStyle(fontSize: 24),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {},
          child: Icon(Icons.add),
        ),
      ),
    ),
  );
}

هذا الكود ينشئ تطبيقًا أساسيًا يحتوي على:

  • AppBar أعلى الشاشة بعنوان "My First App"
  • body يعرض نصًا في المنتصف
  • FloatingActionButton في الزاوية السفلى اليمنى

3.2. إضافة الصور

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

  1. صور من الشبكة (Image.network): تُستخدم لتحميل الصور مباشرة من الإنترنت عبر رابط (URL). يمكنك أيضًا تحديد خصائص مثل العرض والارتفاع.
  2. صور الأصول المحلية (Image.asset): تُستخدم لتحميل صور محلية مخزّنة داخل ملفات المشروع.
    • لاستخدام الأصول المحلية، يجب أولاً إنشاء مجلد (مثل images) ووضع ملفات الصور بداخله.
    • بعد ذلك، يجب تسجيل هذا المجلد في ملف pubspec.yaml تحت قسم assets حتى يعرف التطبيق مكانه.

مثال: صورة من الشبكة

Scaffold(
  body: Center(
    child: Image.network(
      'https://picsum.photos/300/200',
      width: 300,
      height: 200,
      fit: BoxFit.cover,
    ),
  ),
)

مثال: صورة من الأصول المحلية

الخطوة 1: أنشئ مجلد images في جذر مشروعك وأضف ملفًا مثل logo.png.

الخطوة 2: سجّله في pubspec.yaml:

flutter:
  assets:
    - images/logo.png
    # Or include the entire folder:
    # - images/

الخطوة 3: استخدمه في الكود:

Scaffold(
  body: Center(
    child: Image.asset(
      'images/logo.png',
      width: 200,
      height: 200,
    ),
  ),
)

4. تنظيم الكود: StatelessWidget

مع نمو واجهة المستخدم، يصبح وضع كل الكود في كتلة واحدة أمرًا يصعب إدارته. من أفضل الممارسات تنظيم الكود وفصله.

بدلاً من كتابة شجرة ضخمة من عناصر الواجهة داخل الملف الرئيسي، يجب استخلاص أجزاء من واجهة المستخدم إلى أصناف (Classes) مخصصة منفصلة تمتد (extends) من StatelessWidget.

  • طريقة build: يتطلب كل StatelessWidget طريقة (Method) باسم build. تكون هذه الطريقة مسؤولة عن إرجاع شجرة عناصر الواجهة (أي واجهة المستخدم) الخاصة بذلك المكون.
  • فصل الملفات: لتنظيم أفضل، يجب وضع كل شاشة مخصصة أو عنصر واجهة رئيسي في ملف .dart منفصل خاص به (مثل ملف home_screen.dart)، ثم استيراده (import) داخل ملف main.dart.
graph TD
    A[main.dart] --> B(MaterialApp)
    B --> C(home: HomeScreen)
    C -.-> D[home_screen.dart]
    D --> E(StatelessWidget)
    E --> F(Scaffold)
    F --> G(عناصر الـ Body...)
 
    style A fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    style D fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px

مثال

الملف: main.dart

import 'package:flutter/material.dart';
import 'home_screen.dart';
 
void main() {
  runApp(
    MaterialApp(
      home: HomeScreen(),
    ),
  );
}

الملف: home_screen.dart

import 'package:flutter/material.dart';
 
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Screen'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.home, size: 80, color: Colors.teal),
            SizedBox(height: 16),
            Text(
              'Welcome to the Home Screen!',
              style: TextStyle(fontSize: 20),
            ),
          ],
        ),
      ),
    );
  }
}

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


5. المسافات في التخطيط: Container و Padding و Margin

للتحكم في المسافات ولف عناصر واجهة أخرى، يوفّر Flutter مفاهيم Container و Padding و Margin. عنصر Container هو عنصر واجهة يلف عنصرًا فرعيًا واحدًا، ويتيح لك تطبيق التنسيق، والمحاذاة، والمسافات.

من الضروري فهم الفرق بين Margin و Padding:

  • Margin (الهامش الخارجي): المسافة بين الحد الخارجي لعنصر الواجهة والعناصر الموجودة خارجه.
  • Padding (الحشو الداخلي): المسافة بين الحد الداخلي لعنصر الواجهة والمحتوى الفرعي بداخله.

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

graph TD
    subgraph مفهوم نموذج الصندوق
    M[Margin: المساحة خارج الحد]
    subgraph حد الـ Container
    P[Padding: المساحة داخل الحد]
    subgraph عنصر الواجهة الفرعي
    C((عنصر الواجهة الفرعي))
    end
    P --> C
    end
    M --> P
    end

مثال: Container مع Margin و Padding

Scaffold(
  body: Center(
    child: Container(
      margin: EdgeInsets.all(20),    // 20px space OUTSIDE the box
      padding: EdgeInsets.all(16),   // 16px space INSIDE the box
      decoration: BoxDecoration(
        color: Colors.blue[100],
        border: Border.all(color: Colors.blue, width: 2),
        borderRadius: BorderRadius.circular(12),
      ),
      child: Text(
        'I have margin and padding!',
        style: TextStyle(fontSize: 18),
      ),
    ),
  ),
)

مثال: عنصر Padding المستقل

// When you only need padding (no color, no border):
Padding(
  padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
  child: Text('I only need padding, not a full Container.'),
)

مثال: عنصر Center

// Explicitly center a widget inside its parent:
Center(
  child: Text('I am centered!'),
)

6. محاذاة عناصر متعددة: Row و Column

عندما تحتاج إلى عرض عدة عناصر واجهة معًا، لا يكفي استخدام Container لأنه يقبل عنصرًا فرعيًا واحدًا فقط. بدلاً من ذلك، تستخدم تخطيطات تقبل قائمة من children.

6.1. Row (تخطيط أفقي)

يقوم عنصر Row بمحاذاة عناصره الفرعية أفقيًا جنبًا إلى جنب.

  • المحور الرئيسي (Main Axis): أفقي.
  • المحور العرضي (Cross Axis): رأسي.

مثال

Row(
  children: [
    Icon(Icons.star, color: Colors.amber, size: 40),
    Icon(Icons.star, color: Colors.amber, size: 40),
    Icon(Icons.star, color: Colors.amber, size: 40),
    Icon(Icons.star_border, color: Colors.amber, size: 40),
    Icon(Icons.star_border, color: Colors.amber, size: 40),
  ],
)

يعرض هذا الكود 5 أيقونات نجوم في سطر أفقي (3 ممتلئة، 2 فارغة) - تشبه شريط تقييم.

6.2. Column (تخطيط رأسي)

يقوم عنصر Column بمحاذاة عناصره الفرعية رأسيًا، ويضعها الواحد أسفل الآخر.

  • المحور الرئيسي (Main Axis): رأسي.
  • المحور العرضي (Cross Axis): أفقي.

مثال

Column(
  children: [
    CircleAvatar(
      radius: 50,
      backgroundImage: NetworkImage('https://picsum.photos/200'),
    ),
    SizedBox(height: 12),
    Text(
      'John Doe',
      style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
    ),
    SizedBox(height: 4),
    Text(
      'Flutter Developer',
      style: TextStyle(fontSize: 16, color: Colors.grey),
    ),
  ],
)

هذا الكود ينشئ بطاقة ملف شخصي بسيطة تحتوي على صورة رمزية (Avatar) أعلاها، يليها الاسم والمسمى الوظيفي مرصوصين رأسيًا.

6.3. خصائص المحاذاة

يوفّر كل من Row و Column خصائص لتحديد كيفية توزيع عناصرهما الفرعية عبر المساحة المتاحة:

  • mainAxisAlignment: يتحكم في كيفية وضع العناصر الفرعية على طول المحور الرئيسي (مثل توسيط العناصر أفقيًا في Row أو رأسيًا في Column).
  • crossAxisAlignment: يتحكم في كيفية محاذاة العناصر الفرعية على طول المحور العرضي. ملاحظة: تعتمد المحاذاة على المحور العرضي على وجود حد محدد للمساحة. فإذا لم يكن لعنصر Row أو Column ارتفاع/عرض محدد، أو لم يكن هناك Container أب يحدد حدوده، فقد لا تُحدث المحاذاة على المحور العرضي أي تغيير مرئي في التخطيط، لأن العنصر ينكمش ليلائم حجم عناصره الفرعية.
graph LR
    subgraph Row
    R_Main[المحور الرئيسي: أفقي] --> R_Cross[المحور العرضي: رأسي]
    end
 
    subgraph Column
    C_Main[المحور الرئيسي: رأسي] --> C_Cross[المحور العرضي: أفقي]
    end

مثال: MainAxisAlignment

// Evenly space items across the row
Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    ElevatedButton(onPressed: () {}, child: Text('Save')),
    ElevatedButton(onPressed: () {}, child: Text('Cancel')),
    ElevatedButton(onPressed: () {}, child: Text('Delete')),
  ],
)

القيم الشائعة:

القيمةالتأثير
MainAxisAlignment.startتجميع العناصر عند البداية (الافتراضي)
MainAxisAlignment.centerتوسيط العناصر على طول المحور
MainAxisAlignment.endتجميع العناصر عند النهاية
MainAxisAlignment.spaceBetweenمسافة متساوية بين العناصر، بدون مسافة عند الأطراف
MainAxisAlignment.spaceEvenlyمسافة متساوية بين العناصر وحولها
MainAxisAlignment.spaceAroundنصف مسافة عند الأطراف، ومسافة كاملة بين العناصر

مثال: CrossAxisAlignment

// Align children to the start (left) of a Column
Container(
  height: 300,
  width: double.infinity,
  color: Colors.grey[200],
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text('Title', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
      Text('Subtitle goes here', style: TextStyle(fontSize: 16)),
      Text('Description text aligned to the left.'),
    ],
  ),
)

مثال: دمج Row و Column معًا

Scaffold(
  body: Padding(
    padding: EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // Header Row
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('My App', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
            Icon(Icons.settings),
          ],
        ),
        SizedBox(height: 20),
        // Content
        Text('Welcome back!', style: TextStyle(fontSize: 18)),
        SizedBox(height: 12),
        // Button Row
        Row(
          children: [
            Expanded(
              child: ElevatedButton(
                onPressed: () {},
                child: Text('Profile'),
              ),
            ),
            SizedBox(width: 12),
            Expanded(
              child: ElevatedButton(
                onPressed: () {},
                child: Text('Settings'),
              ),
            ),
          ],
        ),
      ],
    ),
  ),
)

هذا الكود يبني تخطيط شاشة بسيط يحتوي على صف رأسي (عنوان + أيقونة)، ونص ترحيبي، وزرّين جنبًا إلى جنب.

6.4. المنطقة الآمنة (Safe Area)

عند وضع التخطيطات على جهاز موبايل حديث، قد تتداخل عناصر الواجهة مع ميزات العتاد مثل نتوء الكاميرا (Notch)، أو ميزات البرمجيات مثل شريط حالة النظام. لمنع ذلك، لُف تخطيطك الرئيسي داخل عنصر SafeArea. يضيف SafeArea تلقائيًا الحشو اللازم لضمان ظهور المحتوى بالكامل دون أن يختفي خلف واجهات نظام التشغيل.

مثال

Scaffold(
  body: SafeArea(
    child: Column(
      children: [
        Text(
          'This text will NOT overlap with the status bar or notch!',
          style: TextStyle(fontSize: 18),
        ),
        SizedBox(height: 20),
        Text('All content is safely visible.'),
      ],
    ),
  ),
)

بدون SafeArea، قد يختفي عنصر Text الأول خلف شريط حالة الهاتف أو نتوء الكاميرا. أما مع SafeArea، يضيف Flutter تلقائيًا الحشو العلوي اللازم.