Раскладка во Flutter устроена одним правилом из документации: constraints go down, sizes go up, parent sets position. Ограничения спускаются вниз, размеры поднимаются вверх, позицию задаёт родитель. Почти каждая ошибка вёрстки объясняется этим правилом.
Как работает проход раскладки
- Родитель передаёт ребёнку
BoxConstraints: минимальную и максимальную ширину и высоту. - Ребёнок выбирает свой размер внутри этих ограничений и сообщает его родителю.
- Родитель решает, где ребёнка расположить.
Ребёнок не знает своей позиции и не может выбрать размер вне ограничений. Поэтому Container(width: 100) на весь экран растягивается, если родитель дал жёсткие ограничения (tight): например, корень приложения требует ровно размер экрана. Чтобы ребёнок мог быть меньше, нужен родитель, ослабляющий ограничения: Center, Align, UnconstrainedBox.
- Tight: min = max, размер задан жёстко.
- Loose: min = 0, ребёнок может быть меньше максимума.
- Unbounded: max = бесконечность. Так
Columnдаёт детям высоту, аListViewдаёт длину по оси прокрутки.
Row и Column
Row и Column это Flex. Раскладка в два шага: сначала дети без flex получают неограниченный размер по главной оси и меряют себя, потом оставшееся место делится между Expanded и Flexible пропорционально flex.
Expandedзаставляет ребёнка занять всю свою долю (tight).Flexibleразрешает занять до своей доли (loose).mainAxisAlignmentиcrossAxisAlignmentдвигают детей,mainAxisSize: MainAxisSize.minсжимает самRowпо содержимому.
Три классические ошибки
Жёлто-чёрная полоса «RenderFlex overflowed». Дети Row шире экрана: длинный текст не переносится, потому что получил неограниченную ширину. Лечится Expanded/Flexible вокруг текста или overflow: TextOverflow.ellipsis внутри Expanded.
«Vertical viewport was given unbounded height». ListView положили прямо в Column. Column даёт ему бесконечную высоту, а список хочет занять всю доступную. Решения: обернуть в Expanded (чаще всего правильно), задать высоту через SizedBox, или shrinkWrap: true (дорого для длинных списков: строит все элементы).
«BoxConstraints forces an infinite width». Ребёнок хочет бесконечную ширину внутри неограниченного родителя, например TextField в Row без Expanded.
Stack и позиционирование
Stack кладёт детей друг на друга. Непозиционированные дети выравниваются по alignment, Positioned задаёт отступы от краёв. Размер Stack по умолчанию определяется непозиционированными детьми. Всё, что выходит за его границы, обрезается (clipBehavior: Clip.hardEdge по умолчанию).
Прокрутка и слайверы
ListView.builder строит только видимые элементы, поэтому подходит для длинных списков. SingleChildScrollView с Column строит всё сразу: только для коротких экранов вроде формы. Для сложных экранов (сворачиваемый заголовок, сетка и список в одной прокрутке) используют CustomScrollView со слайверами: SliverAppBar, SliverList, SliverGrid. Слайвер получает не BoxConstraints, а SliverConstraints: сколько уже прокручено и сколько видно.
Адаптивность
MediaQuery.sizeOf(context) даёт размер экрана и подписывает только на размер, а не на весь MediaQuery (меньше перестроений, чем MediaQuery.of). LayoutBuilder даёт ограничения от родителя, что точнее для компонента внутри панели. SafeArea убирает вырезы и системные панели.
Что спросят на собесе
- Сформулируйте правило раскладки и объясните его на примере.
- Чем
Expandedотличается отFlexible. - Почему
ListViewвнутриColumnпадает и как это исправить. - Tight, loose и unbounded ограничения.
- Когда нужен
CustomScrollViewи слайверы, чем плохshrinkWrap.