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

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

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

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

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

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

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

1class MyApp extends StatelessWidget {
2  static const appTitle = 'Flutter Handbook Demo';
3
4  const MyApp({super.key});
5
6  @override
7  Widget build(BuildContext context) {
8    return MaterialApp(
9      title: appTitle,
10      theme: ThemeData(
11        primarySwatch: Colors.blue,
12      ),
13      debugShowCheckedModeBanner: false,
14      home: Container(
15        color: Colors.red,
16        width: 100.0,
17        height: 100.0,
18      ),
19    );
20  }
21}

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

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

Результат

7

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

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

Пример (здесь и далее мы скрыли под катом часть объёмных примеров для удобства чтения)
1class MyApp extends StatelessWidget {
2  static const appTitle = 'Flutter Handbook Demo';
3
4  const MyApp({super.key});
5
6  @override
7  Widget build(BuildContext context) {
8    return MaterialApp(
9      title: appTitle,
10      theme: ThemeData(
11        primarySwatch: Colors.blue,
12      ),
13      debugShowCheckedModeBanner: false,
14      home: SizedBox(
15        height: 100.0,
16        width: 100.0,
17        child: Container(
18          color: Colors.red,
19          width: 100.0,
20          height: 100.0,
21        ),
22      ),
23    );
24  }
25}

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

6

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

Пример
1class MyApp extends StatelessWidget {
2  static const appTitle = 'Flutter Handbook Demo';
3
4  const MyApp({super.key});
5
6  @override
7  Widget build(BuildContext context) {
8    return MaterialApp(
9      title: appTitle,
10      theme: ThemeData(
11        primarySwatch: Colors.blue,
12      ),
13      debugShowCheckedModeBanner: false,
14      home: Center(
15        child: Container(
16          color: Colors.red,
17          width: 100.0,
18          height: 100.0,
19        ),
20      ),
21    );
22  }
23}

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

5

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

Процесс лейаута во 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

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

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

Screenshot

Виджет 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 — на каждый вызов выводим в консоль значение констрейнтов.

Пример
1class MyApp extends StatelessWidget {
2  static const appTitle = 'Flutter Handbook Demo';
3
4  const MyApp({super.key});
5
6  @override
7  Widget build(BuildContext context) {
8    return MaterialApp(
9      title: appTitle,
10      theme: ThemeData(
11        primarySwatch: Colors.blue,
12      ),
13      debugShowCheckedModeBanner: false,
14      home: Center(
15        child: LayoutBuilder(builder: (context, constraints) {
16          print(constraints);
17          return Container(
18            color: Colors.red,
19            width: 100.0,
20            height: 100.0,
21          );
22        }),
23      ),
24    );
25  }
26}

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

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

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


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

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

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

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E
Предыдущий параграф2.12. Виджеты: идентификатор key
Следующий параграф2.14. Виджеты: основные лейаут-виджеты