Start Debugging

Решение: RenderBox was not laid out в Flutter

RenderBox was not laid out почти всегда вторичная ошибка. Найдите первое утверждение layout выше, обычно это scrollable с неограниченными ограничениями, и исправьте его.

RenderBox was not laid out означает, что Flutter попытался отрисовать или выполнить hit-test для render box, размер которого так и не был вычислен. Это почти всегда производная ошибка: более раннее утверждение layout прервало performLayout для части дерева, и это сообщение лишь обломки. Настоящее решение в том, чтобы прокрутить консоль вверх до первой ошибки, которая обычно представляет собой scrollable (ListView, GridView, SingleChildScrollView), получивший неограниченные ограничения по своей оси прокрутки. Ограничьте этот виджет через Expanded, фиксированный размер или shrinkWrap, и ошибка исчезнет. В этом руководстве используется Flutter 3.44 (стабильный, май 2026) и Dart 3.x.

Срабатывает утверждение hasSize в package:flutter/src/rendering/box.dart. RenderBox получает размер только во время своего прохода performLayout. Если layout для этого box ни разу не отработал успешно, запрос .size (что делают и отрисовка, и hit-testing) активирует защиту. Поэтому сообщение точное, но само по себе бесполезное: оно называет жертву, а не виновника.

Ошибка в контексте

Блок в консоли выглядит так. Точное имя виджета и шестнадцатеричные ID меняются, но форма постоянна:

======== Exception caught by rendering library =====================
The following assertion was thrown during performLayout():
RenderBox was not laid out: RenderShrinkWrappingViewport#4aefd
  relayoutBoundary=up13 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
'package:flutter/src/rendering/box.dart': Failed assertion: line 1966
  pos 12: 'hasSize'

The relevant error-causing widget was:
  ListView  lib/widgets/feed.dart:58
====================================================================

Важны две детали. Во-первых, названный render object (RenderShrinkWrappingViewport, RenderPadding, RenderRepaintBoundary, что угодно) говорит, какое поддерево не смогло выполнить layout. Во-вторых, что важнее, это редко единственное исключение. В режиме debug Flutter печатает первый сбой, а затем продолжает попытки рендеринга, что порождает каскад этих утверждений hasSize. Сообщение, на которое нужно реагировать, находится наверху каскада, а не там, куда упал ваш взгляд.

Почему это происходит

RenderBox имеет размер только после того, как performLayout назначит его. Три ситуации оставляют box без размера:

Box получил ограничения, которые он не может удовлетворить, поэтому его собственный performLayout выбросил исключение. Классический случай это scrollable, получивший неограниченное ограничение по своей оси прокрутки. Вертикальному ListView нужна ограниченная высота, чтобы знать, сколько viewport рендерить; дайте ему бесконечную высоту, и он выбросит Vertical viewport was given unbounded height, layout прервётся, и каждый предок, который затем попытается прочитать свой размер, сообщит RenderBox was not laid out.

Родитель отрисовал или выполнил hit-test для ребёнка, не выполнив сначала его layout. Это баг пользовательского RenderObject: вы написали render object, чей paint читает child.size, но чей performLayout забыл вызвать child.layout(...). Ребёнок так и не получил размер.

Кто-то прочитал .size вне фазы layout. Чтение context.size или renderBox.size во время build, initState или синхронного колбэка, до того как первый кадр выполнил layout виджета, активирует то же самое утверждение. Размер попросту ещё не существует.

Объединяющее правило это контракт layout во Flutter: ограничения идут вниз, размеры возвращаются вверх, и размер box действителен только между завершением его performLayout и следующим markNeedsLayout. Подробнее на официальной странице Understanding constraints, самом полезном документе для любой ошибки layout во Flutter.

Минимальное воспроизведение, которое можно вставить в новое приложение

Самый частый триггер на сегодня: ListView, помещённый непосредственно внутрь Column. Column даёт своим детям неограниченную высоту по главной оси, ListView хочет ограниченную высоту, и layout падает.

// Flutter 3.44, Dart 3.x -- throws, layout aborts, "RenderBox was not laid out" follows.
import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: FeedScreen()));

class FeedScreen extends StatelessWidget {
  const FeedScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          const Text('Latest'),
          // ListView inside a Column: unbounded height on the main axis.
          ListView(
            children: const [
              ListTile(title: Text('One')),
              ListTile(title: Text('Two')),
              ListTile(title: Text('Three')),
            ],
          ),
        ],
      ),
    );
  }
}

Запустите это, и первая ошибка в консоли это Vertical viewport was given unbounded height. Утверждения RenderBox was not laid out под ней это следствие, а не причина. Решение в том, чтобы ограничить ListView.

Решение, подробно

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

1. Задайте scrollable ограниченный размер через Expanded (рекомендуется)

Когда scrollable живёт внутри Column или Row и должен заполнить оставшееся пространство, оберните его в Expanded. Expanded передаёт ребёнку жёсткое (tight), ограниченное ограничение по главной оси, именно то, что нужно viewport:

// Flutter 3.44, Dart 3.x -- Expanded gives the ListView a bounded height.
Column(
  children: [
    const Text('Latest'),
    Expanded(
      child: ListView(
        children: const [
          ListTile(title: Text('One')),
          ListTile(title: Text('Two')),
          ListTile(title: Text('Three')),
        ],
      ),
    ),
  ],
)

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

