Создавай интерфейсы, которые подстраиваются под ограничения родителя, под окно и под соглашения каждой ОС.
Открыть этот урок в KodokonMediaQuery описывает логическое окно, а не физический экран: размер, 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, должна реагировать на свои ограничения, а не на окно - иначе она решит, что она на планшете, тогда как на деле ей тесно. Полезная внутренняя деталь: builder у LayoutBuilder выполняется во время фазы вёрстки, когда ограничения уже известны; поэтому вызвать синхронный 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,
),
);
}
}