Logo

المعمل: تخطيطات واجهات متقدمة، إدارة المساحة، وتنظيم الكود

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

المعمل: تخطيطات واجهة متقدمة، إدارة المساحة، وتنظيم الكود

دمج Row وColumn، وExpanded مقابل Flexible، وفصل البيانات عن الواجهة

نظرة عامة

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


1. التخطيطات الأساسية: Row و Column

تتمثل اللبنات الأساسية لتصميم الواجهات في وضع العناصر إما أفقيًا أو رأسيًا.

  • لمحاذاة العناصر أفقيًا عبر الشاشة، استخدم عنصر Row.
  • لمحاذاة العناصر رأسيًا (الواحد أسفل الآخر)، استخدم عنصر Column.

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

مثال: Row يحتوي على عناصر Column بداخله

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    Column(
      children: [
        Icon(Icons.home, size: 30),
        Text('Home'),
      ],
    ),
    Column(
      children: [
        Icon(Icons.search, size: 30),
        Text('Search'),
      ],
    ),
    Column(
      children: [
        Icon(Icons.person, size: 30),
        Text('Profile'),
      ],
    ),
  ],
)

ينتج هذا الكود شريط تنقّل سفلي (Bottom Navigation) حيث يكون كل عنصر عبارة عن أيقونة يوجد تحتها تسميتها النصية.

إعادة استخدام الكود

عند بناء الواجهات، ستجد نفسك غالبًا تكرر نفس تركيبة عناصر الواجهة. للحفاظ على نظافة الكود وتجنب التكرار، يجب استخلاص عناصر الواجهة المتكررة هذه إلى عناصر واجهة مخصصة (Custom Widgets) أو دوال منفصلة قابلة لإعادة الاستخدام. من خلال تمرير معاملات (Parameters) (مثل النص أو الأيقونة المحددة) إلى هذه المكونات المخصصة، يمكنك عرض نسخ مختلفة ديناميكيًا من نفس التخطيط باستخدام كتلة كود واحدة فقط.

مثال: عنصر واجهة مخصص وقابل لإعادة الاستخدام مع معاملات

بدلاً من نسخ ولصق تركيبة الأيقونة والتسمية ثلاث مرات، استخلصها إلى عنصر واجهة واحد:

// Define the reusable widget once
class NavItem extends StatelessWidget {
  final IconData icon;
  final String label;
 
  const NavItem({required this.icon, required this.label});
 
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Icon(icon, size: 30),
        Text(label),
      ],
    );
  }
}
 
// Use it three times with different data — no code duplication
Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    NavItem(icon: Icons.home,   label: 'Home'),
    NavItem(icon: Icons.search, label: 'Search'),
    NavItem(icon: Icons.person, label: 'Profile'),
  ],
)
graph TD
    A[عنصر Row] --> B[عنصر مخصص 1]
    A --> C[عنصر مخصص 2]
    A --> D[عنصر مخصص 3]
    B --> E[Column]
    E --> F[Icon]
    E --> G[Text]
    style A fill:#e1f5fe,stroke:#01579b
    style B fill:#fff3e0,stroke:#e65100

2. إدارة المساحة: Expanded مقابل Flexible

عند العمل مع أحجام شاشات مختلفة، يجب أن يتكيف تخطيطك معها. تحديد أحجام ثابتة يدويًا (Hardcoding) قد يسبب مشكلات في التخطيط.

عنصر Expanded

يُستخدم عنصر Expanded داخل Row أو Column لإجبار عنصر فرعي على أخذ كل المساحة المتبقية المتاحة. هذا مفيد جدًا عندما يكون لديك عناصر ثابتة الحجم (مثل أيقونة) وتريد أن يمتد النص المجاور لها حتى نهاية الشاشة.

مثال: Expanded يملأ المساحة المتبقية

