Start Debugging

Correção: RenderBox was not laid out no Flutter

RenderBox was not laid out quase sempre é um erro secundário. Procure a primeira asserção de layout acima dele, geralmente um scrollable com restrições não limitadas, e corrija isso.

RenderBox was not laid out significa que o Flutter tentou pintar ou fazer hit-test em um render box cujo tamanho nunca foi calculado. Quase sempre é um erro derivado: uma asserção de layout anterior abortou o performLayout para parte da árvore, e esta mensagem é apenas os destroços. A correção real é subir até o primeiro erro do console, que geralmente é um scrollable (ListView, GridView, SingleChildScrollView) ao qual foram dadas restrições não limitadas no seu eixo de rolagem. Limite esse widget com Expanded, um tamanho fixo ou shrinkWrap e isso desaparece. Este guia usa Flutter 3.44 (estável, maio de 2026) e Dart 3.x.

A asserção que é lançada é hasSize em package:flutter/src/rendering/box.dart. Um RenderBox só recebe um tamanho durante a sua passada de performLayout. Se o layout nunca rodou com sucesso para esse box, pedir .size (algo que tanto a pintura quanto o hit-testing fazem) aciona a guarda. Por isso a mensagem é precisa mas pouco útil sozinha: ela nomeia a vítima, não o culpado.

O erro em contexto

O bloco do console fica assim. O nome exato do widget e os IDs em hexadecimal mudam, mas o formato é constante:

======== 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
====================================================================

Dois detalhes importam. Primeiro, o render object nomeado (RenderShrinkWrappingViewport, RenderPadding, RenderRepaintBoundary, seja qual for) diz qual subárvore falhou ao fazer layout. Segundo, e mais importante, raramente esta é a única exceção. Em modo debug, o Flutter imprime a primeira falha e depois continua tentando renderizar, o que produz uma cascata dessas asserções hasSize. A mensagem sobre a qual você deve agir é a do topo da cascata, não aquela em que seu olhar pousa.

Por que isso acontece

Um RenderBox tem um tamanho somente depois que o performLayout atribui um. Três situações deixam um box sem tamanho:

O box recebeu restrições que não consegue satisfazer, então o seu próprio performLayout lançou uma exceção. O caso clássico é um scrollable que recebeu uma restrição não limitada no seu eixo de rolagem. Um ListView vertical precisa de uma altura limitada para saber quanto viewport renderizar; dê a ele altura infinita e ele lança Vertical viewport was given unbounded height, o layout aborta, e cada ancestral que depois tentar ler o seu tamanho reporta RenderBox was not laid out.

Um pai pintou ou fez hit-test em um filho sem fazer layout dele primeiro. Este é o bug do RenderObject personalizado: você escreveu um render object cujo paintchild.size mas cujo performLayout esqueceu de chamar child.layout(...). O filho nunca recebeu um tamanho.

Alguém leu .size fora da fase de layout. Ler context.size ou renderBox.size durante build, initState ou um callback síncrono, antes de o primeiro frame ter feito o layout do widget, aciona a mesma asserção. O tamanho simplesmente ainda não existe.

A regra unificadora é o contrato de layout do Flutter: as restrições descem, os tamanhos sobem, e o tamanho de um box é válido somente entre o fim do seu performLayout e o próximo markNeedsLayout. Leia mais na página oficial Understanding constraints, que é o documento mais útil para todo erro de layout no Flutter.

Uma reprodução mínima que você pode colar em um app novo

O gatilho mais comum de longe: um ListView colocado diretamente dentro de um Column. O Column dá aos seus filhos altura não limitada no eixo principal, o ListView quer uma altura limitada, e o layout falha.

// 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')),
            ],
          ),
        ],
      ),
    );
  }
}

Rode e o primeiro erro do console é Vertical viewport was given unbounded height. As asserções RenderBox was not laid out abaixo são a consequência, não a causa. A correção é limitar o ListView.

Correção, em detalhe

As correções estão ordenadas pela frequência com que são a resposta certa. Escolha conforme o que o box sem tamanho realmente precisa.

1. Dê a um scrollable um tamanho limitado com Expanded (recomendado)

Quando um scrollable vive dentro de um Column ou Row e deve preencher o espaço restante, envolva-o em Expanded. O Expanded entrega ao filho uma restrição limitada e apertada (tight) no eixo principal, que é exatamente o que o viewport precisa:

// 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')),
        ],
      ),
    ),
  ],
)

Isso mantém o ListView preguiçoso: ele constrói apenas as linhas que estão na tela e rola o resto, que é o que você quer para qualquer lista que possa crescer. Esta é a correção certa para um feed, uma lista de resultados de busca, ou qualquer região com rolagem que tenha um cabeçalho acima.

