Kodokon kodokon.com

التكيّف: LayoutBuilder وMediaQuery والمنصّات

ابنِ واجهات تتكيّف مع قيود الأب، ومع النافذة، ومع أعراف كل نظام تشغيل.

9 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

يصف MediaQuery النافذة المنطقية، لا الشاشة الفيزيائية: الحجم، وpadding (النتوء notch، شريط النظام)، وviewInsets (لوحة المفاتيح)، وtextScaler... وثمة فخّ كلاسيكي: MediaQuery.of(context) يُسجِّل الودجت في جميع هذه الخصائص. فعندما تُفتَح لوحة المفاتيح، يتغير viewInsets وتعيد كل الودجات المُسجَّلة البناء - بما فيها تلك التي لا تقرأ سوى العرض. ومنذ Flutter 3.10، يُكشَف MediaQueryData عبر InheritedModel: فالوصولات الموجَّهة MediaQuery.sizeOf(context) وpaddingOf وviewInsetsOf... لا تُسجِّل الودجت إلا في الجانب المطلوب. وردّ الفعل الخبير: انفِ .of(context).size من قاعدة كودك.

DART
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')),
      ),
    );
  }
}
sizeOf + الأنماط العلائقية (relational patterns): نقاط توقّف قابلة للقراءة.

يجيب MediaQuery عن سؤال "ما حجم النافذة؟"؛ ويجيب LayoutBuilder عن سؤال "كم من المساحة يمنحني أبي؟". والفرق حاسم بالنسبة للمكوّنات القابلة لإعادة الاستخدام: فاللوحة التي تُعرَض تارةً بملء الشاشة، وتارةً في عمود عرضه 320 بكسل، يجب أن تتفاعل مع قيودها، لا مع النافذة - وإلا ظنّت أنها على جهاز لوحي في حين أنها في الواقع في مكان ضيّق. وثمة تفصيل داخلي مفيد: يعمل builder الخاص بـ LayoutBuilder أثناء مرحلة التخطيط (layout)، حين تكون القيود معلومة؛ ولذلك لا يمكنك إطلاق setState متزامن داخله، فإطار العمل يمنع ذلك.

DART
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 من تلقاء نفسها. احتفظ بتفرّعاتك اليدوية للأعراف المتباينة فعلًا، مثل تسمية الإجراءات أو موضع أزرار مربّع الحوار.

DART
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,
      ),
    );
  }
}
يعرض Switch.adaptive مفتاح تبديل بنمط Cupertino على iOS/macOS.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. ما الفرق بين MediaQuery.of(context) وMediaQuery.sizeOf(context)؟
    • يُسجِّل sizeOf الودجت في جانب الحجم فقط: ففتح لوحة المفاتيح لا يعيد بناءه
    • يُرجِع sizeOf الحجم الفيزيائي ببكسلات الشاشة الحقيقية
    • of مُهمَل (deprecated) وسيُزال من إطار العمل
  2. متى ينبغي أن تفضّل LayoutBuilder على MediaQuery لتكييف تخطيط؟
    • عندما يعتمد القرار على المساحة التي يخصّصها الأب، لا على حجم النافذة
    • عندما تحتاج إلى معرفة اتجاه الجهاز
    • عندما تريد تجنّب إعادة البناء عند تغيّر السِمة
    • أبدًا: فالاثنان قابلان للتبادل
  3. لماذا تتجنّب Platform.isIOS في كود الودجت؟
    • يأتي من dart:io غير المتوفّر على الويب، ويتجاهل التجاوزات التي تستخدمها الاختبارات والسِمة
    • إنه أبطأ بشكل ملحوظ من defaultTargetPlatform
    • يُرجِع true على macOS