Введение

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

Также мы рассмотрим, что такое констрейнты (BoxConstraints), посмотрим основные layout-виджеты, и разберёмся, с какими ошибками вы можете столкнуться во время вёрстки, почему они возникают, и как их решать.

Подход к лейауту во 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}

5

Как видим виджет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 и видим следующее:

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 — виджет, который вызывает специальный callback (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.

Основные layout-виджеты

Мы уже знаем что такое констрейнты и как работает расчет лейаута во Flutter, давайте рассмотрим как работают некоторые отдельные виджеты.

Align

Align — виджет, который позволяет спозиционировать дочерний виджет относительно себя.

Если Align имеет bounded-констрейнты, то он занимает максимально доступное место, если находится в unbounded-констрейнтах, то становится размером с дочерний виджет в тех направлениях в которых имеет unbounded значение констрейнта. У Align есть два параметра widthFactor и heightFactor, которые позволяют задать размер Align пропроционально размеру дочернего виджета, а также есть параметр alignment (AlignmentGeometry, Alignment или AlignmentDirectional), который определяет, где именно нужно спозиционировать дочерний виджет.

Расскажем о них подробнее.

AlignmentGeometry — базовый абстрактный класс, у которого определены два поля x и y. Эти два поля задают положение дочернего виджета следующим образом

  • (0.0, 0.0) — дочерний виджет будет по-центру;
  • (-1.0, -1.0) — дочерний виджет будет в верхнем-левом углу;
  • (1.0, 1.0) — дочерний виджет будет в нижнем-право углу.

Alignment — класс, который позволяет задать основные позиции для дочернего виджета без учёта направления текста в приложении (в некоторых языках направление текста идет справа налево). Вот все возможные значения Alignment (сперва выравнивание по вертикали, потом — по горизонтали) - bottomCenter, bottomLeft, bottomRight, center, centerLeft, centerRight, topCenter, topLeft, topRight.

fluttern

AlignmentDirectional — класс, который позволяет задать основные позиции для дочернего виджета с учетом направления текста которое используется в приложении. Вот все возможные значения AlignmentDirectional (сперва выравнивание по вертикали, потом — по горизонтали) - bottomCenter, bottomEnd, bottomStart, center, centerEnd, centerStart, topCenter, topEnd, topStart.

fluttern

Пример использования Align — размещаем виджет сверху-справа:

1home: Align(
2  alignment: Alignment.topRight,
3  child: Container(
4    color: Colors.red,
5    width: 100.0,
6    height: 100.0,
7  ),
8),

19

Center

Center — виджет-обертка над Align. Работает точно так же как Align, но позиционирует дочерний виджет всегда по центру.

ConstrainedBox

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

При этом мы можем «сузить» констрейнты, т.е. не можем выходить за пределы внешних констрейнтов, например констрейнт 100≤размер≤200 мы можем переопределить как 125≤размер≤175, но не можем переопределить как 0≤размер≤300.

Пример использования ConstrainedBox за пределами текущих констрейнтов — пытаемся переопределить констрейнты для Container:

1home: ConstrainedBox(
2  constraints: BoxConstraints.tight(Size(100, 100.0)),
3  child: Container(
4    color: Colors.red,
5    width: 100.0,
6    height: 100.0,
7  ),
8),

4

Как мы видим Container все так же растягивается на весь экран т.к. мы не можем переопределить констрейнты которые не вписываются в текущие.

Пример использования ConstrainedBox в пределах текущих констрейнтов — пытаемся переопределить констрейнты для Container:

1home: Center(
2  child: ConstrainedBox(
3    constraints: BoxConstraints.tight(Size(50.0, 50.0)),
4    child: Container(
5      color: Colors.red,
6      width: 100.0,
7      height: 100.0,
8    ),
9  ),
10),

3

Как видим, Container принимает размер 50х50 в соответствии с констрейнтами из ConstrainedBox, несмотря на переданные в него параметры width и height.

UnconstrainedBox

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

У UnconstrainedBox есть уже знакомый нам параметр alignment (AlignmentGeometry, Alignment или AlignmentDirectional), а также особый параметр constrainedAxis (Axis). С помощью constrainedAxis можно задать конкретную ось, по которой будут игнорироваться констрейнты. По-умолчанию констрейнты будут игнорироваться по обоим осям.

UnconstrainedBox используется довольно редко, об одном из примеров использования команда Flutter рассказала в следующем ролике:

Пример использования UnconstrainedBox — убираем констрейнты для Container:

1home: UnconstrainedBox(
2  child: Container(
3    color: Colors.red,
4    width: 100.0,
5    height: 100.0,
6  ),
7),

11

Как мы видим Container принимает желаемый размер, однако использование данного виджета может привести к ошибке — мы можем выйти за пределы доступных границ.

Пример использования UnconstrainedBox — выход за пределы доступных границ:

1home: UnconstrainedBox(
2  child: Container(
3    color: Colors.red,
4    width: 1000.0,
5    height: 1000.0,
6  ),
7),

10

В данном примере Container также принимает желаемый размер, но при этом возникает ошибка т.к. виджет не умещается.

OverflowBox

OverflowBox — виджет, который работает точно так же, как и UnconstrainedBox, но не вызывает ошибку если дочерний виджет не умещается.

Большинство ошибок в верстке возникают именно при работе с unbounded-констрейнтами стоит особое внимание уделять виджетам которые отдают unbounded-констрейнты, а также изучить как работать с виджетами которые помогают работать с unbounded констрейнтами

FittedBox

FittedBox — виджет, который позволяет «вписать» и расположить дочерний виджет в текущих границах.

FittedBox часто используется с «негибкими» виджетами (например картинки или подобные виджеты), размер которых неизвестен заранее и не зависит от текущих констрейнтов, и мы хотим встроить их с учетом текущих границ.

FittedBox стоит использовать, когда дочерний виджет больше или меньше границ в которых мы хотим его расположить. После стадии лейаута FittedBox производит трансформацию чтобы вписать дочерний виджет особым образом.

Мы можем определить каким образом вписать дочерний виджет с помощью трех параметров alignment (AlignmentGeometry, Alignment или AlignmentDirectional), clipBehavior (Clip) и fit (BoxFit).

BoxFit — может принимать одно из следующих значений (наглядные примеры можно увидеть в документации):

BoxFit.fill — дочерний виджет полностью заполнит текущие границы, при этом пропорции при необходимости будут изменены;

BoxFit.contain (дефолтное значение) — дочерний виджет полностью впишется в текущие границы, примет максимально возможный размер и при этом пропорции не будут изменены, по краям будет пустота если пропорции отличаются и передан соответствующий clipBehavior;

BoxFit.cover — дочерний виджет полностью заполнит текущие границы, примет минимально возможный размер и при этом пропорции не будут изменены, дочерний виджет будет “обрезан” по краям если пропорции отличаются и передан соответствующий clipBehavior;

BoxFit.fitWidth — дочерний виджет полностью заполнит текущие границы по ширине, при этом пропорции не будут изменены, дочерний виджет будет “обрезан” по краям если пропорции отличаются и передан соответствующий clipBehavior;

BoxFit.fitHeight — дочерний виджет полностью заполнит текущие границы по высоте, при этом пропорции не будут изменены, дочерний виджет будет “обрезан” по краям если пропорции отличаются и передан соответствующий clipBehavior;

BoxFit.none — не трансформирует размер дочернего виджета, при этом виджет может быть обрезан «по краям», если не умещается в текущих границах и передан соответствующий clipBehavior;

BoxFit.scaleDown — работает по сути так же, как и BoxFit.contain, но допускает только уменьшение дочернего виджета.

Например, вместо использования OverflowBox, чтобы избежать ошибки и полностью отобразить контент, мы можем воспользоваться виджетом FittedBox.

1home: FittedBox(
2  child: UnconstrainedBox(
3    child: Container(
4      color: Colors.red,
5      width: 1000.0,
6      height: 1000.0,
7    ),
8  ),
9),

17

Как мы видим, контейнер полностью был вписан в текущие границы, никаких ошибок нет и его пропорции сохранены.

Flex, Column и Row

Flex, Column и Row — виджеты, которые принимают список дочерних виджетов и размещают их в ряд один за другим в определённом направлении. Flex — это базовый виджет, который может работать и как Column, и как Row. Column и Row — это на самом деле обёртки над Flex.

Давайте рассмотрим работу с Flex-виджетами на примере работы с Row.

Row отдаёт дочерним виджетам констрейнты, которые по высоте равны полученным от родителя, а по ширине unbounded.

Пример использования Row:

1home: Material(
2        color: Colors.white,
3        child: Center(
4          child: Row(
5            children: [
6              Container(
7                color: Colors.red,
8                child: const Text(
9                  'Hello',
10                  style: TextStyle(fontSize: 56),
11                ),
12              ),
13              Container(
14                color: Colors.blue,
15                child: const Text(
16                  'Flutter',
17                  style: TextStyle(fontSize: 56),
18                ),
19              ),
20            ],
21          ),
22        ),
23      ),

18

В примере выше нет никаких проблем, давайте попробуем увеличить количество текста:

1home: Material(
2        color: Colors.white,
3        child: Center(
4          child: Row(
5            children: [
6              Container(
7                color: Colors.red,
8                child: const Text(
9                  'Hello',
10                  style: TextStyle(fontSize: 56),
11                ),
12              ),
13              Container(
14                color: Colors.blue,
15                child: const Text(
16                  'Lorem ipsum is placeholder text commonly used in the graphic',
17                  style: TextStyle(fontSize: 56),
18                ),
19              ),
20            ],
21          ),
22        ),
23      ),

8

Как мы видим, текст не уместился, и мы получили ошибку A RenderFlex overflowed by 3352 pixels on the right...

Дело в том, что Row — это особый виджет с особой логикой размещения виджетов и разработчиками фреймворка Flutter было принято решение оставить ответственность за констрейнты и размеры (Size) на разработчике.

Такое решение было принято потому, что фреймворк не может знать как именно вы хотите расположить виджеты и кому из них сколько выделить места. Например, если первому виджету передать всю доступную ширину, то он, возможно, займёт её целиком, и для остальных уже не останется места.

Таким образом все виджеты получают unbounded-констрейнты по ширине. Ответственность за то, что все виджеты уместятся — лежит на разработчике.

Чтобы разделить доступную ширину среди детей, есть три виджета — Flexible, Expanded и Spacer.

1const Flexible({
2  super.key,
3  this.flex = 1,
4  this.fit = FlexFit.loose,
5  required super.child,
6});
  • flex определяет вес среди всех детей
  • fit определяет констрейнты (loose или tight).
1const Expanded({
2  super.key,
3  super.flex,
4  required super.child,
5}) : super(fit: FlexFit.tight);

Expanded наследуется от Flexible и всегда имеет FlexFit.tight

1const Spacer({super.key, this.flex = 1})
2  : assert(flex != null),
3    assert(flex > 0);

Spacer это Expanded у которого нет child, позволяет заполнить пустотой оставшееся место

Чтобы исправить ошибку в нашем последнем примере, мы можем обернуть не уместившийся виджет в Expanded, тогда первый виджет займёт место, равное своему размеру, а второй — всё оставшееся место.

Пример использования Row c Expanded:

1home: Material(
2  color: Colors.white,
3  child: Center(
4    child: Row(
5      children: [
6        Container(
7          color: Colors.red,
8          child: const Text(
9            'Hello',
10            style: TextStyle(fontSize: 56),
11          ),
12        ),
13        Expanded(
14          child: Container(
15            color: Colors.blue,
16            child: const Text(
17              'Lorem ipsum is placeholder text commonly used in the graphic',
18              style: TextStyle(fontSize: 56),
19            ),
20          ),
21        ),
22      ],
23    ),
24  ),
25),

15

Первый виджет Text получил unbounded-констрейнты и занял столько места сколько ему нужно, а второй виджет Text получил bounded-констрейнты равные оставшемуся свободному месту и занял его, при этом текст переносится на новую строку..

Если мы обернём оба виджета в Expanded, то они оба получат bounded-констрейнты равные половине доступного места. Если мы хотим распределить место в других пропорциях, стоит воспользоваться параметром flex, так если первому виджету передать значение flex 1, а второму 3 - то первый займет 25% места, а второй 75% (т.е. свободное место будет определяться как доля flex текущего виджета от суммы всех flex).

fluttern

IntrinsicWidth и IntrinsicHeight

IntrinsicWidth и IntrinsicHeight — это специальные виджеты которые могут быть использованы в unbounded- и loose-констрейнтах, чтобы предотвратить «растягивание» виджета и определить максимальную ширину/высоту в зависимости от содержания виджетов.

Данные виджеты часто используются в Column и Row чтобы задать ширину/высоту для виджета равную максимальной из детей.

Пример использования Column без IntrinsicWidth:

1Material(
2        color: Colors.white,
3        child: Center(
4          child: Column(
5            mainAxisAlignment: MainAxisAlignment.center,
6            children: [
7              Container(
8                color: Colors.red,
9                child: const Text(
10                  'Hello',
11                  style: TextStyle(fontSize: 56),
12                ),
13              ),
14              Container(
15                color: Colors.blue,
16                child: const Text(
17                  'Flutter',
18                  style: TextStyle(fontSize: 56),
19                ),
20              ),
21              Container(
22                color: Colors.green,
23                child: const Text(
24                  'Handbook',
25                  style: TextStyle(fontSize: 56),
26                ),
27              ),
28            ],
29          ),
30        ),
31      ),

14

В данном примере мы видим, что каждый Container принимает ширину текста

Если нам необходимо выровнять всех детей Column по самому широкому элементу, мы можем воспользоваться виджетом IntrinsicWidth. Давайте обернём Column в IntrinsicWidth и добавим crossAxisAlignment: CrossAxisAlignment.stretch.

CrossAxisAlignment.stretch заставит всех детей принять ширину самого Column, а IntrinsicWidth предотвратит растягивание Column на всю ширину экрана и заставит его принять ширину по максимальной ширине контента.

Пример использования Column c IntrinsicWidth:

1home: Material(
2        color: Colors.white,
3        child: Center(
4          child: IntrinsicWidth(
5            child: Column(
6              mainAxisAlignment: MainAxisAlignment.center,
7              crossAxisAlignment: CrossAxisAlignment.stretch,
8              children: [
9                Container(
10                  color: Colors.red,
11                  child: const Text(
12                    'Hello',
13                    style: TextStyle(fontSize: 56),
14                  ),
15                ),
16                Container(
17                  color: Colors.blue,
18                  child: const Text(
19                    'Flutter',
20                    style: TextStyle(fontSize: 56),
21                  ),
22                ),
23                Container(
24                  color: Colors.green,
25                  child: const Text(
26                    'Handbook',
27                    style: TextStyle(fontSize: 56),
28                  ),
29                ),
30              ],
31            ),
32          ),
33        ),
34      ),

9

Важное замечание! Не стоит использовать intrinsic-виджеты если можно обойтись без них. Данный виджет добавляет спекулятивную фазу расчета лейаута перед финальной, что делает расчет лейаута дороже (в худшем случае сложность может быть O(N²) от размера пересчитываемого поддерева)

Виджеты со скроллом и списки

К виджетам со скроллом и спискам относятся SingleChildScrollView, ListView и многие другие (более подробно работа со списками будет рассмотрена в следующих параграфах).

У этих виджетов особым образом определяется их размер и констрейнты для дочерних виджетов:

  • они растягиваются по ширине и высоте на всё доступное место, поэтому их нельзя размещать в unbounded-констрейнтах;
  • по направлению скрола такие виджеты отдают unbounded-констрейнты;
  • ортогонально направлению скрола предоставляют tight-констрейнты.

Пример использования SingleChildScrollView:

1home: Material(
2  color: Colors.white,
3  child: SingleChildScrollView(
4    child: Column(
5      children: Colors.primaries
6          .map(
7            (color) => Container(
8              height: 200.0,
9              color: color,
10            ),
11          )
12          .toList(),
13    ),
14  ),
15),

Как мы видим SingleChildScrollView занял весь экран, список из Container скроллится и несмотря на то, что мы указали только height, в Container по ширине виджеты растянулись сами.

Давайте запустим LayoutExplorer выберем Column и посмотрим какие в данном случае будут констрейнты.

Screenshot

Column — получил от SingleChildScrollView tight-констрейнты по ширине (w = 390.0) и unbounded по высоте.

Container — получил от Column loose-констрейнты по ширине (0.0 <= w <= 390.0) и unbounded по высоте.

По высоте виджет Container определил свой размер согласно переданному значению height, а по ширине растянулся на все доступное место - это стандартное поведение виджета Container у которого нет дочернего виджета, в bounded-констрейнтах он занимает все свободное место, а в unbounded-констрейнтах становится минимально возможного размера.

fluttern

  • под констрейнтом понимается констрейнт по конкретной оси, по ширине или высоте;
  • ширина или высота — это, соответсвенно, значение width и height переданное в Container.

Пример использования SingleChildScrollView в Column, в unbounded-констрейнтах

1home: Material(
2  color: Colors.white,
3  child: Column(
4    children: [
5      SingleChildScrollView(
6        child: Column(
7          children: Colors.primaries
8              .map(
9                (color) => Container(
10                  height: 200.0,
11                  color: color,
12                ),
13              )
14              .toList(),
15        ),
16      ),
17      Container(
18        height: 100.0,
19        color: Colors.grey,
20      ),
21    ],
22  ),
23),

16

В данном примере мы получили ошибку A RenderFlex overflowed by 2856 pixels on the bottom.... Это происходит потому, что SingleChildScrollView пытается занять всё свободное место, в то время когда мы его помещаем в unbounded-констрейнты — эту ошибку можно исправить если обернуть SingleChildScrollView в Expanded, в таком случае мы получим следующий результат:

LimitedBox

LimitedBox — виджет, который позволяет ограничить размер дочернего виджета в случае, если текущие констрейнты — unbounded.

Это может быть полезным, когда мы хотим сделать адаптивный виджет, который может быть размещен и в bounded- и в unbounded-констрейнтах.

Например, мы могли воспользоваться LimitedBox вместо Expanded чтобы исправить ошибку и определить максимальный размер для SingleChildScrollView если он расположен в unbounded-констрейнтах

1home: Material(
2  color: Colors.white,
3  child: Column(
4    children: [
5      LimitedBox(
6        maxHeight: 500.0,
7        child: SingleChildScrollView(
8          child: Column(
9            children: Colors.primaries
10                .map(
11                  (color) => Container(
12                    height: 200.0,
13                    color: color,
14                  ),
15                )
16                .toList(),
17          ),
18        ),
19      ),
20      Container(
21        height: 200.0,
22        color: Colors.grey,
23      ),
24    ],
25  ),
26),

Stack

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

Чтобы спозиционировать виджет внутри Stack используется виджет Positioned (данный виджет обязательно должен использоваться внутри Stack).

1  const Positioned({
2    super.key,
3    this.left,
4    this.top,
5    this.right,
6    this.bottom,
7    this.width,
8    this.height,
9    required super.child,
10  })

Positioned — это виджет который управляет, где относительно самого Stack будет спозиционирован child.

left, top, right, bottom — отступы от краёв Stack

width — ширина child (одновременно могут быть заданы не более 2 значений из left, right, width)

height — высота child (одновременно могут быть заданы не более 2 значений из top, bottom, height)

Параметр с помощью которого управляются констрейнты называется fit (StackFit)

StackFit.loose — всем виджетам передаются loose констрейнты с максимальной шириной и выстой равными ширине и высоте самого Stack

StackFit.expand — неспозиционнированные виджеты получают tight-констрейнты равные максимально возможным, спозиционнированые получают unbounded- loose-констрейнты

StackFit.passthrough - неспозиционнированные дети получают констрейнты которые Stack получил от родителя, спозиционнированые получают loose констрейнты

StackFit.passthrough часто используется, когда Stack находится в Row/Column - например, если Stack находится в Row и обернут в Expanded, он будет иметь tight-констрейнты по горизонтали и loose-констрейнты по вертикали.

Пример использования Stack с StackFit.loose

1home: Material(
2  color: Colors.white,
3  child: Center(
4    child: SizedBox(
5      width: 350.0,
6      height: 350.0,
7      child: Container(
8        color: Colors.black12,
9        child: Stack(
10          alignment: Alignment.center,
11          fit: StackFit.loose,
12          children: [
13            Container(
14              color: Colors.red,
15              height: 100.0,
16              width: 100.0,
17            ),
18            Container(
19              color: Colors.blue,
20              height: 50.0,
21              width: 50.0,
22            ),
23            Positioned(
24              bottom: 25.0,
25              child: Container(
26                color: Colors.green,
27                height: 100.0,
28                width: 100.0,
29              ),
30            ),
31            Positioned(
32              bottom: 50.0,
33              child: Container(
34                color: Colors.yellow,
35                height: 50.0,
36                width: 50.0,
37              ),
38            ),
39          ],
40        ),
41      ),
42    ),
43  ),
44),

12

Пример использования Stack с StackFit.expand

13

Заключение

Итак, в этом параграфе мы ознакомились с основными принципами лейаута во Flutter и рассмотрели базовые layout-виджеты.

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

Каждый раз, когда сталкиваетесь с новым виджетом, рекомендуем начинать с документации — команда Flutter проводит отличную работу по документированию, а также снимает короткие видеоролики в которых наглядно демонстрируется поведение и примеры использования различных компонентов Flutter.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф2.9. Widgets: keys
Следующий параграф2.11. Widgets: формы и кнопки