สร้างอินเทอร์เฟซที่ปรับตัวเข้ากับข้อจำกัดของ parent เข้ากับหน้าต่าง และเข้ากับธรรมเนียมของแต่ละ OS
เปิดบทเรียนนี้ใน KodokonMediaQuery อธิบาย หน้าต่างเชิงตรรกะ (logical window) ไม่ใช่หน้าจอทางกายภาพ: ขนาด, padding (ติ่งหน้าจอ, แถบระบบ), viewInsets (แป้นพิมพ์), textScaler... กับดักคลาสสิก: MediaQuery.of(context) ทำให้วิดเจ็ตติดตาม (subscribe) คุณสมบัติเหล่านี้ ทั้งหมด เมื่อแป้นพิมพ์เปิดขึ้น 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 ตอบว่า "parent ของฉัน ให้พื้นที่ฉันเท่าไร?" ความแตกต่างเล็กน้อยนี้ชี้ขาดสำหรับคอมโพเนนต์ที่นำกลับมาใช้ซ้ำได้: แผงที่บางครั้งแสดงเต็มหน้าจอ บางครั้งอยู่ในคอลัมน์กว้าง 320 px ต้องตอบสนองต่อ ข้อจำกัด (constraints) ของมัน ไม่ใช่ต่อหน้าต่าง มิฉะนั้นมันจะนึกว่าตัวเองอยู่บนแท็บเล็ตทั้งที่จริงมันกำลังถูกบีบอยู่ รายละเอียดภายในที่มีประโยชน์: builder ของ LayoutBuilder ทำงานระหว่าง ช่วงเลย์เอาต์ (layout phase) ตอนที่รู้ข้อจำกัดแล้ว คุณจึงไม่สามารถสั่ง 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 ในขณะที่เคารพ การ override ด้วย ซึ่งจำเป็นสำหรับการทดสอบการเรนเดอร์แบบ 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,
),
);
}
}