构建能够适应父节点约束、窗口以及各个操作系统惯例的界面。
在 Kodokon 中打开本课MediaQuery 描述的是逻辑窗口,而不是物理屏幕:尺寸、padding(刘海、系统栏)、viewInsets(键盘)、textScaler…… 一个经典的陷阱:MediaQuery.of(context) 会把 widget 订阅到所有这些属性上。当键盘弹出时,viewInsets 发生变化,每一个订阅了的 widget 都会重建,包括那些只读取宽度的 widget。自 Flutter 3.10 起,MediaQueryData 通过一个 InheritedModel 暴露出来:MediaQuery.sizeOf(context)、paddingOf、viewInsetsOf…… 这些有针对性的访问器只会把 widget 订阅到它所请求的那个方面。专家的条件反射:把 .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 像素宽的列里显示的面板,必须对它的约束做出反应,而不是对窗口做出反应,否则它明明局促得很,却以为自己是在一台平板上。一个有用的内部细节:LayoutBuilder 的 builder 在布局阶段运行,此时约束已经确定;因此你不能在它内部触发一次同步的 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 的差异,在 widget 这一侧的权威来源是 Theme.of(context).platform:它派生自 defaultTargetPlatform,同时又尊重各种覆盖,这对于从测试中检验 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,
),
);
}
}