تخطَّ إلى المحتوى

توزيع المساحة في Flutter: Expanded و Flexible و Spacer و Wrap

الدرس 13 من 33 · Flutter

في درس Row و Column تعرّفت على Expanded سريعاً. لكن توزيع المساحة هو قلب تصميم الواجهات: عمودان بنسبة 2 إلى 1، وزر يلتصق بآخر الصف، ووسوم تنزل لسطر جديد عندما يضيق المكان. في هذا الدرس ستتقن الأدوات الأربع التي تحل هذه المسائل كلها.

Row يعمل على مرحلتين:

  1. يسأل الأبناء العاديين عن الحجم الذي يحتاجونه ويعطيهم إياه.
  2. يقسّم المساحة الباقية على الأبناء المرنين (Expanded و Flexible و Spacer).

تخيّل أباً يوزع مصروفاً: يدفع أولاً الفواتير الثابتة، ثم يقسم الباقي على الأبناء بحسب نصيب كل واحد. ونفس الكلام ينطبق على Column لكن بالارتفاع.

Expanded يأخذ كل المساحة الباقية. وإذا وُجد أكثر من واحد، تتقاسم المساحة بحسب flex:

Row(
children: [
Expanded(
flex: 2,
child: Container(height: 60, color: Colors.indigo),
),
Expanded(
flex: 1,
child: Container(height: 60, color: Colors.amber),
),
],
)

مجموع النِّسَب 3، فيأخذ الأزرق ثلثي العرض والأصفر الثلث. القيمة الافتراضية لـ flex هي 1، فإذا لم تكتبها تساوت الحصص.

استخدام مشهور: حقل كتابة يأخذ كل العرض، وبجانبه زر بحجمه الطبيعي:

Row(
children: [
const Expanded(
child: TextField(
decoration: InputDecoration(hintText: 'اكتب رسالة...'),
),
),
IconButton(onPressed: () {}, icon: const Icon(Icons.send)),
],
)

Expanded يجبر ابنه على ملء حصته كلها. أما Flexible فيقول لابنه: «لك أن تأخذ حتى هذه الحصة، وإن كفاك أقل فخذ الأقل».

Row(
children: [
Flexible(
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.amber.shade100,
child: const Text('اسم قصير'),
),
),
const Icon(Icons.verified, color: Colors.blue),
],
)

هنا الصندوق يأخذ عرض نصه فقط، فتظهر علامة التوثيق ملاصقة للاسم. لو استخدمنا Expanded لتمدد الصندوق وابتعدت العلامة إلى آخر الصف. وإذا كان الاسم طويلاً جداً، فإن Flexible يمنعه من تجاوز المساحة المتاحة، فينزل النص لسطر جديد بدلاً من خطأ Overflow.

Expanded Flexible
يأخذ من المساحة الباقية نعم نعم
يجبر الابن على ملء الحصة نعم لا
متى تستخدمه عنصر يجب أن يملأ الفراغ عنصر قد يكون صغيراً لكن لا يجوز أن يخرج عن الحدود

والحقيقة أن Expanded ما هو إلا Flexible مع fit: FlexFit.tight.

Spacer فراغ فارغ يتمدد ليأخذ المساحة الباقية. يدفع ما قبله وما بعده بعيداً عن بعضهما:

Row(
children: [
const Text('الإجمالي'),
const Spacer(),
const Text('350 جنيهاً'),
const SizedBox(width: 8),
IconButton(onPressed: () {}, icon: const Icon(Icons.info_outline)),
],
)

النص «الإجمالي» يبقى في بداية الصف، والسعر والزر يذهبان إلى نهايته. والفرق بينه وبين SizedBox: الأول فراغ مرن بحسب المتاح، والثاني فراغ ثابت بالمقدار الذي تكتبه.

mainAxisSize: كم يأخذ الصف نفسه؟

Section titled “mainAxisSize: كم يأخذ الصف نفسه؟”

افتراضياً يتمدد Row بعرض المتاح كله، ويتمدد Column بالارتفاع كله. إذا أردته أن ينكمش على قدر محتواه:

Container(
color: Colors.indigo.shade50,
padding: const EdgeInsets.all(8),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.star, color: Colors.amber),
SizedBox(width: 4),
Text('4.8'),
],
),
)

بدون MainAxisSize.min يتمدد الصندوق الملون بعرض الشاشة. معها يصبح شارة صغيرة على قدر النجمة والرقم.

