التصميم المتجاوب في Flutter: MediaQuery و LayoutBuilder
تطبيق Flutter الواحد يعمل على هاتف صغير عرضه 360، وعلى تابلت عرضه 800، وعلى متصفح بعرض 1400. إذا صممت لحجم واحد فقط، سيبدو تطبيقك مضغوطاً على الصغير أو ممطوطاً وفارغاً على الكبير. التصميم المتجاوب (Responsive Design) معناه أن تتكيف الواجهة مع المساحة المتاحة. في هذا الدرس ستتعلم أدواته الأساسية.
القاعدة الأولى: لا تكتب أحجاماً ثابتة للشاشة
Section titled “القاعدة الأولى: لا تكتب أحجاماً ثابتة للشاشة”أكبر خطأ: Container(width: 360) لأن هاتفك عرضه 360. على هاتف أصغر يحدث Overflow، وعلى أكبر يبقى فراغ.
استخدم الأدوات المرنة التي تعلمتها:
ExpandedوFlexibleلتقسيم المساحة بالنِّسَب.double.infinityلأخذ كل العرض.Wrapللنزول لسطر جديد.GridViewمعmaxCrossAxisExtentلعدد أعمدة يتغير تلقائياً.constraints: BoxConstraints(maxWidth: ...)لوضع حد أقصى.
هذه الأدوات وحدها تحل أغلب المشاكل. وما بقي له أداتان: MediaQuery و LayoutBuilder.
MediaQuery: معلومات عن الشاشة
Section titled “MediaQuery: معلومات عن الشاشة”MediaQuery يعطيك معلومات عن الجهاز والنافذة:
final size = MediaQuery.sizeOf(context);final width = size.width;final height = size.height;final orientation = MediaQuery.orientationOf(context);final isLandscape = orientation == Orientation.landscape;MediaQuery.sizeOf(context): حجم الشاشة (أو النافذة) بالعرض والارتفاع.MediaQuery.orientationOf(context): الاتجاه:portraitرأسي، أوlandscapeأفقي.MediaQuery.paddingOf(context): المناطق المحجوبة بنتوء الكاميرا وشريط الحالة.MediaQuery.viewInsetsOf(context).bottom: ارتفاع لوحة المفاتيح عندما تكون ظاهرة.
استخدام بسيط: صورة غلاف ارتفاعها ربع ارتفاع الشاشة مهما كان الجهاز.
SizedBox( height: MediaQuery.sizeOf(context).height * 0.25, width: double.infinity, child: const ColoredBox(color: Colors.indigo),)LayoutBuilder: المساحة المتاحة لي أنا
Section titled “LayoutBuilder: المساحة المتاحة لي أنا”MediaQuery يخبرك بحجم الشاشة كلها. لكن الـ Widget قد يكون داخل عمود جانبي يأخذ ثلث الشاشة فقط. المهم إذن ليس حجم الشاشة، بل المساحة التي أعطاها الأب لهذا الـ Widget. هذا ما يعطيه LayoutBuilder:
LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth < 600) { return const Text('تخطيط ضيق'); } return const Text('تخطيط واسع'); },)constraints.maxWidth هو أقصى عرض متاح لهذا المكان بالتحديد. والدالة builder تُستدعى من جديد كلما تغيّرت المساحة: عند تدوير الهاتف، أو تغيير حجم نافذة المتصفح.
تخيّل الفرق هكذا: MediaQuery يخبرك مساحة الشقة كلها، و LayoutBuilder يخبرك مساحة الغرفة التي تقف فيها. وعندما ترتب الأثاث، يهمك حجم الغرفة.
MediaQuery |
LayoutBuilder |
|
|---|---|---|
| يقيس | الشاشة أو النافذة كلها | المساحة المتاحة للـ Widget |
| مناسب لـ | قرارات على مستوى الشاشة، ولوحة المفاتيح، والاتجاه | مكونات يُعاد استخدامها في أماكن مختلفة الحجم |
نقاط التحول: Breakpoints
Section titled “نقاط التحول: Breakpoints”نقطة التحول عرض معيّن يتغير عنده التخطيط. الأرقام الشائعة:
| العرض | الجهاز الغالب | التخطيط المعتاد |
|---|---|---|
| أقل من 600 | هاتف | عمود واحد، وشريط تنقل سفلي |
| من 600 إلى 1024 | تابلت | عمودان |
| أكثر من 1024 | سطح المكتب | ثلاثة أعمدة، وقائمة جانبية دائمة |
أشهر نمط متجاوب: قائمة وتفاصيل. على الهاتف تظهر القائمة وحدها، والضغط على عنصر يفتح شاشة جديدة. وعلى التابلت تظهر القائمة والتفاصيل جنباً إلى جنب.
AspectRatio: نسبة ثابتة بين العرض والارتفاع
Section titled “AspectRatio: نسبة ثابتة بين العرض والارتفاع”مشغل فيديو يجب أن يبقى بنسبة 16 إلى 9 مهما كان العرض:
AspectRatio( aspectRatio: 16 / 9, child: Container( color: Colors.black, alignment: Alignment.center, child: const Icon(Icons.play_arrow, color: Colors.white, size: 48), ),)يأخذ AspectRatio كل العرض المتاح، ويحسب الارتفاع من النسبة. على شاشة عرضها 320 يكون الارتفاع 180، وعلى عرض 640 يكون 360.
FittedBox: صغّر المحتوى ليدخل
Section titled “FittedBox: صغّر المحتوى ليدخل”عندك رقم كبير في بطاقة ضيقة، ولا تريده أن ينكسر على سطرين ولا أن يخرج عن الحدود:
const SizedBox( width: 120, child: FittedBox( fit: BoxFit.scaleDown, child: Text('1,250,000', style: TextStyle(fontSize: 40)), ),)FittedBox يصغّر ابنه حتى يدخل في المساحة. و BoxFit.scaleDown تعني: «صغّر إذا لزم الأمر، لكن لا تكبّر أبداً».
المشروع: قائمة وتفاصيل متجاوبة
Section titled “المشروع: قائمة وتفاصيل متجاوبة”class ResponsiveNotes extends StatefulWidget { const ResponsiveNotes({super.key});
@override State<ResponsiveNotes> createState() => _ResponsiveNotesState();}
class _ResponsiveNotesState extends State<ResponsiveNotes> { static const _notes = [ 'شراء مستلزمات البيت', 'مذاكرة درس Flutter', 'الاتصال بالطبيب', 'تجهيز عرض العمل', ];
int _selected = 0;
Widget _buildList({required bool showSelection}) { return ListView.builder( itemCount: _notes.length, itemBuilder: (context, index) { return ListTile( leading: const Icon(Icons.note_outlined), title: Text(_notes[index]), selected: showSelection && index == _selected, onTap: () => setState(() => _selected = index), ); }, ); }
Widget _buildDetails() { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( _notes[_selected], textAlign: TextAlign.center, style: const TextStyle(fontSize: 24), ), ), ); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('ملاحظاتي')), body: LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth >= 600;
if (!isWide) { return Column( children: [ Expanded(child: _buildList(showSelection: false)), Container( width: double.infinity, padding: const EdgeInsets.all(16), color: Theme.of(context).colorScheme.primaryContainer, child: Text('المحددة: ${_notes[_selected]}'), ), ], ); }
return Row( children: [ SizedBox( width: 280, child: _buildList(showSelection: true), ), const VerticalDivider(width: 1), Expanded(child: _buildDetails()), ], ); }, ), ); }}كيف يعمل:
- عرض أقل من 600: القائمة تملأ الشاشة، وتحتها شريط صغير يعرض الملاحظة المحددة.
- عرض 600 أو أكثر: القائمة في عمود ثابت عرضه 280، وبجانبها التفاصيل في المساحة الباقية، والعنصر المحدد ملوّن في القائمة.
القرار كله في سطر واحد: final isWide = constraints.maxWidth >= 600;. والدالتان _buildList و _buildDetails تُستخدمان في التخطيطين، فلم نكرر كود القائمة مرتين.
لتجربته بسهولة شغّل التطبيق على المتصفح بالأمر flutter run -d chrome، ثم كبّر النافذة وصغّرها وراقب التخطيط يتبدل عند 600.
جرّب بنفسك
Section titled “جرّب بنفسك”- شغّل المشروع على المتصفح وغيّر عرض النافذة.
- غيّر نقطة التحول من 600 إلى 800 ولاحظ الفرق.
- أضف
AspectRatioبنسبة 16 إلى 9 أعلى التفاصيل كصورة للملاحظة. - تحدٍّ: أضف نقطة تحول ثانية عند 1024 يظهر بعدها عمود ثالث فيه «معلومات إضافية».
أخطاء شائعة
Section titled “أخطاء شائعة”- أرقام ثابتة لعرض الشاشة:
width: 360يعمل على هاتفك فقط. استخدم النِّسَب والأدوات المرنة. - حسابات كثيرة بنسبة من عرض الشاشة لكل شيء: خط حجمه
width * 0.05يصبح عملاقاً على التابلت. النِّسَب مناسبة للتخطيط العام، وليس لحجم الخطوط والأزرار. - استخدام MediaQuery داخل مكوّن يُعاد استخدامه: المكوّن لا يعرف أين سيوضع. استخدم
LayoutBuilderليتكيف مع مكانه الفعلي.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ما الفرق بين MediaQuery و LayoutBuilder؟
MediaQuery يعطي حجم الشاشة أو النافذة، أما LayoutBuilder فيعطي القيود التي فرضها الأب على هذا المكان تحديداً.
سؤال
ماذا يفعل AspectRatio بقيمة aspectRatio: 16 / 9؟
يأخذ العرض المتاح ويحسب الارتفاع منه بالنسبة المحددة، فيبقى الشكل متناسقاً على كل الشاشات.
الخلاصة
Section titled “الخلاصة”- لا تكتب أحجاماً ثابتة للشاشة. ابدأ بـ
ExpandedوWrapوGridViewالمرنة. MediaQueryلمعلومات الشاشة: الحجم والاتجاه ولوحة المفاتيح.LayoutBuilderيعطي المساحة المتاحة للمكوّن، وهو الأنسب لتبديل التخطيط.- نقاط التحول الشائعة 600 و 1024، وأشهر نمط هو «قائمة وتفاصيل».
AspectRatioلنسبة ثابتة، وFittedBoxلتصغير المحتوى حتى يدخل.