Row(
  children: [
    Icon(Icons.label),          // Fixed size — takes only what it needs
    Expanded(
      child: Text(
        'This text stretches to fill the rest of the row',
        overflow: TextOverflow.ellipsis,
      ),
    ),
    Icon(Icons.arrow_forward),  // Fixed size — appears at the far right
  ],
)

بدون Expanded، سيأخذ عنصر Text فقط المساحة التي يحتاجها محتواه. أما مع Expanded، فسيملأ أي مساحة متبقية بين الأيقونتين.

عنصر Flexible

يتيح لك عنصر Flexible تخصيص مساحة الشاشة بشكل نسبي باستخدام نسب مئوية. فبدلاً من تحديد ارتفاع أو عرض ثابت، تحدد خاصية flex. يقوم النظام بحساب إجمالي المساحة المتاحة عن طريق جمع كل قيم flex، ثم يخصص المساحة بناءً على تلك النسبة.

فمثلاً، إذا كان لديك ثلاثة أقسام في الواجهة وأعطيتها قيم flex تساوي 7 و1 و2، فإن المجموع يكون 10. ستُقسَّم الشاشة تلقائيًا بحيث يأخذ العنصر الأول 70%، والثاني 10%، والثالث 20% من المساحة.

مثال: Flexible يقسّم المساحة بشكل نسبي

Column(
  children: [
    Flexible(
      flex: 7,
      child: Container(
        color: Colors.blue[100],
        child: Center(child: Text('Header — 70%')),
      ),
    ),
    Flexible(
      flex: 1,
      child: Container(
        color: Colors.grey[300],
        child: Center(child: Text('Divider — 10%')),
      ),
    ),
    Flexible(
      flex: 2,
      child: Container(
        color: Colors.green[100],
        child: Center(child: Text('Footer — 20%')),
      ),
    ),
  ],
)

تتقاسم الأقسام الثلاثة ارتفاع الشاشة بالنسبة 7:1:2 - بغض النظر عن حجم الجهاز المستخدم.

pie title توزيع مساحة الشاشة (قيم Flex)
    "قسم الرأس (Flex: 7)" : 70
    "قسم الفاصل (Flex: 1)" : 10
    "قسم التذييل (Flex: 2)" : 20

Expanded مقابل Flexible - مقارنة سريعة

العنصرالسلوك
Expandedيملأ دائمًا كل المساحة المتبقية (يعادل Flexible مع fit: FlexFit.tight)
Flexibleيأخذ حتى حصته النسبية فقط؛ يمكن أن يكون العنصر الفرعي أصغر من ذلك

ملاحظة حول الصور: عند وضع الصور داخل تخطيطات أو حاويات مرنة، قد لا يتم تحديد حجمها تلقائيًا بشكل صحيح. يجب ضبط خصائص المطابقة (Fit) على الصورة لضمان تغطيتها لمساحة الحاوية المخصصة لها بشكل مثالي دون تشويه.

Flexible(
  flex: 7,
  child: Image.asset(
    'assets/header.jpg',
    fit: BoxFit.cover,   // Covers the container without distortion
    width: double.infinity,
  ),
)

3. تنظيم كود الواجهة

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

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

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

مثال: تقسيم شاشة كبيرة واحدة إلى عناصر أقسام صغيرة

قبل - كل شيء محشور في طريقة build واحدة:

// Hard to read, hard to maintain
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      children: [
        Image.asset('assets/photo.jpg', height: 250, fit: BoxFit.cover),
        Padding(
          padding: const EdgeInsets.all(16),
          child: Align(
            alignment: Alignment.centerLeft,
            child: Text('Product Title', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
          ),
        ),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Text('A long description of the product goes here...'),
        ),
      ],
    ),
  );
}

بعد - كل قسم أصبح عنصر واجهة مستقلًا بذاته:

// Clean main page — reads like an outline
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      children: [
        ImageSection(),
        TitleSection(),
        DescriptionSection(),
      ],
    ),
  );
}
 