2. Use shrinkWrap quando a lista é curta e deve se ajustar ao seu conteúdo

Se a lista é realmente pequena e finita (um punhado de linhas de configuração, um menu fixo) e você quer que ela ocupe apenas a altura do seu conteúdo, defina shrinkWrap: true. Isso diz ao ListView para medir seus filhos e reportar a altura combinada deles em vez de exigir um viewport limitado:

// 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')),
      ],
    ),
  ],
)

O custo é real: o shrinkWrap faz layout de todos os filhos antecipadamente, anulando a renderização preguiçosa que torna o ListView barato. Use-o apenas para listas curtas e limitadas. Para qualquer coisa que possa crescer para dezenas de itens, volte à correção 1. Adicionar physics: NeverScrollableScrollPhysics() impede que a lista interna role de forma independente, que costuma ser o que você quer quando o Column externo é a superfície de rolagem.

3. Dê ao box uma restrição limitada explícita

Às vezes a resposta certa é um tamanho concreto. Um SizedBox com altura fixa, ou um ConstrainedBox com uma altura máxima, dá ao scrollable um limite com o qual trabalhar:

// 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 */],
  ),
)

Um ListView horizontal dentro de um Column é a imagem espelhada da reprodução: o Column limita a largura mas deixa a altura não limitada, e o viewport horizontal precisa de uma altura limitada. Uma height fixa resolve isso de forma limpa. Use ConstrainedBox(constraints: BoxConstraints(maxHeight: 300)) quando o conteúdo puder ser mais curto que o teto.

4. Faça layout do filho antes de ler o tamanho dele em um RenderObject personalizado

Se você escreveu um RenderObject personalizado (ou uma subclasse de RenderBox), a asserção está dizendo que o performLayout acessou o tamanho de um filho antes de fazer layout dele. Sempre chame child.layout(...) antes de ler 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
}

O flag parentUsesSize: true é obrigatório quando o tamanho do próprio pai depende do filho. Omita-o e o Flutter pode pular o relayout quando o filho muda, o que produz layouts obsoletos que parecem esse mesmo erro de forma intermitente. O contrato está documentado na página da API RenderBox.size: o tamanho só é válido durante e depois do performLayout, e lê-lo a partir de um pai requer parentUsesSize: true no momento do layout.

5. Adie a leitura do tamanho para depois do primeiro frame

Se você precisa do tamanho renderizado de um widget em Dart (para posicionar um overlay, dimensionar um irmão, ou alimentar uma medição de volta ao estado), não leia context.size durante o build. O render box ainda não fez layout. Leia-o depois do frame:

// 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);
  });
}

Para medir durante o layout em vez de depois dele, recorra ao LayoutBuilder (que entrega a você as restrições do pai) ou a um RenderObject com parentUsesSize: true, não a uma leitura de tamanho pós-frame. A abordagem pós-frame é para o caso “só preciso dos pixels finais uma vez”.

Pegadinhas e erros parecidos

Encontrar o verdadeiro culpado rápido

Como este erro faz cascata, o caminho mais rápido é ler o console de cima para baixo e parar na primeira exceção. Depois afunile o subárvore:

  1. O primeiro erro nomeia um widget e uma localização de origem (lib/widgets/feed.dart:58). Abra esse arquivo e veja qual é o pai do widget nomeado. Um scrollable cujo pai é um Column, um Row, um IntrinsicHeight, ou outro scrollable é o seu suspeito.
  2. Ative debugPaintSizeEnabled = true; no main (ou alterne Debug Paint no Flutter Inspector) para ver o contorno de cada box. Um box que não pinta nada ou que colapsa para uma linha é o que falhou ao fazer layout.
  3. Abra o Layout Explorer no DevTools e selecione o widget que falha. O painel de restrições dele mostra se recebeu h=unbounded ou w=unbounded, o que confirma o diagnóstico. Se você não usou o DevTools para trabalho de layout, o passo a passo em perfilar o jank de um app Flutter com DevTools cobre como abrir uma sessão contra um dispositivo real; a mesma sessão maneja o Layout Explorer.

A lição mais profunda é que RenderBox was not laid out nunca é o bug em si. É o Flutter reportando que não conseguiu terminar o trabalho que um widget anterior começou. Treine-se para ignorar a mensagem mais barulhenta e encontrar a primeira, a silenciosa, e este erro deixa de ser um mistério. Mantenha seus scrollables limitados, faça layout dos filhos antes de medi-los, e nunca leia um tamanho antes do frame que o produz, e a asserção nunca dispara.

Relacionado

Fontes

Comments

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

< Voltar