В этом параграфе мы поговорим о лейауте во 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.
Как вы думаете что будет выведено на экран?
Результат
Странно — почему-то виджет 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 вновь занимает весь экран.
А теперь давайте попробуем обернуть наш 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 наконец-то принял желаемый размер.
Давайте разберёмся, почему так происходит.
Процесс лейаута во 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 и видим следующее:

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

Виджет 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. Поэтому советуем пройти квиз, чтобы закрепить эту тему.
А как будете готовы — переходите к следующему параграфу.