// ── Separate widget files ──────────────────────────────
 
class ImageSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Image.asset(
      'assets/photo.jpg',
      height: 250,
      width: double.infinity,
      fit: BoxFit.cover,
    );
  }
}
 
class TitleSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Align(
        alignment: Alignment.centerLeft,
        child: Text(
          'Product Title',
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        ),
      ),
    );
  }
}
 
class DescriptionSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Text('A long description of the product goes here...'),
    );
  }
}
graph TD
    Page[واجهة الصفحة الرئيسية] --> S1[عنصر قسم الصورة]
    Page --> S2[عنصر قسم العنوان]
    Page --> S3[عنصر قسم الوصف]
    S3 --> P[Padding]
    P --> T[عنصر Text]
    style Page fill:#e8f5e9,stroke:#1b5e20
    style S1 fill:#fce4ec,stroke:#880e4f
    style S2 fill:#fce4ec,stroke:#880e4f
    style S3 fill:#fce4ec,stroke:#880e4f

4. دمج نماذج البيانات (Data Models)

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

تحقق ذلك بإنشاء نموذج بيانات (Data Model) (صنف قياسي يحتوي على خصائص مثل الاسم، الوصف، إلخ). فبدلاً من كتابة النصوص مباشرة داخل الواجهة، تمرّر نسخًا (Instances) من هذا النموذج إلى عناصر الواجهة المخصصة لديك، مما يتيح للواجهة عرض أي بيانات تستقبلها بشكل ديناميكي.

مثال: تعريف نموذج بيانات

// product.dart — pure data, no UI code
class Product {
  final String name;
  final String description;
  final String imageUrl;
  final double price;
 
  const Product({
    required this.name,
    required this.description,
    required this.imageUrl,
    required this.price,
  });
}

مثال: قائمة من المنتجات (بيانات تجريبية)

// data.dart — sample data kept separate from the UI
const List<Product> products = [
  Product(
    name: 'Wireless Headphones',
    description: 'Noise-cancelling over-ear headphones with 30 h battery.',
    imageUrl: 'assets/headphones.jpg',
    price: 79.99,
  ),
  Product(
    name: 'Mechanical Keyboard',
    description: 'Compact tenkeyless keyboard with blue switches.',
    imageUrl: 'assets/keyboard.jpg',
    price: 54.99,
  ),
];

مثال: عنصر واجهة يستقبل نموذجًا بدلاً من نصوص خام

// product_card.dart — UI that works with ANY Product instance
class ProductCard extends StatelessWidget {
  final Product product;   // accepts a model, not separate strings
 
  const ProductCard({required this.product});
 
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Image.asset(product.imageUrl, fit: BoxFit.cover),
          Padding(
            padding: const EdgeInsets.all(12),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(product.name,
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                const SizedBox(height: 4),
                Text(product.description),
                const SizedBox(height: 8),
                Text('\$${product.price.toStringAsFixed(2)}',
                    style: TextStyle(color: Colors.green, fontSize: 16)),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
 
// Render all products with a single list — no hardcoding
ListView(
  children: products.map((p) => ProductCard(product: p)).toList(),
)

الآن، إضافة منتج جديد تتطلب فقط إضافة عنصر واحد إلى قائمة products - وتتحدث الواجهة تلقائيًا.


🛠️ تكليف تطبيقي

مهمتك في هذا المعمل هي بناء شاشة واجهة كاملة تستخدم كل المفاهيم التي ناقشناها.

  1. قسّم الشاشة إلى أقسام منطقية.
  2. استخدم عنصري Row وColumn لبناء البنية الأساسية.
  3. طبّق Expanded وFlexible حيثما كان ذلك مناسبًا لضمان تكيّف الشاشة بشكل صحيح عبر الأجهزة المختلفة.
  4. افصل الأقسام المختلفة إلى عناصر واجهة مخصصة ومستقلة ونظيفة.