2. Используйте shrinkWrap, когда список короткий и должен подстраиваться под содержимое

Если список действительно маленький и конечный (горстка строк настроек, фиксированное меню) и вы хотите, чтобы он занимал только высоту своего содержимого, задайте shrinkWrap: true. Это говорит ListView измерить своих детей и сообщить их суммарную высоту вместо требования ограниченного viewport:

// Flutter 3.44, Dart 3.x -- shrinkWrap sizes the list to its children.
Column(
  children: [
    const Text('Settings'),
    ListView(
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      children: const [
        ListTile(title: Text('Profile')),
        ListTile(title: Text('Notifications')),
        ListTile(title: Text('Privacy')),
      ],
    ),
  ],
)

Компромисс реален: shrinkWrap выполняет layout всех детей заранее, сводя на нет ленивый рендеринг, который делает ListView дешёвым. Используйте его только для коротких ограниченных списков. Для всего, что может вырасти до десятков элементов, вернитесь к решению 1. Добавление physics: NeverScrollableScrollPhysics() не даёт внутреннему списку прокручиваться независимо, что обычно и нужно, когда внешний Column является поверхностью прокрутки.

3. Задайте box явное ограниченное ограничение

Иногда верный ответ это конкретный размер. SizedBox с фиксированной высотой или ConstrainedBox с максимальной высотой даёт scrollable границу, с которой можно работать:

// Flutter 3.44, Dart 3.x -- a fixed viewport height for a horizontal carousel.
SizedBox(
  height: 200,
  child: ListView(
    scrollDirection: Axis.horizontal,
    children: const [/* cards */],
  ),
)

Горизонтальный ListView внутри Column это зеркальное отражение воспроизведения: Column ограничивает ширину, но оставляет высоту неограниченной, а горизонтальному viewport нужна ограниченная высота. Фиксированная height решает это чисто. Используйте ConstrainedBox(constraints: BoxConstraints(maxHeight: 300)), когда содержимое может быть короче предела.

4. Выполните layout ребёнка прежде, чем читать его размер в пользовательском RenderObject

Если вы написали пользовательский RenderObject (или подкласс RenderBox), утверждение говорит вам, что performLayout обратился к размеру ребёнка до выполнения его layout. Всегда вызывайте child.layout(...) перед чтением child.size:

// Flutter 3.44, Dart 3.x -- lay out the child, THEN read its size.
@override
void performLayout() {
  final BoxConstraints childConstraints = constraints.loosen();
  child!.layout(childConstraints, parentUsesSize: true); // must come first
  size = constraints.constrain(child!.size);              // now .size is valid
}

Флаг parentUsesSize: true обязателен, когда собственный размер родителя зависит от ребёнка. Опустите его, и Flutter может пропустить relayout при изменении ребёнка, что порождает устаревшие layout, выглядящие как эта же ошибка с перерывами. Контракт задокументирован на странице API RenderBox.size: размер действителен только во время и после performLayout, а чтение его из родителя требует parentUsesSize: true в момент layout.

5. Отложите чтение размера до момента после первого кадра

Если вам нужен отрисованный размер виджета в Dart (чтобы спозиционировать overlay, задать размер соседу или вернуть измерение обратно в состояние), не читайте context.size во время build. Render box ещё не прошёл layout. Прочитайте его после кадра:

// Flutter 3.44, Dart 3.x -- the size exists only after layout has run.
@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (!mounted) return;
    final Size? size = context.size; // valid now: the frame has been laid out
    setState(() => _measuredHeight = size?.height);
  });
}

Чтобы измерять во время layout, а не после него, обратитесь к LayoutBuilder (который передаёт вам ограничения родителя) или к RenderObject с parentUsesSize: true, а не к чтению размера после кадра. Подход с чтением после кадра предназначен для случая “мне просто нужны финальные пиксели один раз”.

Подводные камни и похожие ошибки

Как быстро найти настоящего виновника

Поскольку эта ошибка каскадирует, самый быстрый путь это читать консоль сверху вниз и остановиться на первом исключении. Затем сузьте поддерево:

  1. Первая ошибка называет виджет и место в исходнике (lib/widgets/feed.dart:58). Откройте этот файл и посмотрите, что является родителем названного виджета. Scrollable, чей родитель это Column, Row, IntrinsicHeight или другой scrollable, и есть ваш подозреваемый.
  2. Включите debugPaintSizeEnabled = true; в main (или переключите Debug Paint во Flutter Inspector), чтобы видеть контур каждого box. Box, который ничего не рисует или схлопывается в линию, и есть тот, у которого не получился layout.
  3. Откройте Layout Explorer в DevTools и выберите падающий виджет. Его панель ограничений покажет, получил ли он h=unbounded или w=unbounded, что подтверждает диагноз. Если вы не пользовались DevTools для работы с layout, разбор в профилировании джанка приложения Flutter с DevTools охватывает открытие сессии на реальном устройстве; та же сессия управляет Layout Explorer.

Более глубокий урок в том, что RenderBox was not laid out никогда не является самим багом. Это Flutter сообщает, что не смог закончить работу, начатую более ранним виджетом. Приучите себя игнорировать самое громкое сообщение и находить первое, тихое, и эта ошибка перестанет быть загадкой. Держите свои scrollable ограниченными, выполняйте layout детей до их измерения и никогда не читайте размер до кадра, который его создаёт, и утверждение никогда не сработает.

Связанное

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад