تخطيطات Flutter والتصميم المتجاوب

Expanded و Flexible

50 دقيقة الدرس 3 من 16

مقدمة إلى العناصر المرنة

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

كلا العنصرين يغلّفان عنصراً فرعياً ويعيّنان له عامل مرونة يحدد مقدار المساحة المتبقية التي يحصل عليها. الفرق الرئيسي يكمن في كيفية تعاملهما مع المساحة المخصصة.

ملاحظة: يمكن استخدام Expanded و Flexible فقط كعناصر فرعية مباشرة لـ Row أو Column أو Flex. وضعهما داخل Container أو أي عنصر آخر سيسبب خطأ.

عنصر Expanded

يجبر Expanded عنصره الفرعي على ملء كل المساحة المتبقية المتاحة على المحور الرئيسي. يجب على العنصر الفرعي أن يأخذ بالضبط المساحة المخصصة له -- لا أكثر ولا أقل.

مثال أساسي لـ Expanded

Row(
  children: <Widget>[
    Container(width: 80, height: 50, color: Colors.red),
    Expanded(
      child: Container(height: 50, color: Colors.green),
    ),
    Container(width: 80, height: 50, color: Colors.blue),
  ],
)
// أحمر: 80 بكسل | أخضر: يملأ المتبقي | أزرق: 80 بكسل

في هذا المثال، الحاويتان الحمراء والزرقاء لهما عرض ثابت 80 بكسل لكل منهما. الحاوية الخضراء، المغلّفة بـ Expanded، تأخذ كل العرض المتبقي. إذا كان Row بعرض 400 بكسل، تحصل الحاوية الخضراء على 400 - 80 - 80 = 240 بكسل.

عنصر Flexible

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

مثال أساسي لـ Flexible

Row(
  children: <Widget>[
    Flexible(
      child: Container(
        width: 100,  // يستخدم فقط 100 بكسل حتى لو كان المتاح أكثر
        height: 50,
        color: Colors.orange,
      ),
    ),
    Container(width: 80, height: 50, color: Colors.purple),
  ],
)
// برتقالي: حتى المساحة المخصصة لكن 100 بكسل فقط | بنفسجي: 80 بكسل

مع Flexible، تطلب الحاوية البرتقالية 100 بكسل. إذا كانت المساحة المخصصة 320 بكسل، تأخذ الحاوية 100 بكسل فقط -- المساحة المتبقية 220 بكسل تبقى فارغة. مع Expanded، كانت ستُجبر على التمدد إلى 320 بكسل.

عامل flex

يقبل كلا من Expanded و Flexible معامل flex (الافتراضي: 1) الذي يحدد نسبة المساحة المتبقية التي يحصل عليها كل عنصر فرعي. تُقسم المساحة وفقاً لنسبة قيم flex.

مساحة تناسبية باستخدام flex

Row(
  children: <Widget>[
    Expanded(
      flex: 2,
      child: Container(height: 50, color: Colors.red),
    ),
    Expanded(
      flex: 1,
      child: Container(height: 50, color: Colors.green),
    ),
    Expanded(
      flex: 1,
      child: Container(height: 50, color: Colors.blue),
    ),
  ],
)
// إجمالي flex = 2 + 1 + 1 = 4
// أحمر: 2/4 = 50% | أخضر: 1/4 = 25% | أزرق: 1/4 = 25%
نصيحة: فكّر في قيم flex كأجزاء من كل. إذا كانت لديك قيم flex هي 2 و 1 و 1 (إجمالي 4)، يحصل العنصر الأول على جزئين من 4. يمكنك استخدام أي أعداد صحيحة موجبة -- flex: 3 و flex: 7 يعطيان تقسيم 30%/70%.

FlexFit: Tight مقابل Loose

الفرق الأساسي بين Expanded و Flexible يعود إلى خاصية FlexFit:

  • FlexFit.tight (يستخدمه Expanded) -- يجب على العنصر الفرعي ملء المساحة المخصصة بالكامل. يُجبر على أن يكون بالضبط بحجم تخصيصه.
  • FlexFit.loose (يستخدمه Flexible) -- يمكن للعنصر الفرعي أن يكون أصغر من المساحة المخصصة. يحدد حجمه بشكل طبيعي، حتى الحد الأقصى للتخصيص.

