0Pricing
Flutter Mobile Development · Aula

RepaintBoundary, Widgets Constantes e Redução de Reconstruções

Reduza repinturas e reconstruções desnecessárias com limites e construtores const.

RepaintBoundary, Widgets Constantes e Redução de Reconstruções é uma aula grátis de Flutter Mobile Development no CoddyKit. Esta é a aula 3 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Flutter Mobile Development, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Flutter Mobile Development inclui 4 aulas no total.

Partes desta aula ainda não foram traduzidas e aparecem em inglês.

Rebuilds vs Repaints

Flutter performance work lives at three layers, and conflating them wastes effort:

  • Rebuild — build() runs again, producing a new widget tree. Cheap if widgets are immutable and shallow, but it can cascade.
  • Relayout — RenderObjects recompute size/position. Triggered by constraint or child changes.
  • Repaint — pixels are re-rasterized to a layer. Expensive for gradients, shadows, and complex paths.

This lesson targets two distinct wins: pruning rebuilds with const and structure, and isolating repaints with RepaintBoundary. They solve different problems — do not reach for one when you need the other.

Why const Widgets Skip Rebuilds

A const widget is canonicalized by the Dart compiler: every evaluation of the same const expression returns the identical instance. When a parent rebuilds, Flutter compares the new child widget to the old one. If they are the same instance (identical(old, new) is true), Flutter short-circuits and does not rebuild that subtree at all.

  • Without const, each parent build creates a fresh Widget object, forcing the element to update its child.
  • With const, the canonical instance is reused, so the subtree is skipped entirely.

This is the cheapest optimization in Flutter — it costs you a keyword and prunes whole branches of the rebuild.

// Dart compile-time canonicalization: const instances are identical.
class Point {
  final int x;
  final int y;
  const Point(this.x, this.y);
}

void main() {
  const a = Point(1, 2);
  const b = Point(1, 2);
  // Both refer to the SAME canonical instance.
  print(identical(a, b)); // true

  final c = Point(1, 2); // runtime instance, not canonicalized
  print(identical(a, c)); // false
}

Applying const in a Widget Tree

Mark every widget you can as const. The rule: a widget can be const if all its constructor arguments are themselves compile-time constants. Static labels, icons, padding, and spacers are prime candidates.

  • A const child inside a frequently-rebuilding parent is the highest-value placement — the parent rebuilds, the child is skipped.
  • Enable the prefer_const_constructors and prefer_const_literals_to_create_immutables lints so the analyzer flags missed opportunities.

Below, only the live counter text changes; the title, divider, and icon never rebuild because they are const.

class CounterPanel extends StatelessWidget {
  final int count;
  const CounterPanel({super.key, required this.count});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('Live Counter'),      // const: skipped on rebuild
        const Divider(),                  // const: skipped
        Text('$count'),                   // dynamic: must rebuild
        const Icon(Icons.timer),          // const: skipped
      ],
    );
  }
}

When const Is Impossible: Hoist the Subtree

You cannot make a widget const if it depends on runtime values (a theme color, a fetched string, a callback closure). But you can still avoid rebuilding it by hoisting it out of the rebuilding scope.

  • Build the expensive-but-static subtree once in a parent, store it in a field or pass it as a child parameter, and let the rebuilding widget reuse that same reference.
  • This is exactly the pattern AnimatedBuilder and ValueListenableBuilder use with their child argument: the child is built once and threaded through every animation frame untouched.
// The child is built ONCE and reused on every animation tick.
AnimatedBuilder(
  animation: rotationController,
  // 'child' is created a single time, not per-frame.
  child: const ExpensiveLogo(),
  builder: (context, child) {
    return Transform.rotate(
      angle: rotationController.value * 6.28,
      child: child, // reused reference: no rebuild of ExpensiveLogo
    );
  },
)

How Repaints Propagate

Flutter composites the UI into layers. When a RenderObject is marked dirty for paint, Flutter repaints the entire layer it belongs to — not just that one object. By default, sibling widgets often share a layer.

  • An animation that constantly repaints (a spinner, a progress bar, a blinking cursor) marks its layer dirty every frame.
  • If a heavy static widget (a big image, a complex gradient background) shares that layer, it gets re-rasterized every frame too — pure waste.

The fix is to give the volatile widget its own layer so its repaints stay contained. That is what RepaintBoundary does.

RepaintBoundary: Isolating a Layer

RepaintBoundary wraps a subtree and forces it into a separate compositing layer. Repaints inside the boundary no longer dirty the parent layer, and repaints outside no longer force the boundary's contents to re-rasterize.

  • Wrap the frequently repainting widget so its churn stays isolated, OR
  • Wrap the expensive static widget so a noisy neighbor can't drag it into per-frame repaints.

The boundary has a real cost: an extra layer means extra memory and a compositing step. Use it surgically where the profiler shows repaint waste — not everywhere.

Stack(
  children: [
    // Heavy, static background — isolate it so the spinner can't
    // force it to re-rasterize every frame.
    const RepaintBoundary(
      child: ComplexGradientBackground(),
    ),
    // Constantly repainting overlay gets its own layer too.
    const Center(
      child: RepaintBoundary(
        child: CircularProgressIndicator(),
      ),
    ),
  ],
)

Lists Insert RepaintBoundary for You

A common question: do you need to wrap every ListView item in a RepaintBoundary? Usually no — ListView, GridView, and other SliverList-based widgets already wrap each item in a RepaintBoundary by default (controlled by the addRepaintBoundaries flag, which defaults to true).

  • This means scrolling repaints don't cascade across all visible rows.
  • Wrapping items again is redundant and adds layer overhead. Leave the default on.
  • You set addRepaintBoundaries: false only for trivially cheap items where the extra layer costs more than it saves.
