2.13. Виджеты: лейаут

Автор: Александр Маренков

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

Всё это — основа для работы с лейаут-виджетами, которые мы рассмотрим в следующей статье.

А рассказ про лейаут мы начнём с небольшого примера.

Пример для затравки

Подход к лейауту во Flutter отличается от подходов в HTML/CSS или нативной мобильной разработке. Сначала он может показаться странным и неудобным, но это дело привычки — если изучить основные принципы все становится понятным и легким.

Давайте рассмотрим небольшой пример:

class MyApp extends StatelessWidget {
  static const appTitle = 'Flutter Handbook Demo';

  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: appTitle,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: Container(
        color: Colors.red,
        width: 100.0,
        height: 100.0,
      ),
    );
  }
}

Мы добавляем виджет Container , задаем ему красный фон и указываем размер 100 на 100.

Как вы думаете что будет выведено на экран?

Результат

7_901fafa484.svg

Странно — почему-то виджет Container получился размером не 100 на 100, а на весь экран.

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

Пример (здесь и далее мы скрыли под катом часть объёмных примеров для удобства чтения)
class MyApp extends StatelessWidget {
  static const appTitle = 'Flutter Handbook Demo';

  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: appTitle,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: SizedBox(
        height: 100.0,
        width: 100.0,
        child: Container(
          color: Colors.red,
          width: 100.0,
          height: 100.0,
        ),
      ),
    );
  }
}

Запускаем приложение и видим ту же самую проблему — виджет Container вновь занимает весь экран.

6_1cbce67260.svg

А теперь давайте попробуем обернуть наш Container в виджет Center и посмотрим изменится ли что-нибудь:

Пример
class MyApp extends StatelessWidget {
  static const appTitle = 'Flutter Handbook Demo';

  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: appTitle,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: Center(
        child: Container(
          color: Colors.red,
          width: 100.0,
          height: 100.0,
        ),
      ),
    );
  }
}

Как видим, виджетContainer наконец-то принял желаемый размер.

5_16e8b496c6.svg

Давайте разберёмся, почему так происходит.

Процесс лейаута во Flutter

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

Таким образом для расчета лейаута производится два прохода — один проход вниз по дереву при котором передаются констрейнты (BoxConstraints) и один проход вверх по дереву при котором возвращаются полученные размеры (Size).

На самом деле есть исключения при которых происходит как меньше, так и больше проходов, но подробнее об этом поговорим далее.

Классы Size и BoxConstraints

Давайте разберемся что же из себя представляют классы Size и BoxConstraints.

Size — класс, который описывает размер, содержит два поля width  и height.

BoxConstraints — это класс, который описывает возможные размеры виджета, а именно минимальную и максимальную ширину (параметры minWidth  и maxWidth ), а также минимальную и максимальную высоту (параметры minHeight  и maxHeight ).

Констрейнты можно разделить по строгости значений на:

  • loose — когда минимальная ширина и высота равны нулю, а максимальная ширина и высота имеют конкретное значение, не равное нулю. Иными словами это гибкие констрейнты, которые предоставляют дочернему виджету выбор в определенных пределах (например при BoxConstraints.loose(const Size(200.0, 300.0)) размеры виджета могут быть следующими 0 <= width <= 200.0 и 0 <= height <= 300.0);
  • tight — когда минимальная ширина равна максимальной и минимальная высота равна максимальной высоте. Иными словами это негибкие констрейнты, которые заставляют виджет быть определенного размера (например при BoxConstraints.tight(const Size(200.0, 300.0)) размеры виджета будут следующими width = 200.0 и height = 300.0);

Стоит отметить, что констрейнты могут быть различными по ширине и высоте — например, loose по ширине и tight по высоте.

Также констрейнты можно разделить по пределу значений на:

  • unbounded ****— когда максимальная ширина и/или максимальная высота равны бесконечности (double.infinity). Иными словами это гибкие констрейнты, которые определяют только минимальную ширину и/или высоту для дочернего виджета, не задавая при этом максимального предела (значения Size в таком случае будут находится в следующих пределах 200.0 <= width <= ∞ и 300.0 <= height <= ∞).
  • bounded — когда максимальная ширина и максимальная высота не равны бесконечности(double.infinity).

А теперь давайте, рассмотрим что происходило в нашем примере с красным контейнером.

