Введение

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

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

Подход к лейауту во Flutter отличается от подходов в html/css или нативной мобильной разработке. Сначала он может показаться странным и неудобным, но это дело привычки — если изучить основные принципы все становится понятным и легким.

Давайте рассмотрим небольшой пример:

class MyApp extends StatelessWidget {
  static const appTitle = 'Flutter Handbook Demo';

  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: appTitle,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: Container(
        color: Colors.red,
        width: 100.0,
        height: 100.0,
      ),
    );
  }
}

Мы добавляем виджет Container , задаем ему красный фон и указываем размер 100 на 100.

Как вы думаете что будет выведено на экран?

Результат

7

Странно – почему-то виджет Container получился размером не 100 на 100, а на весь экран.

Во Flutter есть специальный виджет SizedBox – это виджет который задает размер для дочернего виджета (подробней о нем поговорим далее).

Давайте попробуем обернуть наш Container в виджет SizedBox размером 100 на 100 и посмотрим, что из этого получится:

class MyApp extends StatelessWidget {
  static const appTitle = 'Flutter Handbook Demo';

  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: appTitle,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: SizedBox(
        height: 100.0,
        width: 100.0,
        child: Container(
          color: Colors.red,
          width: 100.0,
          height: 100.0,
        ),
      ),
    );
  }
}

Запускаем приложение и видим ту же самую проблему — виджет Container вновь занимает весь экран.

6

А теперь давайте попробуем обернуть наш Container в виджет Center и посмотрим изменится ли что-нибудь:

class MyApp extends StatelessWidget {
  static const appTitle = 'Flutter Handbook Demo';

  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: appTitle,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: Center(
        child: Container(
          color: Colors.red,
          width: 100.0,
          height: 100.0,
        ),
      ),
    );
  }
}

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 – на каждый вызов выводим в консоль значение констрейнтов:

class MyApp extends StatelessWidget {
  static const appTitle = 'Flutter Handbook Demo';

  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: appTitle,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: Center(
        child: LayoutBuilder(builder: (context, constraints) {
          print(constraints);
          return Container(
            color: Colors.red,
            width: 100.0,
            height: 100.0,
          );
        }),
      ),
    );
  }
}

При запуске приложения мы увидим, что на каждый 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 — размещаем виджет сверху-справа:

home: Align(
  alignment: Alignment.topRight,
  child: Container(
    color: Colors.red,
    width: 100.0,
    height: 100.0,
  ),
),

19

Center

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

ConstrainedBox

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

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

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

home: ConstrainedBox(
  constraints: BoxConstraints.tight(Size(100, 100.0)),
  child: Container(
    color: Colors.red,
    width: 100.0,
    height: 100.0,
  ),
),

4

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

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

home: Center(
  child: ConstrainedBox(
    constraints: BoxConstraints.tight(Size(50.0, 50.0)),
    child: Container(
      color: Colors.red,
      width: 100.0,
      height: 100.0,
    ),
  ),
),

3

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

UnconstrainedBox

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

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

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

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

home: UnconstrainedBox(
  child: Container(
    color: Colors.red,
    width: 100.0,
    height: 100.0,
  ),
),

11

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

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

home: UnconstrainedBox(
  child: Container(
    color: Colors.red,
    width: 1000.0,
    height: 1000.0,
  ),
),

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.

home: FittedBox(
  child: UnconstrainedBox(
    child: Container(
      color: Colors.red,
      width: 1000.0,
      height: 1000.0,
    ),
  ),
),

17

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

Flex, Column и Row

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

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

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

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

home: Material(
        color: Colors.white,
        child: Center(
          child: Row(
            children: [
              Container(
                color: Colors.red,
                child: const Text(
                  'Hello',
                  style: TextStyle(fontSize: 56),
                ),
              ),
              Container(
                color: Colors.blue,
                child: const Text(
                  'Flutter',
                  style: TextStyle(fontSize: 56),
                ),
              ),
            ],
          ),
        ),
      ),

18

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

