ابنِ واجهات تتكيّف مع قيود الأب، ومع النافذة، ومع أعراف كل نظام تشغيل.
افتح هذا الدرس في Kodokonيصف MediaQuery النافذة المنطقية، لا الشاشة الفيزيائية: الحجم، وpadding (النتوء notch، شريط النظام)، وviewInsets (لوحة المفاتيح)، وtextScaler... وثمة فخّ كلاسيكي: MediaQuery.of(context) يُسجِّل الودجت في جميع هذه الخصائص. فعندما تُفتَح لوحة المفاتيح، يتغير viewInsets وتعيد كل الودجات المُسجَّلة البناء - بما فيها تلك التي لا تقرأ سوى العرض. ومنذ Flutter 3.10، يُكشَف MediaQueryData عبر InheritedModel: فالوصولات الموجَّهة MediaQuery.sizeOf(context) وpaddingOf وviewInsetsOf... لا تُسجِّل الودجت إلا في الجانب المطلوب. وردّ الفعل الخبير: انفِ .of(context).size من قاعدة كودك.
import 'package:flutter/material.dart';
class AdaptiveGrid extends StatelessWidget {
const AdaptiveGrid({super.key});
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final columns = switch (width) {
>= 900 => 4,
>= 600 => 3,
_ => 2,
};
return GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
),
itemCount: 24,
itemBuilder: (context, i) => Card(
child: Center(child: Text('Tile $i')),
),
);
}
}يجيب MediaQuery عن سؤال "ما حجم النافذة؟"؛ ويجيب LayoutBuilder عن سؤال "كم من المساحة يمنحني أبي؟". والفرق حاسم بالنسبة للمكوّنات القابلة لإعادة الاستخدام: فاللوحة التي تُعرَض تارةً بملء الشاشة، وتارةً في عمود عرضه 320 بكسل، يجب أن تتفاعل مع قيودها، لا مع النافذة - وإلا ظنّت أنها على جهاز لوحي في حين أنها في الواقع في مكان ضيّق. وثمة تفصيل داخلي مفيد: يعمل builder الخاص بـ LayoutBuilder أثناء مرحلة التخطيط (layout)، حين تكون القيود معلومة؛ ولذلك لا يمكنك إطلاق setState متزامن داخله، فإطار العمل يمنع ذلك.
import 'package:flutter/material.dart';
class MasterDetail extends StatelessWidget {
const MasterDetail({
super.key,
required this.list,
required this.detail,
});
final Widget list;
final Widget detail;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 720) {
return list;
}
return Row(
children: [
SizedBox(width: 320, child: list),
const VerticalDivider(width: 1),
Expanded(child: detail),
],
);
},
);
}
}بالنسبة للفروق بين iOS وAndroid، فإن مصدر الحقيقة على جانب الودجت هو Theme.of(context).platform: فهو مشتقّ من defaultTargetPlatform مع احترام التجاوزات (overrides) - وهو أمر أساسي لاختبار عرض iOS من اختبار أو لمحاكاة منصّة أخرى. والعديد من عمليات التكيّف مدمجة أصلًا في إطار العمل: فيزياء التمرير (ارتداد iOS مقابل توهّج Android)، وانتقالات الصفحات، وإيماءة السحب من الحافّة للعودة. وتنتقل بواني .adaptive (Switch.adaptive، CircularProgressIndicator.adaptive) إلى عرض Cupertino من تلقاء نفسها. احتفظ بتفرّعاتك اليدوية للأعراف المتباينة فعلًا، مثل تسمية الإجراءات أو موضع أزرار مربّع الحوار.
import 'package:flutter/material.dart';
class SettingsTile extends StatelessWidget {
const SettingsTile({
super.key,
required this.value,
required this.onChanged,
});
final bool value;
final ValueChanged<bool> onChanged;
@override
Widget build(BuildContext context) {
final platform = Theme.of(context).platform;
final isApple = platform == TargetPlatform.iOS ||
platform == TargetPlatform.macOS;
return ListTile(
title: Text(
isApple ? 'Settings' : 'Preferences',
),
trailing: Switch.adaptive(
value: value,
onChanged: onChanged,
),
);
}
}