Что такое адаптивная и отзывчивая вёрстка в Flutter и как её реализовать?

Ответ

В Flutter адаптивная и отзывчивая вёрстка — это подход к проектированию пользовательского интерфейса, при котором приложение корректно отображается и удобно используется на устройствах с разными размерами экранов (от маленького телефона до большого планшета или десктопа) и ориентациями.

Основные инструменты и техники:

  1. MediaQuery — для получения характеристик экрана.

    final mediaData = MediaQuery.of(context);
    final screenWidth = mediaData.size.width;
    final screenHeight = mediaData.size.height;
    final pixelRatio = mediaData.devicePixelRatio;
    final isPortrait = screenHeight > screenWidth;
  2. LayoutBuilder — для реагирования на размеры родительского виджета. Более предпочтителен, чем MediaQuery, когда виджет должен адаптироваться к доступному пространству, а не ко всему экрану.

    LayoutBuilder(
      builder: (context, constraints) {
        // constraints.maxWidth — максимальная доступная ширина
        if (constraints.maxWidth > 600) {
          return _buildWideLayout(); // Макет для планшета/десктопа
        } else {
          return _buildNarrowLayout(); // Макет для телефона
        }
      },
    )
  3. Использование гибких виджетов (Flexible, Expanded) внутри Row и Column.

    Row(
      children: [
        Expanded( // Занимает всё оставшееся пространство
          flex: 2,
          child: Container(color: Colors.red),
        ),
        Expanded(
          flex: 1,
          child: Container(color: Colors.blue),
        ),
      ],
    )
  4. OrientationBuilder — для реакции на изменение ориентации.

    OrientationBuilder(
      builder: (context, orientation) {
        return orientation == Orientation.portrait
            ? _buildPortraitScaffold()
            : _buildLandscapeScaffold();
      },
    )
  5. Определение брейкпоинтов (breakpoints) для разных категорий устройств.

    class Breakpoints {
      static const double mobile = 600; // До 600px — телефон
      static const double tablet = 900; // 600-900px — планшет
      // >900px — десктоп
    }
    
    // Использование
    if (screenWidth < Breakpoints.mobile) {
      // Мобильный UI
    } else if (screenWidth < Breakpoints.tablet) {
      // Планшетный UI
    } else {
      // Десктопный UI
    }

Практический пример: адаптивный список деталей (master-detail):

class AdaptiveDetailPage extends StatelessWidget {
  final Widget masterView;
  final Widget detailView;

  const AdaptiveDetailPage({
    super.key,
    required this.masterView,
    required this.detailView,
  });

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;

    // На широком экране показываем master и detail рядом
    if (screenWidth > 600) {
      return Row(
        children: [
          SizedBox(width: 300, child: masterView),
          Expanded(child: detailView),
        ],
      );
    } 
    // На узком экране используем навигацию между экранами
    else {
      return masterView; // При тапе на элемент открываем detailView на новом экране
    }
  }
}

Ключевые принципы:

  • Относительные единицы: Использовать FractionallySizedBox, проценты, flex вместо фиксированных пикселей.
  • Тестирование: Обязательно тестировать UI на эмуляторах/устройствах с разными диагоналями и плотностями пикселей.
  • Содержание прежде всего: Макет должен адаптироваться, чтобы контент оставался читаемым и доступным.

Ответ 18+ 🔞

Э, слушай, тут про адаптивную вёрстку во Flutter. Ну, типа, чтобы твоё приложение не выглядело как хуй в пальто — на телефоне одно, а на планшете всё плывёт и кнопку "купить" надо искать лупой, блядь.

