親の制約、ウィンドウ、そして各OSの慣習に適応するインターフェースを作りましょう。
このレッスンを Kodokon で開くMediaQueryは物理的な画面ではなく、論理的なウィンドウを記述します。サイズ、padding(ノッチ、システムバー)、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 pxの列の中で表示されるパネルは、ウィンドウではなく自分の制約に反応しなければなりません。さもないと、実際は窮屈なのにタブレットの上にいると思い込んでしまいます。役立つ内部の詳細があります。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の違いについては、ウィジェット側の真実の源は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,
),
);
}
}