FlexFit.tight مقابل FlexFit.loose

// Expanded هو اختصار لـ:
Flexible(
  fit: FlexFit.tight,
  child: Container(height: 50, color: Colors.red),
)

// Flexible يستخدم افتراضياً:
Flexible(
  fit: FlexFit.loose,
  child: Container(height: 50, color: Colors.blue),
)

// يمكنك جعل Flexible يتصرف مثل Expanded:
Flexible(
  fit: FlexFit.tight,
  flex: 1,
  child: Container(height: 50, color: Colors.green),
)
ملاحظة: Expanded هو حرفياً غلاف مريح حول Flexible(fit: FlexFit.tight). هما متطابقان وظيفياً عند استخدام tight fit.

عنصر Spacer

عنصر Spacer هو عنصر مريح ينشئ مساحة فارغة ممتدة. هو مكافئ لـ Expanded(child: SizedBox.shrink()). يفيد Spacer في دفع العناصر الفرعية بعيداً عن بعضها بدون إنشاء عناصر مرئية.

استخدام Spacer

Row(
  children: <Widget>[
    Text('يسار', style: TextStyle(fontSize: 18)),
    Spacer(),  // يدفع 'يسار' و 'يمين' إلى الطرفين المتقابلين
    Text('يمين', style: TextStyle(fontSize: 18)),
  ],
)

// مع عامل flex:
Row(
  children: <Widget>[
    Text('بداية'),
    Spacer(flex: 2),  // ضعف المساحة
    Text('وسط'),
    Spacer(flex: 1),  // نصف المساحة
    Text('نهاية'),
  ],
)
نصيحة: Spacer أنظف من استخدام Expanded(child: SizedBox()) ويوصّل القصد بشكل أفضل. استخدمه عندما تحتاج مساحة فارغة مرنة بين العناصر.

توزيع المساحة بشكل تناسبي

متطلب شائع هو تقسيم التخطيط إلى أقسام تناسبية. إليك كيفية إنشاء نسب تقسيم شائعة:

نسب تقسيم شائعة

// تقسيم 50/50
Row(
  children: [
    Expanded(flex: 1, child: Container(color: Colors.red)),
    Expanded(flex: 1, child: Container(color: Colors.blue)),
  ],
)

// تقسيم 1/3 و 2/3
Row(
  children: [
    Expanded(flex: 1, child: Container(color: Colors.red)),
    Expanded(flex: 2, child: Container(color: Colors.blue)),
  ],
)

// تقسيم 25/50/25
Row(
  children: [
    Expanded(flex: 1, child: Container(color: Colors.red)),
    Expanded(flex: 2, child: Container(color: Colors.green)),
    Expanded(flex: 1, child: Container(color: Colors.blue)),
  ],
)

دمج Expanded مع عناصر ثابتة الحجم

النمط الأكثر عملية هو مزج العناصر ثابتة الحجم مع عناصر Expanded. يخصص Flutter المساحة أولاً للعناصر الفرعية ثابتة الحجم، ثم يوزع المساحة المتبقية بين عناصر Expanded/Flexible بناءً على عوامل flex الخاصة بها.

تخطيط ثابت + ممتد

Row(
  children: <Widget>[
    // ثابت: صورة رمزية (56 بكسل)
    CircleAvatar(radius: 28, child: Icon(Icons.person)),
    SizedBox(width: 12),
    // مرن: يأخذ المساحة المتبقية
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('أحمد علي', style: TextStyle(fontWeight: FontWeight.bold)),
          Text('متصل', style: TextStyle(color: Colors.green, fontSize: 12)),
        ],
      ),
    ),
    // ثابت: زر إجراء
    IconButton(icon: Icon(Icons.more_vert), onPressed: () {}),
  ],
)

مثال عملي: تخطيطات مقسمة

تخطيط بلوحتين حيث اللوحة اليسرى أضيق واللوحة اليمنى أعرض:

تخطيط مقسم بلوحتين