ماذا لو كان عندك عشرة وسوم (Tags) لا يسعها سطر واحد؟ Row سيُظهر خطأ Overflow بالشريط الأصفر والأسود. الحل Wrap: يرتب الأبناء في صف، وعندما ينتهي العرض ينزل تلقائياً إلى سطر جديد، مثل الكلمات في الفقرة.

const Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(label: Text('Flutter')),
Chip(label: Text('Dart')),
Chip(label: Text('تطبيقات الموبايل')),
Chip(label: Text('واجهات المستخدم')),
Chip(label: Text('أندرويد')),
Chip(label: Text('iOS')),
],
)
  • spacing: المسافة الأفقية بين العناصر في نفس السطر.
  • runSpacing: المسافة الرأسية بين الأسطر.
  • Chip: Widget جاهز على شكل كبسولة صغيرة، مناسب للوسوم والتصنيفات.
lib/order_summary.dart
class OrderSummary extends StatelessWidget {
const OrderSummary({super.key});
Widget _line(String label, String value, {bool bold = false}) {
final style = TextStyle(
fontSize: 16,
fontWeight: bold ? FontWeight.bold : FontWeight.normal,
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
Flexible(child: Text(label, style: style)),
const Spacer(),
Text(value, style: style),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ملخص الطلب')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(label: Text('توصيل سريع')),
Chip(label: Text('دفع عند الاستلام')),
Chip(label: Text('قابل للاسترجاع')),
],
),
const SizedBox(height: 16),
_line('سعر المنتجات', '320 جنيهاً'),
_line('التوصيل', '30 جنيهاً'),
const Divider(),
_line('الإجمالي', '350 جنيهاً', bold: true),
const Spacer(),
Row(
children: [
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: () {},
child: const Text('تأكيد الطلب'),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: () {},
child: const Text('إلغاء'),
),
),
],
),
],
),
),
);
}
}

في هذه الشاشة استخدمنا الأدوات الأربع:

  • Wrap للوسوم: إذا ضاق العرض ينزل الوسم الثالث لسطر جديد.
  • Spacer داخل كل سطر: يدفع السعر إلى نهاية الصف.
  • Spacer داخل Column: يدفع الزرين إلى أسفل الشاشة، مهما كان ارتفاعها.
  • Expanded بـ flex: 2 و flex: 1: زر التأكيد عرضه ضعف زر الإلغاء.

والدالة _line مثال على عادة جيدة: عندما يتكرر نفس الشكل ثلاث مرات، اجعله دالة أو Widget وأعد استخدامه.

  1. شغّل ملخص الطلب، ثم صغّر عرض النافذة وراقب الوسوم وهي تنزل لسطر جديد.
  2. احذف Spacer الموجود في Column ولاحظ أين يذهب الزران.
  3. اجعل الزرين بنفس العرض.
  4. تحدٍّ: ابنِ صفاً فيه ثلاثة صناديق ملونة بنسبة 1 إلى 2 إلى 3.
  • Expanded خارج Row أو Column: يسبب خطأ «Incorrect use of ParentDataWidget». Expanded و Flexible و Spacer تعمل فقط كأبناء مباشرين لـ Row أو Column أو Flex.
  • Expanded داخل Column موجود في قائمة تمرير: القائمة ارتفاعها غير محدود، فلا توجد «مساحة باقية» لتقسيمها، ويظهر خطأ.
  • استخدام Row لعناصر كثيرة متغيرة العدد: يظهر Overflow على الشاشات الصغيرة. استخدم Wrap.

سؤال

صف فيه Expanded بقيمة flex: 3 وآخر بقيمة flex: 1. كم يأخذ الأول من العرض؟

سؤال

عندك عدد غير معروف من الوسوم وتريدها أن تنزل لسطر جديد عندما يضيق العرض. ماذا تستخدم؟

  • Row و Column يعطيان الأبناء العاديين حاجتهم، ثم يقسمان الباقي على الأبناء المرنين.
  • Expanded يملأ حصته كلها، و flex تحدد النِّسَب بين الحصص.
  • Flexible يسمح للابن بأن يكون أصغر من حصته، ويمنعه من تجاوزها.
  • Spacer فراغ مرن يدفع العناصر إلى الأطراف.
  • Wrap ينزل بالعناصر إلى سطر جديد بدلاً من خطأ Overflow.