0Pricing
Flutter Mobile Development · Lekcja

RepaintBoundary, widgety const i ograniczanie przebudowy

Ograniczaj niepotrzebne przemalowywanie i przebudowy za pomocą granic oraz konstruktorów const

RepaintBoundary, widgety const i ograniczanie przebudowy to bezpłatna lekcja Flutter Mobile Development na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Flutter Mobile Development, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Flutter Mobile Development zawiera 4 lekcji w sumie.

Części tej lekcji nie zostały jeszcze przetłumaczone i są wyświetlane po angielsku.

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.

Często zadawane pytania

Czy lekcja „RepaintBoundary, widgety const i ograniczanie przebudowy” jest bezpłatna?

Tak — pełny tekst „RepaintBoundary, widgety const i ograniczanie przebudowy” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Flutter Mobile Development, przejdź na CoddyKit PRO. Kurs Flutter Mobile Development zawiera 4 lekcji w sumie.

Co nauczysz się w „RepaintBoundary, widgety const i ograniczanie przebudowy”?

Ograniczaj niepotrzebne przemalowywanie i przebudowy za pomocą granic oraz konstruktorów const Ćwiczysz Flutter Mobile Development z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Flutter Mobile Development?

Nie wymagamy żadnego doświadczenia. Flutter Mobile Development w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „RepaintBoundary, widgety const i ograniczanie przebudowy”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Flutter Mobile Development?

Tak. Każda lekcja Flutter Mobile Development zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Trzy drzewa: Widget, Element i RenderObject
  2. Profilowanie zacięć za pomocą osi czasu DevTools
  3. RepaintBoundary, widgety const i ograniczanie przebudowy
  4. Rozgrzewanie shaderów i migracja do Impellera
← Powrót do Flutter Mobile Development