ListView.builder(
  itemCount: messages.length,
  // addRepaintBoundaries defaults to true — each row is already isolated.
  itemBuilder: (context, index) {
    return MessageTile(message: messages[index]);
  },
)

setState Rebuilds the Whole build()

Calling setState marks the entire State's build() method dirty. Everything that method returns is rebuilt — even widgets unrelated to the changed value. In a large screen this is the most common source of jank.

  • The narrower your build(), the cheaper each setState.
  • Push the volatile state down into the smallest possible widget so only it rebuilds.

Below, a single tap rebuilds the whole screen — including the static header and footer — because they live in the same build() as the counter.

class DashboardState extends State<Dashboard> {
  int taps = 0;

  @override
  Widget build(BuildContext context) {
    // setState rebuilds ALL of this, header and footer included.
    return Column(
      children: [
        const ExpensiveHeader(),
        Text('Taps: $taps'),
        ElevatedButton(
          onPressed: () => setState(() => taps++),
          child: const Text('Tap'),
        ),
        const ExpensiveFooter(),
      ],
    );
  }
}

Pruning with ValueListenableBuilder

To rebuild only the part that depends on a value, replace setState with a scoped listenable. ValueListenableBuilder rebuilds just its builder closure when the value changes; everything outside it stays put.

  • The header, footer, and surrounding layout are built once and never rebuild on value changes.
  • Combine with the child argument to hoist any static subtree inside the builder.

This converts a whole-screen rebuild into a surgical, single-widget rebuild.

final taps = ValueNotifier<int>(0);

@override
Widget build(BuildContext context) {
  return Column(
    children: [
      const ExpensiveHeader(), // built once, never rebuilt
      ValueListenableBuilder<int>(
        valueListenable: taps,
        builder: (context, value, child) => Text('Taps: $value'),
      ),
      ElevatedButton(
        onPressed: () => taps.value++, // no setState
        child: const Text('Tap'),
      ),
      const ExpensiveFooter(), // built once, never rebuilt
    ],
  );
}

Measuring Before You Optimize

Never guess — measure. Flutter ships tools that show exactly where rebuilds and repaints happen:

  • Repaint Rainbow (DevTools / debugRepaintRainbowEnabled): overlays a rotating border color on each layer every time it repaints. A widget that flickers colors constantly is repainting too often — a candidate for RepaintBoundary.
  • Track Widget Builds / Rebuild Stats: counts how many times each widget rebuilds, exposing widgets that rebuild far more than expected.
  • Performance Overlay: shows the UI (build) and raster (paint) thread budgets; spikes tell you which thread is the bottleneck.

Optimize only what the profiler flags. A const or a boundary added blindly can cost more than it saves.

import 'package:flutter/rendering.dart';

void main() {
  // Visualize layer repaints during development.
  debugRepaintRainbowEnabled = true;
  runApp(const MyApp());
}

A Decision Checklist

Put the three tools in order when you face jank:

  • Too many rebuilds? Add const where arguments are constant; hoist static subtrees via the child parameter; scope state with ValueListenableBuilder instead of screen-wide setState.
  • Too many repaints? Wrap the volatile or the expensive widget in RepaintBoundary to isolate its layer.
  • Not sure which? Turn on Rebuild Stats and Repaint Rainbow first.

Key distinction: const fights rebuilds; RepaintBoundary fights repaints. Using the wrong one leaves the bottleneck untouched and may add overhead.

Quick Check

A screen shows a static, expensive blurred background image. On top of it sits a small CircularProgressIndicator that animates every frame. The profiler shows the whole background re-rasterizing 60 times per second. What is the correct fix?

Recap

You now have a precise mental model for cutting Flutter render cost:

  • Rebuilds are pruned by const (canonical instances are skipped via identical), by hoisting static subtrees through the child parameter, and by scoping state with ValueListenableBuilder instead of whole-screen setState.
  • Repaints are isolated by RepaintBoundary, which moves a subtree onto its own compositing layer so volatile and static widgets stop contaminating each other.
  • Lists already insert repaint boundaries per item — don't double-wrap.
  • Measure first with Rebuild Stats, Repaint Rainbow, and the Performance Overlay; apply each tool only where the profiler proves it pays off.

The golden rule: const fights rebuilds, RepaintBoundary fights repaints — match the tool to the symptom.

Perguntas Frequentes

A aula “RepaintBoundary, Widgets Constantes e Redução de Reconstruções” é grátis?

Sim — o texto completo de “RepaintBoundary, Widgets Constantes e Redução de Reconstruções” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Flutter Mobile Development, atualize para CoddyKit PRO. O curso de Flutter Mobile Development inclui 4 aulas no total.

O que vou aprender em “RepaintBoundary, Widgets Constantes e Redução de Reconstruções”?

Reduza repinturas e reconstruções desnecessárias com limites e construtores const. Você pratica Flutter Mobile Development com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar Flutter Mobile Development?

Nenhuma experiência prévia é necessária. Flutter Mobile Development no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 3 de 4.

Quanto tempo leva a aula “RepaintBoundary, Widgets Constantes e Redução de Reconstruções”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de Flutter Mobile Development?

Sim. Cada aula de Flutter Mobile Development inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. As Três Árvores: Widget, Element e RenderObject
  2. Criação de Perfis de Travamentos com a Linha do Tempo do DevTools
  3. RepaintBoundary, Widgets Constantes e Redução de Reconstruções
  4. Aquecimento de Sombreadores e Migração para o Impeller
← Voltar para Flutter Mobile Development