Row(
  children: <Widget>[
    Expanded(
      flex: 1,
      child: Container(
        color: Colors.grey[200],
        child: ListView(
          children: [
            ListTile(title: Text('العنصر 1')),
            ListTile(title: Text('العنصر 2')),
            ListTile(title: Text('العنصر 3')),
          ],
        ),
      ),
    ),
    Expanded(
      flex: 3,
      child: Container(
        padding: EdgeInsets.all(16),
        child: Text('منطقة المحتوى', style: TextStyle(fontSize: 24)),
      ),
    ),
  ],
)

مثال عملي: شريط جانبي + محتوى

تخطيط تطبيق شائع مع شريط جانبي ثابت العرض ومنطقة محتوى مرنة:

تخطيط شريط جانبي + محتوى

Row(
  children: <Widget>[
    // شريط جانبي ثابت
    Container(
      width: 250,
      color: Colors.blueGrey[900],
      child: Column(
        children: [
          SizedBox(height: 40),
          Text('القائمة', style: TextStyle(color: Colors.white, fontSize: 20)),
          ListTile(
            leading: Icon(Icons.dashboard, color: Colors.white),
            title: Text('لوحة التحكم', style: TextStyle(color: Colors.white)),
          ),
          ListTile(
            leading: Icon(Icons.settings, color: Colors.white),
            title: Text('الإعدادات', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
    ),
    // محتوى مرن
    Expanded(
      child: Container(
        padding: EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('لوحة التحكم', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
            SizedBox(height: 16),
            Text('مرحباً بعودتك! إليك نظرتك العامة.'),
          ],
        ),
      ),
    ),
  ],
)

مثال عملي: أعمدة تناسبية

صف إحصائيات حيث تأخذ كل بطاقة إحصائية مساحة متساوية:

بطاقات إحصائية متساوية العرض

Row(
  children: <Widget>[
    Expanded(
      child: Card(
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            children: [
              Icon(Icons.people, size: 32, color: Colors.blue),
              SizedBox(height: 8),
              Text('1,234', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
              Text('مستخدمون', style: TextStyle(color: Colors.grey)),
            ],
          ),
        ),
      ),
    ),
    Expanded(
      child: Card(
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            children: [
              Icon(Icons.shopping_cart, size: 32, color: Colors.green),
              SizedBox(height: 8),
              Text('567', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
              Text('طلبات', style: TextStyle(color: Colors.grey)),
            ],
          ),
        ),
      ),
    ),
    Expanded(
      child: Card(
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            children: [
              Icon(Icons.attach_money, size: 32, color: Colors.orange),
              SizedBox(height: 8),
              Text('\$9,876', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
              Text('الإيرادات', style: TextStyle(color: Colors.grey)),
            ],
          ),
        ),
      ),
    ),
  ],
)
تحذير: لا تستخدم أبداً Expanded أو Flexible داخل عنصر يوفر مساحة غير محدودة على المحور الرئيسي (مثل ListView القابل للتمرير). يحاول Expanded ملء «المساحة المتبقية»، لكن إذا كانت المساحة لا نهائية، لا يستطيع Flutter حساب الحجم ويُطلق خطأ. استخدم أحجاماً ثابتة أو SizedBox في السياقات القابلة للتمرير بدلاً من ذلك.

الملخص

  • Expanded يجبر عنصره الفرعي على ملء كل المساحة المخصصة (FlexFit.tight).
  • Flexible يسمح لعنصره الفرعي أن يكون أصغر من المساحة المخصصة (FlexFit.loose).
  • عامل flex يتحكم في توزيع المساحة التناسبي -- flex أعلى يعني مساحة أكبر.
  • Spacer ينشئ مساحة فارغة مرنة، مفيد لدفع العناصر بعيداً عن بعضها.
  • يخصص Flutter المساحة للعناصر الفرعية ثابتة الحجم أولاً، ثم يوزع المساحة المتبقية بين عناصر flex.
  • يجب أن يكون Expanded و Flexible عناصر فرعية مباشرة لـ Row أو Column أو Flex.
  • لا تستخدم Expanded/Flexible داخل عناصر قابلة للتمرير بمحور رئيسي غير محدود.