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