In Flutter, what does passing a prebuilt child to a builder widget such as ValueListenableBuilder save, and when does the trick stop helping?
answer
- built once by the outer build
- handed back to the builder callback
- same instance, so updateChild skips it
- useless if the child reads the value
- lost when the outer widget rebuilds
basics
~20 sThe child is built once, outside the builder callback, and handed back unchanged on every notification, so the framework skips rebuilding it. It fails when that subtree needs the value, or when the enclosing widget rebuilds and recreates it.
solid answer
~40 s`ValueListenableBuilder` (and `ListenableBuilder`) call `builder` every time the listenable notifies. Anything created **inside** `builder` is a new instance each time and rebuilds. Anything passed as `child` was created once by the enclosing `build` and is handed back as the builder's `child` argument, so `Element.updateChild` sees the identical widget and skips its subtree. The API doc calls this a good practice for value-independent subtrees. It stops helping in three cases: when the subtree depends on the value, since it must then be built inside `builder`; when the enclosing widget itself rebuilds, which recreates `child`; and when the subtree is already `const`, since it is then stable anyway.
code
dart · 35 linesimport 'package:flutter/material.dart';
class LapPanel extends StatelessWidget {
const LapPanel({super.key, required this.elapsed, required this.laps});
final ValueNotifier<Duration> elapsed;
final List<Duration> laps;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Duration>(
valueListenable: elapsed,
builder: (BuildContext context, Duration value, Widget? child) {
return Column(
children: [
Text('${value.inSeconds}.${(value.inMilliseconds % 1000) ~/ 100} s'),
child!,
],
);
},
child: LapList(laps: laps),
);
}
}
class LapList extends StatelessWidget {
const LapList({super.key, required this.laps});
final List<Duration> laps;
@override
Widget build(BuildContext context) {
return Column(children: [for (final Duration lap in laps) Text('${lap.inMilliseconds} ms')]);
}
}go deeper
Recall that a builder's child is built once outside the callback and passed back, so it is not rebuilt on every change.
Explain why the same instance makes updateChild skip the subtree, and why a rebuild of the enclosing widget recreates the child.
Split value-dependent from value-independent parts on hot builders and verify with Track Widget Builds that the expensive part stopped rebuilding.
Encourage patterns that keep fast-changing values in small builders, so screens stay cheap as the team adds to them.
## The problem a builder creates A builder widget such as **`ValueListenableBuilder<T>`** or **`ListenableBuilder`** listens to a `Listenable` and, on each notification, calls `setState` in its own small `State`. Its `build` then calls your **`builder`** callback. Everything that callback constructs is new on every notification: ```dart ValueListenableBuilder<Duration>( valueListenable: elapsed, builder: (context, value, _) => Row(children: [ const Icon(Icons.timer), ExpensiveLapChart(laps: laps), // rebuilt on every tick Text(format(value)), ]), ) ``` `ExpensiveLapChart` does not depend on `value`, yet it is rebuilt at the notifier's rate because it is created inside `builder`. ## The child parameter Both builders accept an optional **`child`** and pass it back as the builder's last argument (`ValueWidgetBuilder<T>` is `(context, value, child)`; `ListenableBuilder` uses `TransitionBuilder`, `(context, child)`): 1. The **enclosing** `build` constructs `ExpensiveLapChart(laps: laps)` once and passes it as `child:`. 2. On each notification, the builder's `State` rebuilds and calls `builder(context, value, child)` with **the same instance**. 3. Your callback places `child` in the tree. 4. `Element.updateChild` finds the identical widget where the old one was, so the chart's element is reused and its subtree is not rebuilt. The `ValueListenableBuilder` API doc puts it this way: build a subtree that does not depend on the value **once**, pass it as `child`, and it can "improve performance significantly in some cases". ## When it does not help | Situation | Why the child is rebuilt anyway | |---|---| | the subtree reads `value` | it must be built inside `builder`, so no reuse | | the widget containing the builder rebuilds | its `build` constructs a new `child` instance | | `child` is already `const` | it was stable anyway; the parameter adds nothing | | the child depends on an inherited widget that changes | inherited dependencies rebuild it directly | The second row surprises people. The trick protects the child against the **listenable's** notifications, not against rebuilds of the code that creates it. If the page above calls `setState`, a new `ExpensiveLapChart` is created and handed in, and it rebuilds once, which is correct because its inputs may have changed. ## Where else the pattern appears The framework uses the same idea for animations: transition widgets and `AnimatedBuilder` take a `child` so the animated wrapper rebuilds while the content does not. That API belongs with the animation builders. The rebuild reasoning is the same. `MaterialApp.builder` also receives the navigator's subtree as a prebuilt `child`. ## A practical recipe - Split the builder's output into **value-dependent** and **value-independent** parts. - Build the independent part outside and pass it as `child`. If it takes only constants, write it as `const` instead. - Keep the part inside `builder` as small as possible, ideally just the `Text` or `Transform` that uses the value. - Check with **Track Widget Builds** that the expensive widget no longer shows a `build` event on each notification. ## A worked before and after Take a stopwatch panel that ticks ten times a second and shows the elapsed time above a list of 30 recorded laps: 1. **Before.** The `LapList` is created inside `builder`. Each tick builds one `Text`, one `LapList` and 30 lap `Text` widgets, all to show one new number. 2. **After.** The `LapList` is passed as `child`. Each tick builds the `Column` and the one `Text`. The `LapList` element sees the same widget and is skipped, together with its 30 children. 3. **When a lap is recorded,** the panel's parent rebuilds with a new `laps` list. A new `LapList` is constructed and passed in, and it rebuilds once, which is correct because its data changed. ## Related misconceptions - A `child` is not cached across the enclosing widget's rebuilds; it is only reused across the builder's own. - Passing `child` does not make the builder rebuild less often. The notification rate is the same, only the work per notification drops.
- Why is the child argument typed Widget? and when may it be null?The `child` parameter is optional. If you do not pass one, the builder receives `null`. The doc notes that it can be null when the whole subtree depends on the value, for example a builder that returns just a `Text` of the current string. When you did pass one, using `child!` inside the builder is safe.
- Does the child parameter reduce how often the builder callback runs?No. The builder still runs on every notification that changes the value (`ValueNotifier` skips sets to an equal value). The child parameter only reduces the work done per call, by keeping the value-independent subtree out of the rebuild.
A sign painter updating a scoreboard's digits: the frame and logo are mounted once and left in place, and only the numbers are repainted each time the score changes. Replace the whole board, though, and the frame comes down with it.
saying these in an interview costs you the question
- The child is cached forever, even when the enclosing widget rebuilds
- Passing child makes the builder callback run less often
- A child that displays the notifier's value can be passed as child
- Widgets created inside builder are reused between notifications
- The child parameter only matters for animations