В первом примере Container был растянут на весь экран — это произошло потому что виджет MaterialApp отдает tight BoxConstraints, которые равны размеру экрана. Это особенность работы виджета MaterialApp — он обязует быть на весь экран виджет, который мы передаем в качестве главного экрана home.

Во втором примере произошло то же самое — SizedBox и вложенный в него Container были растянуты на весь экран, а в примере с Center наоборот, сработало. Чтобы понять почему это произошло надо взглянуть на то, какие значения BoxConstraints были переданы контейнеру в каждом из случаев.

Примечание

Главное правило лейаута — если какой-то из виджетов принимает неправильный размер, то первым делом стоит обратить внимание не на параметры этого виджета, а на родительский виджет и BoxConstraints, которые он передаёт

Инспектируем BoxConstraints с помощью DevTools

Во Flutter есть инструмент, который позволяет инспектировать констрейнты виджетов приложения запущенного в debug / profile режимах, — это DevTools.

Давайте рассмотрим наш пример с виджетом Center и воспользуемся DevTools.

Запускаем DevTools, выбираем вкладку LayoutExplorer, затем в окне WidgetTree выбираем наш виджет Center и видим следующее:

Screenshot_2023_02_26_at_15_56_30_fc111eae2a.webp

Виджет Center получил от MaterialApp tight-констрейнты размером 430 на 932 (констрейнты указаны в скобках w = 430.0 и h = 932.0, ширина и высота определены строго, поэтому значения констрейнтов tight), сам Center определил свой размер соответственно 430 на 932 (указано над скобками w = 430.0 и h = 932.0).

Теперь давайте выберем виджет Container и посмотрим какие у него констрейнты и размер:

Screenshot_2023_02_26_at_15_55_17_1124e6c754.webp

Виджет Container получил от Center loose-констрейнты размером 0<=430 на 0<=932 (констрейнты указаны в скобках 0 <= w <= 430.0 и 0 <= h <= 932.0).

Это означает, что Container может быть любого размера в пределах данных значений: в результате размеры Container определились как 100 на 100 (указано над скобками w = 100.0 и h = 100.0), в соответствии с переданными width и height.

Рассмотрев пример с использованием ****DevTools, мы видим, что действительно размер виджета определяется следующим образом в зависимости от констрейнтов:

Примечание

— когда виджет получает tight-констрейнты, то его ширина и/или высота будут точно им соответствовать;
— когда виджет получает loose**-**констрейнты, то его ширина и/или высота будет в их диапазоне.

Определяем BoxConstraints с помощью виджета LayoutBuilder

Бывают случаи, когда нам необходимо знать констрейнты не во время дебага приложения, а в рантайме — например, в зависимости от текущих констрейнтов по-разному строить дерево виджетов. Для таких случаев нам не подойдет DevTools, и стоит воспользоваться LayoutBuilder.

LayoutBuilder — виджет, который вызывает специальный коллбэк (builder) на каждое изменение констрейнтов, при этом в качестве аргумента передаётся контекст и значение текущих констрейнтов. Результатом вызова builder должен быть виджет, который будет встроен в дерево виджетов.

Давайте взглянем на простой пример использования LayoutBuilder — на каждый вызов выводим в консоль значение констрейнтов.

Пример
class MyApp extends StatelessWidget {
  static const appTitle = 'Flutter Handbook Demo';

  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: appTitle,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: Center(
        child: LayoutBuilder(builder: (context, constraints) {
          print(constraints);
          return Container(
            color: Colors.red,
            width: 100.0,
            height: 100.0,
          );
        }),
      ),
    );
  }
}

При запуске приложения увидим, что на каждый build в консоль будет выводиться следующее flutter: BoxConstraints(0.0<=w<=430.0, 0.0<=h<=932.0) — это констрейнты которые мы получаем от родительского виджета Center.

Знания о текущих констрейнтах обычно нам нужны для адаптивной верстки — когда в зависимости от констрейнтов мы хотим по-разному скомпоновать виджеты, например для portrait- и landscape**-**ориентации или для разных размеров экранов.

Для работы с portrait- и landscape-ориентациями есть специальный виджет OrientationBuilder, который построен на базе LayoutBuilder.


Отлично, с лейаутом разобрались. В следующей статье мы подробно рассмотрим лейаут-виджетами: именно с их помощью осуществляется расположение контента на странице.

И для этого вам как раз пригодится знание BoxConstraints. Поэтому советуем пройти квиз, чтобы закрепить эту тему.

А как будете готовы — переходите к следующему статье.