home: Material(
        color: Colors.white,
        child: Center(
          child: Row(
            children: [
              Container(
                color: Colors.red,
                child: const Text(
                  'Hello',
                  style: TextStyle(fontSize: 56),
                ),
              ),
              Container(
                color: Colors.blue,
                child: const Text(
                  'Lorem ipsum is placeholder text commonly used in the graphic',
                  style: TextStyle(fontSize: 56),
                ),
              ),
            ],
          ),
        ),
      ),

8

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

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

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

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

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

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

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

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

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

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

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

home: Material(
  color: Colors.white,
  child: Center(
    child: Row(
      children: [
        Container(
          color: Colors.red,
          child: const Text(
            'Hello',
            style: TextStyle(fontSize: 56),
          ),
        ),
        Expanded(
          child: Container(
            color: Colors.blue,
            child: const Text(
              'Lorem ipsum is placeholder text commonly used in the graphic',
              style: TextStyle(fontSize: 56),
            ),
          ),
        ),
      ],
    ),
  ),
),

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:

Material(
        color: Colors.white,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Container(
                color: Colors.red,
                child: const Text(
                  'Hello',
                  style: TextStyle(fontSize: 56),
                ),
              ),
              Container(
                color: Colors.blue,
                child: const Text(
                  'Flutter',
                  style: TextStyle(fontSize: 56),
                ),
              ),
              Container(
                color: Colors.green,
                child: const Text(
                  'Handbook',
                  style: TextStyle(fontSize: 56),
                ),
              ),
            ],
          ),
        ),
      ),

14

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

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

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

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

home: Material(
        color: Colors.white,
        child: Center(
          child: IntrinsicWidth(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Container(
                  color: Colors.red,
                  child: const Text(
                    'Hello',
                    style: TextStyle(fontSize: 56),
                  ),
                ),
                Container(
                  color: Colors.blue,
                  child: const Text(
                    'Flutter',
                    style: TextStyle(fontSize: 56),
                  ),
                ),
                Container(
                  color: Colors.green,
                  child: const Text(
                    'Handbook',
                    style: TextStyle(fontSize: 56),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),

9

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

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

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

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

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

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

home: Material(
  color: Colors.white,
  child: SingleChildScrollView(
    child: Column(
      children: Colors.primaries
          .map(
            (color) => Container(
              height: 200.0,
              color: color,
            ),
          )
          .toList(),
    ),
  ),
),

Как мы видим 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-констрейнтах

home: Material(
  color: Colors.white,
  child: Column(
    children: [
      SingleChildScrollView(
        child: Column(
          children: Colors.primaries
              .map(
                (color) => Container(
                  height: 200.0,
                  color: color,
                ),
              )
              .toList(),
        ),
      ),
      Container(
        height: 100.0,
        color: Colors.grey,
      ),
    ],
  ),
),

16

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

LimitedBox

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

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

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

home: Material(
  color: Colors.white,
  child: Column(
    children: [
      LimitedBox(
        maxHeight: 500.0,
        child: SingleChildScrollView(
          child: Column(
            children: Colors.primaries
                .map(
                  (color) => Container(
                    height: 200.0,
                    color: color,
                  ),
                )
                .toList(),
          ),
        ),
      ),
      Container(
        height: 200.0,
        color: Colors.grey,
      ),
    ],
  ),
),

Stack

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

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

  const Positioned({
    super.key,
    this.left,
    this.top,
    this.right,
    this.bottom,
    this.width,
    this.height,
    required super.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

home: Material(
  color: Colors.white,
  child: Center(
    child: SizedBox(
      width: 350.0,
      height: 350.0,
      child: Container(
        color: Colors.black12,
        child: Stack(
          alignment: Alignment.center,
          fit: StackFit.loose,
          children: [
            Container(
              color: Colors.red,
              height: 100.0,
              width: 100.0,
            ),
            Container(
              color: Colors.blue,
              height: 50.0,
              width: 50.0,
            ),
            Positioned(
              bottom: 25.0,
              child: Container(
                color: Colors.green,
                height: 100.0,
                width: 100.0,
              ),
            ),
            Positioned(
              bottom: 50.0,
              child: Container(
                color: Colors.yellow,
                height: 50.0,
                width: 50.0,
              ),
            ),
          ],
        ),
      ),
    ),
  ),
),

12

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

13

Заключение

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

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

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

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

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

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