Благодаря предыдущему параграфу мы уже знаем, что такое констрейнты и как работает расчёт лейаута во 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),
Видим:
У Align
есть два параметра widthFactor
и heightFactor
, которые позволяют задать размер Align
пропроционально размеру дочернего виджета, а также есть параметр alignment
(AlignmentGeometry
, Alignment
или AlignmentDirectional
), который определяет, где именно нужно спозиционировать дочерний виджет.
Расскажем о них подробнее.
AlignmentGeometry
AlignmentGeometry
— базовый абстрактный класс, у которого определены два поля x
и y
. Эти два поля задают положение дочернего виджета следующим образом
- (0.0, 0.0) — дочерний виджет будет по-центру;
- (-1.0, -1.0) — дочерний виджет будет в верхнем-левом углу;
- (1.0, 1.0) — дочерний виджет будет в нижнем-право углу.
Alignment
Alignment
— класс, который позволяет задать основные позиции для дочернего виджета без учёта направления текста в приложении (в некоторых языках направление текста идет справа налево).
Все возможные значения Alignment
AlignmentDirectional
AlignmentDirectional
****— класс, который позволяет задать основные позиции для дочернего виджета с учётом направления текста, которое используется в приложении.
Все возможные значения AlignmentDirectional
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),
Видим:
Как мы видим 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),
Видим:
Как видим, 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),
Результат:
Как мы видим Container
принимает желаемый размер, однако использование данного виджета может привести к ошибке — мы можем выйти за пределы доступных границ.
Пример
Выход за пределы доступных границ:
1home: UnconstrainedBox(
2 child: Container(
3 color: Colors.red,
4 width: 1000.0,
5 height: 1000.0,
6 ),
7),
Видим:
В примере выше 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),
Получаем:
Как мы видим, контейнер полностью был вписан в текущие границы, никаких ошибок нет и его пропорции сохранены.
FittedBox
часто используется с «негибкими» виджетами (например картинки или подобные виджеты), размер которых неизвестен заранее и не зависит от текущих констрейнтов, и мы хотим встроить их с учетом текущих границ.
Этот виджет стоит использовать, когда дочерний виджет больше или меньше границ, в которых мы хотим его расположить. После стадии лейаута FittedBox
производит трансформацию, чтобы вписать дочерний виджет особым образом.
Мы можем определить каким образом вписать дочерний виджет с помощью трех параметров alignment
(AlignmentGeometry
, Alignment
или 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
Flex
, Column
и Row
— виджеты, которые принимают список дочерних виджетов и размещают их в ряд один за другим в определённом направлении. Flex
— это базовый виджет, который может работать и как Column
, и как Row
. Column
и 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 ),
В примере выше нет никаких проблем, давайте попробуем увеличить количество текста:
Пример
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...
Дело в том, что 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
).
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),
Первый виджет Text
получил unbounded
-констрейнты и занял столько места сколько ему нужно, а второй виджет Text
получил bounded
-констрейнты, равные оставшемуся свободному месту, и занял его, при этом текст переносится на новую строку.
Если мы обернём оба виджета в Expanded
, то они оба получат bounded
-констрейнты равные половине доступного места. Если мы хотим распределить место в других пропорциях, стоит воспользоваться параметром flex
, так если первому виджету передать значение flex
1, а второму 3 — то первый займет 25% места, а второй 75% (т.е. свободное место будет определяться как доля flex
текущего виджета от суммы всех flex
).
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
принимает ширину текста:
Если нам необходимо выровнять всех детей 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 ),
👉 Важно: не стоит использовать
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 в DevTools, выберем Column
и посмотрим какие в данном случае будут констрейнты:

Видим, что виджет Column
получил от SingleChildScrollView
tight
-констрейнты по ширине (w = 390.0
) и unbounded
по высоте. А Container
получил от Column
loose
-констрейнты по ширине (0.0 <= w <= 390.0
) и unbounded по высоте.
По высоте виджет Container
определил свой размер согласно переданному значению height
, а по ширине растянулся на всё доступное место — это стандартное поведение виджета Container
, у которого нет дочернего виджета, в bounded
-констрейнтах он занимает все свободное место, а в unbounded
-констрейнтах становится минимально возможного размера.
Вот как это можно представить на иллюстрации.
Здесь под констрейнтом
понимается констрейнт по конкретной оси, по ширине или высоте. А ширина или высота — это, соответсвенно, значение 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....
Это происходит потому, что 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
-констрейнты равные максимально возможным, спозиционнированые получают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),
Видим:
А если установить значение StackFit.expand
, получится:
Вот и всё! Конечно, это далеко не все виджеты, которые нам предоставляет фреймворк, но разобравшись с ними будет довольно просто разобраться с остальными.
Чтобы закрепить знания традиционно советуем пройти квиз.
А в следующем параграфе мы завершим наш разговор о виджетах — поговорим о том, как принимать данные от пользователя с помощью форм, валидировать значения инпутов и управлять их состоянием.