Irbisa · Шпаргалка

Адаптивный интерфейс

Брейкпоинты, LayoutBuilder, NavigationRail, складные экраны, ориентация, SafeArea, масштаб текста

Middle-разработчик · 20 вопросов

  1. 01

    Чем отличается responsive UI от adaptive UI во Flutter?

    Responsive UI перестраивает тот же контент под доступное место, adaptive UI меняет поведение приложения под устройство, способ ввода и платформу.

  2. 02

    Чем отличается MediaQuery от LayoutBuilder, и что из них должно определять раскладку виджета?

    MediaQuery описывает всё окно (размер, отступы, масштаб текста, настройки пользователя), а LayoutBuilder отдаёт ограничения, которые родитель реально передал этому виджету.

    class ProductCard extends StatelessWidget {  const ProductCard({super.key, required this.product});   final Product product;   @override…
  3. 03

    Какие брейкпоинты вы используете, чтобы переключать экран между раскладками телефона, планшета и десктопа, и где должна стоять эта проверка?

    Берите window size classes из Material 3: compact до 600 dp, medium 600–839, expanded 840–1199, large 1200–1599, extra-large от 1600.

    enum WindowSize { compact, medium, expanded, large, extraLarge } WindowSize windowSizeOf(double width) => switch (width) {      < 600 => WindowSize.compact,      < 840 => WindowSize.medium,      < 1200 => WindowSize.expanded,…
  4. 04

    Как обрабатывать ориентацию во Flutter: MediaQuery.orientationOf, OrientationBuilder и блокировка через SystemChrome?

    Поворот просто даёт новый размер окна: ничего не пересоздаётся, зависящие от размера виджеты перестраиваются.

    Future<void> main() async {  WidgetsFlutterBinding.ensureInitialized();  // a hint: large Android screens and iPad windows may ignore it  await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);  runApp(const App());}…
  5. 05

    Коллега масштабирует все размеры по ширине экрана через flutter_screenutil. Почему многие команды так не делают и что используют вместо этого?

    Размеры во Flutter уже в логических пикселях, и 16 dp на любом телефоне примерно одного физического размера; масштабирование по ширине лишь показывает на большом экране тот же макет, только крупнее.

    // wrong: every size proportional to a 375 x 812 design frame// SizedBox(//   width: 343.w, height: 48.h, // 763 x 71 dp on an 834 dp wide iPad//   child: FilledButton(onPressed: pay, child: Text('Pay', style: TextStyle(fontSize: 16.sp))),// )…
  6. 06

    Открытие клавиатуры перестраивает виджеты, рядом с которыми нет ни одного текстового поля. При чём тут MediaQuery.of(context)?

    MediaQuery.of(context) подписывает виджет на все поля MediaQueryData, а клавиатура меняет viewInsets на каждом кадре своего появления.

  7. 07

    На iPhone отступ 34 px под этим SendBar пропадает, как только открывается клавиатура. Какой отступ читает SafeArea, и баг ли это?

    Scaffold(  body: Column(    children: [      const Expanded(child: MessageList()),      SafeArea(        top: false,        child: SendBar(controller: _controller),      ),    ],  ),);

    SafeArea читает MediaQuery.padding, а это viewPadding минус viewInsets, поэтому когда клавиатура закрывает home indicator, нижний отступ становится нулём.

  8. 08

    Найдите баг: этот экран входа выглядит нормально, пока не откроется клавиатура, а затем внизу появляется жёлто-чёрная полоса переполнения.

    class SignInScreen extends StatelessWidget {  const SignInScreen({super.key});   @override  Widget build(BuildContext context) {    final height = MediaQuery.sizeOf(context).height;    return Scaffold(      appBar: AppBar(title: const Text('Sign in')),      body: Column(children: [        SizedBox(height: height * 0.4, child: const Center(child: FlutterLogo(size: 96))),        const SignInForm(), // two fields and a button, about 200 px      ]),    );  }}

    Шапка занимает 40% высоты окна, но MediaQuery.sizeOf не меняется, когда открывается клавиатура, а тело Scaffold сжимается на её высоту.

  9. 09

    Пользователь выставил системный размер шрифта 200%, и ваша самодельная нижняя панель превратила подписи в три точки. Что даёт MediaQuery.textScalerOf, и где ограничение масштаба оправдано?

    MediaQuery.textScalerOf(context) возвращает TextScaler, и scaler.scale(14) даёт размер, которым реально нарисуется шрифт в 14 px.

    final scaler = MediaQuery.textScalerOf(context);final large = scaler.scale(14) > 20; // the real painted size, not 14 * factor // right: reflow instead of truncating at 200%final userRow = large    ? Column(…
  10. 10

    Эта сетка шириной 1000 px. Сколько колонок она построит, какой ширины будет каждая карточка, и что изменится в боковой панели шириной 400 px?

    GridView.builder(  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(    maxCrossAxisExtent: 240,    crossAxisSpacing: 16,    mainAxisSpacing: 16,    childAspectRatio: 0.75,  ),  itemCount: products.length,  itemBuilder: (context, i) => ProductCard(product: products[i]),);

    Четыре колонки по 238 px: число колонок считается как (1000 / (240 + 16)).ceil() = 4, ширина как (1000 − 3 × 16) / 4.

  11. 11

    Ряд карточек обёрнут в IntrinsicHeight, чтобы они были одной высоты с самой высокой, и список начал подтормаживать. Во что обходится этот виджет?

    IntrinsicHeight спрашивает всё поддерево, какой высоты оно хотело бы быть, ещё до настоящей раскладки.

  12. 12

    Найдите баг: StatCard правильно раскладывается в сетке, но горизонтальная лента из тех же карточек на дашборде падает с ошибкой layout.

    class StatCard extends StatelessWidget {  const StatCard({super.key, required this.stat});  final Stat stat;  @override  Widget build(BuildContext context) => LayoutBuilder(        builder: (context, c) => c.maxWidth >= 240            ? Row(children: [Expanded(child: Text(stat.label)), Text(stat.value)])            : Column(children: [Text(stat.label), Text(stat.value)]),      );}// dashboard: a strip of cards that scrolls sidewaysWidget statStrip(List<Stat> stats) => ListView(      scrollDirection: Axis.horizontal,      children: [for (final s in stats) StatCard(stat: s)],    );

    Горизонтальный ListView даёт детям неограниченную ширину, поэтому c.maxWidth равен double.infinity, проверка >= 240 всегда выбирает Row, а Expanded внутри Row с неограниченной шириной бросает «RenderFlex children have non-zero flex but incoming width constraints are unbounded».

  13. 13

    При растягивании окна десктопа с ширины телефона на весь экран сбрасывается открытая вкладка. Как подключить нижнюю панель и navigation rail к одному роутеру?

    Храните выбранную вкладку в роутере, а не в навигационной панели, и меняйте только панель вокруг одного shell: NavigationBar в compact, NavigationRail от 600 dp.

    const destinations = [  (icon: Icons.inbox, label: 'Inbox'),  (icon: Icons.send, label: 'Sent'),]; // builder of StatefulShellRoute.indexedStack…
  14. 14

    Сборка под iOS должна ощущаться нативной. Сколько дают конструкторы .adaptive во Flutter, и что придётся ветвить руками?

    Конструкторы .adaptive покрывают короткий список контролов, которые на iOS и macOS рисуют своего Cupertino-двойника.

    // Controls: the adaptive constructor is the whole changeSwitch.adaptive(value: enabled, onChanged: onToggle); // CupertinoSwitch on Apple showAdaptiveDialog<void>(  context: context,  builder: (ctx) => AlertDialog.adaptive(…
  15. 15

    Тот же экран на Flutter теперь едет на десктоп и в web. Что нужно поменять ради мыши и клавиатуры, кроме более широкой раскладки?

    Всё, что не было нужно пальцу: подсветку при наведении и курсор-указатель, меню по правому клику, видимое кольцо фокуса с разумным порядком Tab и скролл, который можно тащить мышью там, где пользователь этого ждёт.

    class ContactRow extends StatelessWidget {  const ContactRow({super.key, required this.contact});  final Contact contact;   @override  Widget build(BuildContext context) {…
  16. 16

    Входящие представляют собой список на телефоне и список плюс письмо на планшете. После поворота в две панели жест «назад» снимает невидимый роут. Как это моделировать?

    Сделайте выбранное письмо состоянием, а не запушенным роутом.

    class _InboxScreenState extends State<InboxScreen> {  String? _selectedId; // lives ABOVE the breakpoint  final _navKey = GlobalKey<NavigatorState>();   void _select(String? id) => setState(() => _selectedId = id);…
  17. 17

    Пользователь на десктопе сужает это окно: список прыгает наверх, выбор пропадает. Что происходит с состоянием, когда ширина пересекает 840?

    class OrdersScreen extends StatelessWidget {  const OrdersScreen({super.key});   @override  Widget build(BuildContext context) {    return LayoutBuilder(      builder: (context, c) => c.maxWidth >= 840          ? const WideOrders()    // StatefulWidget: list + detail, owns _selectedId          : const NarrowOrders(), // StatefulWidget: list only, owns _selectedId    );  }}

    При пересечении 840 WideOrders заменяется на NarrowOrders.

  18. 18

    Приложение перетащили в split screen на Android, а на складном устройстве половина формы оказалась под шарниром. Что Flutter сообщает об этом железе?

    MediaQuery.displayFeaturesOf(context) перечисляет сгибы, шарниры и вырезы в координатах окна.

    class HingeAwarePanes extends StatelessWidget {  const HingeAwarePanes({super.key, required this.start, required this.end});  final Widget start;  final Widget end;   @override…
  19. 19

    Дизайн должен держаться на четырёх ширинах и при масштабе текста 200%. Как это протестировать, не поддерживая сорок golden-файлов?

    Разделите задачу.

    const sizes = [Size(390, 844), Size(673, 841), Size(1024, 768), Size(1440, 900)]; void main() {  for (final size in sizes) {    for (final scale in const [1.0, 2.0]) {      testWidgets('orders at ${size.width.toInt()} dp, ${scale}x text', (tester) async {…
  20. 20

    Телефон, планшет и десктоп из одной кодовой базы Flutter: писать три дерева виджетов или одно дерево, полное условий по ширине?

    Ни то, ни другое: делите по слоям.