2.14. Виджеты: основные лейаут-виджеты

Благодаря предыдущему параграфу мы уже знаем, что такое констрейнты и как работает расчёт лейаута во Flutter.

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

Давайте приступим!

Align

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

  • Если Align имеет bounded - констрейнты, то он занимает максимально доступное место.
  • Если находится в unbounded - констрейнтах, то становится размером с дочерний виджет в тех направлениях в которых имеет unbounded значение констрейнта.
Пример 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

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

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

AlignmentGeometry

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

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

Alignment

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

Все возможные значения Alignment

fluttern

AlignmentDirectional

AlignmentDirectional ****— класс, который позволяет задать основные позиции для дочернего виджета с учётом направления текста, которое используется в приложении.

Все возможные значения AlignmentDirectional

fluttern

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 — убираем констрейнты для Container:

Пример

Убираем констрейнты для Container:

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

Результат:

11

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

Пример

Выход за пределы доступных границ:

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

Видим:

10

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

У UnconstrainedBox есть уже знакомый нам параметр alignment (AlignmentGeometry, Alignment или AlignmentDirectional), а также особый параметр constrainedAxis (Axis).

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

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

OverflowBox

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

Большинство ошибок в верстке возникают именно при работе с unbounded - констрейнтами.

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

FittedBox

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

Пример

Вместо использования 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

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

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

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

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

С параметром alignment вы уже знакомы, а значения параметра fit стоит разобрать подробнее.

Значения параметра fit

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

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

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

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

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

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

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

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

Flex, Column и Row

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

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

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

Пример
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      ),

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

8

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

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

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

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

1const Flexible({
2  super.key,
3  this.flex = 1,
4  this.fit = FlexFit.loose,
5  required super.child,
6});

Параметр flex определяет вес среди всех детей, а fit — констрейнты (loose или tight).

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

1const Expanded({
2  super.key,
3  super.flex,
4  required super.child,
5}) : super(fit: FlexFit.tight);

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

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

Чтобы исправить ошибку в нашем последнем примере, мы можем обернуть не уместившийся виджет в 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      ),

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

14

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

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

Пример
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²) от размера пересчитываемого поддерева.

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

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

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

  • они растягиваются по ширине и высоте на всё доступное место, поэтому их нельзя размещать в 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 в DevTools, выберем 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-констрейнтах

Пример использования 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),

В данном примере мы получили ошибку A RenderFlex overflowed by 2856 pixels on the bottom....

16

Это происходит потому, что 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.

Подробнее о его параметрах
  • 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-констрейнты равные максимально возможным, спозиционнированые получают unboundedloose-констрейнты

  • 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

А если установить значение StackFit.expand, получится:

13


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

Чтобы закрепить знания традиционно советуем пройти квиз.

А в следующем параграфе мы завершим наш разговор о виджетах — поговорим о том, как принимать данные от пользователя с помощью форм, валидировать значения инпутов и управлять их состоянием.

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E

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

Предыдущий параграф2.13. Виджеты: лейаут
Следующий параграф2.15. Виджеты: формы и кнопки