توزيع المساحة في Flutter: Expanded و Flexible و Spacer و Wrap
في درس Row و Column تعرّفت على Expanded سريعاً. لكن توزيع المساحة هو قلب تصميم الواجهات: عمودان بنسبة 2 إلى 1، وزر يلتصق بآخر الصف، ووسوم تنزل لسطر جديد عندما يضيق المكان. في هذا الدرس ستتقن الأدوات الأربع التي تحل هذه المسائل كلها.
كيف يوزع Row المساحة؟
Section titled “كيف يوزع Row المساحة؟”Row يعمل على مرحلتين:
- يسأل الأبناء العاديين عن الحجم الذي يحتاجونه ويعطيهم إياه.
- يقسّم المساحة الباقية على الأبناء المرنين (
ExpandedوFlexibleوSpacer).
تخيّل أباً يوزع مصروفاً: يدفع أولاً الفواتير الثابتة، ثم يقسم الباقي على الأبناء بحسب نصيب كل واحد. ونفس الكلام ينطبق على Column لكن بالارتفاع.
Expanded والنِّسَب: flex
Section titled “Expanded والنِّسَب: flex”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)), ],)Flexible: خذ ما تحتاجه فقط
Section titled “Flexible: خذ ما تحتاجه فقط”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: فراغ مرن
Section titled “Spacer: فراغ مرن”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 يتمدد الصندوق الملون بعرض الشاشة. معها يصبح شارة صغيرة على قدر النجمة والرقم.
Wrap: النزول لسطر جديد
Section titled “Wrap: النزول لسطر جديد”ماذا لو كان عندك عشرة وسوم (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 جاهز على شكل كبسولة صغيرة، مناسب للوسوم والتصنيفات.
المشروع: ملخص طلب
Section titled “المشروع: ملخص طلب”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 وأعد استخدامه.
جرّب بنفسك
Section titled “جرّب بنفسك”- شغّل ملخص الطلب، ثم صغّر عرض النافذة وراقب الوسوم وهي تنزل لسطر جديد.
- احذف
Spacerالموجود فيColumnولاحظ أين يذهب الزران. - اجعل الزرين بنفس العرض.
- تحدٍّ: ابنِ صفاً فيه ثلاثة صناديق ملونة بنسبة 1 إلى 2 إلى 3.
أخطاء شائعة
Section titled “أخطاء شائعة”- Expanded خارج Row أو Column: يسبب خطأ «Incorrect use of ParentDataWidget».
ExpandedوFlexibleوSpacerتعمل فقط كأبناء مباشرين لـRowأوColumnأوFlex. - Expanded داخل Column موجود في قائمة تمرير: القائمة ارتفاعها غير محدود، فلا توجد «مساحة باقية» لتقسيمها، ويظهر خطأ.
- استخدام Row لعناصر كثيرة متغيرة العدد: يظهر Overflow على الشاشات الصغيرة. استخدم
Wrap.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
صف فيه Expanded بقيمة flex: 3 وآخر بقيمة flex: 1. كم يأخذ الأول من العرض؟
مجموع النِّسَب 4، فيأخذ الأول 3 من 4 أي ثلاثة أرباع العرض، والثاني الربع.
سؤال
عندك عدد غير معروف من الوسوم وتريدها أن تنزل لسطر جديد عندما يضيق العرض. ماذا تستخدم؟
Wrap يرتب الأبناء في أسطر وينتقل تلقائياً إلى سطر جديد، أما Row فيسبب خطأ Overflow.
الخلاصة
Section titled “الخلاصة”RowوColumnيعطيان الأبناء العاديين حاجتهم، ثم يقسمان الباقي على الأبناء المرنين.Expandedيملأ حصته كلها، وflexتحدد النِّسَب بين الحصص.Flexibleيسمح للابن بأن يكون أصغر من حصته، ويمنعه من تجاوزها.Spacerفراغ مرن يدفع العناصر إلى الأطراف.Wrapينزل بالعناصر إلى سطر جديد بدلاً من خطأ Overflow.