Основные штуки, без которых нихуя не получится:

  1. MediaQuery — это как спросить у телефона: "А ты кто такой, ёпта?" Ты узнаёшь размер экрана, плотность пикселей — всю подноготную. Без этого как слепой кот — будешь на ощупь верстать.

    final mediaData = MediaQuery.of(context);
    final screenWidth = mediaData.size.width; // Узнал ширину
    final screenHeight = mediaData.size.height; // Узнал высоту
    // И теперь можешь думать, что куда впихнуть.
  2. LayoutBuilder — а это поумнее будет. Он смотрит не на весь экран, а на то пространство, которое ему родитель выделил. Овердохуища полезная штука, когда у тебя не весь экран занят, а только кусок.

    LayoutBuilder(
      builder: (context, constraints) {
        // constraints.maxWidth — вот столько места есть, не больше.
        if (constraints.maxWidth > 600) {
          return _buildWideLayout(); // Развернулся на полную, планшетный режим
        } else {
          return _buildNarrowLayout(); // Скромно, по-мобильному
        }
      },
    )
  3. Гибкие виджеты (Flexible, Expanded). Это чтобы не делить пиксели вручную, как последний распиздяй. Сказал "расширяйся" — и они сами поделили оставшееся место. Красота, ебать.

    Row(
      children: [
        Expanded( // Этот жадный — заберёт 2 доли из трёх
          flex: 2,
          child: Container(color: Colors.red),
        ),
        Expanded( // А этому одна доля достанется
          flex: 1,
          child: Container(color: Colors.blue),
        ),
      ],
    )
  4. OrientationBuilder — чувак, который понимает, что телефон повернули. Альбомная или портретная? Он скажет.

    OrientationBuilder(
      builder: (context, orientation) {
        return orientation == Orientation.portrait
            ? _buildPortraitScaffold() // Вертикально — одна верстка
            : _buildLandscapeScaffold(); // Горизонтально — другая
      },
    )
  5. Брейкпоинты. Это такие магические числа, после которых всё меняется. Типа, меньше 600 пикселей — это телефон, больше — уже можно планшетную раскладку городить. Главное — не наделать их овердохуища, а то запутаешься, как собака сука.

    class Breakpoints {
      static const double mobile = 600;
      static const double tablet = 900;
    }
    
    // Использование
    if (screenWidth < Breakpoints.mobile) {
      // Телефон, всё компактно
    } else if (screenWidth < Breakpoints.tablet) {
      // Планшет, можно пошире
    } else {
      // Десктоп, ёперный театр, разгоняйся на полную
    }

Пример из жизни: список и детали (master-detail). Вот где ад начинается, если не подумать об адаптивности. На телефоне показываем список, тапнул — открылась деталька на весь экран. На планшете надо сразу и список, и детали рядом показывать, ядрёна вошь!

class AdaptiveDetailPage extends StatelessWidget {
  final Widget masterView; // Список
  final Widget detailView; // Детали

  const AdaptiveDetailPage({
    super.key,
    required this.masterView,
    required this.detailView,
  });

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;

    // Если экран широкий (планшет/десктоп) — вываливаем всё рядом
    if (screenWidth > 600) {
      return Row(
        children: [
          SizedBox(width: 300, child: masterView), // Список фиксированной ширины
          Expanded(child: detailView), // Детали занимают всё остальное
        ],
      );
    } 
    // Если экран узкий (телефон) — показываем только список, детали будут в новом окне
    else {
      return masterView;
    }
  }
}

Главные правила, которые надо вбить себе в башку:

  • Не пиши фиксированные размеры, как последний бздун. Используй проценты, flex, FractionallySizedBox. Мир не состоит из iPhone 13 Pro Max.
  • Тестируй, блядь, на всём! От самого мелкого пылесоса-телефона до огромного планшета. И поворачивай экран, а то выйдет хитрая жопа, которая только в портрете работает.
  • Контент — главный. Адаптивность — это не ради прикола. Это чтобы текст можно было прочитать, а кнопку — нажать, а не промазать мимо пальцем. Если для этого надо всё перелопатить — перелопачивай. Иначе доверия ебать ноль у пользователей будет.