Kodokon kodokon.com

การปรับตัว: LayoutBuilder, MediaQuery และแพลตฟอร์ม

สร้างอินเทอร์เฟซที่ปรับตัวเข้ากับข้อจำกัดของ parent เข้ากับหน้าต่าง และเข้ากับธรรมเนียมของแต่ละ OS

9 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

MediaQuery อธิบาย หน้าต่างเชิงตรรกะ (logical window) ไม่ใช่หน้าจอทางกายภาพ: ขนาด, padding (ติ่งหน้าจอ, แถบระบบ), viewInsets (แป้นพิมพ์), textScaler... กับดักคลาสสิก: MediaQuery.of(context) ทำให้วิดเจ็ตติดตาม (subscribe) คุณสมบัติเหล่านี้ ทั้งหมด เมื่อแป้นพิมพ์เปิดขึ้น 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 pattern: จุดพักที่อ่านง่าย (breakpoint)

MediaQuery ตอบคำถามว่า "หน้าต่างใหญ่แค่ไหน?" ส่วน LayoutBuilder ตอบว่า "parent ของฉัน ให้พื้นที่ฉันเท่าไร?" ความแตกต่างเล็กน้อยนี้ชี้ขาดสำหรับคอมโพเนนต์ที่นำกลับมาใช้ซ้ำได้: แผงที่บางครั้งแสดงเต็มหน้าจอ บางครั้งอยู่ในคอลัมน์กว้าง 320 px ต้องตอบสนองต่อ ข้อจำกัด (constraints) ของมัน ไม่ใช่ต่อหน้าต่าง มิฉะนั้นมันจะนึกว่าตัวเองอยู่บนแท็บเล็ตทั้งที่จริงมันกำลังถูกบีบอยู่ รายละเอียดภายในที่มีประโยชน์: builder ของ LayoutBuilder ทำงานระหว่าง ช่วงเลย์เอาต์ (layout phase) ตอนที่รู้ข้อจำกัดแล้ว คุณจึงไม่สามารถสั่ง 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 ในขณะที่เคารพ การ override ด้วย ซึ่งจำเป็นสำหรับการทดสอบการเรนเดอร์แบบ 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 เรนเดอร์ toggle แบบ Cupertino บน iOS/macOS

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. อะไรคือความแตกต่างระหว่าง MediaQuery.of(context) กับ MediaQuery.sizeOf(context)?
    • sizeOf ทำให้วิดเจ็ตติดตามเฉพาะแง่มุมของขนาดเท่านั้น: การเปิดแป้นพิมพ์จะไม่ทำให้มันสร้างใหม่
    • sizeOf คืนขนาดทางกายภาพเป็นพิกเซลจริงของหน้าจอ
    • of ถูกเลิกใช้แล้วและจะถูกนำออกจากเฟรมเวิร์ก
  2. เมื่อใดที่คุณควรเลือก LayoutBuilder แทน MediaQuery ในการปรับเลย์เอาต์?
    • เมื่อการตัดสินใจขึ้นอยู่กับพื้นที่ที่ parent จัดสรรให้ ไม่ใช่ขนาดของหน้าต่าง
    • เมื่อคุณต้องรู้แนวการวางของอุปกรณ์
    • เมื่อคุณต้องการหลีกเลี่ยงการสร้างใหม่เมื่อธีมเปลี่ยน
    • ไม่เลย: ทั้งสองใช้แทนกันได้
  3. ทำไมจึงควรเลี่ยง Platform.isIOS ในโค้ดวิดเจ็ต?
    • มันมาจาก dart:io ซึ่งใช้ไม่ได้บนเว็บ และเพิกเฉยต่อการ override ที่เทสต์และธีมใช้
    • มันช้ากว่า defaultTargetPlatform อย่างเห็นได้ชัด
    • มันคืนค่า true บน macOS