Is a builder's "child" useful?

Widgets like AnimatedBuilder have a child parameter as a performance optimization.

Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: animation,
    builder: (context, child) => RebuildEachFrame(child: child),
    child: Container(),
  );
}

Would there be any downside to removing it?

Widget build(BuildContext context) {
  final container = Container();
  return AnimatedBuilder(
    animation: animation,
    builder: (context) => RebuildEachFrame(child: container),
  );
}
1 Like

Not really sure what is the question here, and the referenced docs comment on this topic, but

This code defines a widget that spins a green square continually. It is built with an AnimatedBuilder and makes use of the child feature to avoid having to rebuild the Container each time.

Imagine you would rotate some heavy widget, like your whole app. You only want to change the roration using the Transform widget, but the child (the whole app in this case) does nothing based on the animation and therefore does not need to do any more builds. By removing the child and putting everything to builder, you make the framework build the entire stuff in builder every tick of the animation. In general, we should aim to minimazing unnecessary buildings and painting of widgets with things like moving things that don’t depend on animation to child, putting those BlocBuilders etc. as deep as possible, etc.

2 Likes

Totally agree with not making the framework build all the stuff, every tick of the animation.

But my main question is about whether there’s any difference between storing a widget instance as a local variable in the outer build() function vs. using the child parameter.

Here’s a DartPad sample that’s either built once total or once each frame, depending on whether a callback is invoked inside or outside the ValueListenableBuilder. Hopefully it shows that setting the child parameter isn’t necessary for preventing rebuilds.

Good dartpad to showcase it. I would assume that your caching using the app variable is to my understanding equal to the child argument.

A — (in your dartpad demo with buildOutsideListenableBuilder = true), counter = 1

  @override
  Widget build(BuildContext context) {
    Widget? app;
    if (buildOutsideListenableBuilder) {
      app = _build();
    }

    return ValueListenableBuilder(
      valueListenable: controller,
      builder: (context, theta, child) {
        return Transform.rotate(angle: theta, child: app);
      },
    );
  }

B — is equal to (counter = 1)

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: controller,
      builder: (context, theta, child) {
        return Transform.rotate(angle: theta, child: child);
      },
      child: _build(),
    );
  }

C — vs rebuild every time (in your dartpad demo with buildOutsideListenableBuilder = false), counter is increasing while rotating

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: controller,
      builder: (context, theta, child) {
        return Transform.rotate(angle: theta, child: _build());
      },
    );
  }

I think putting the widget in the child argument is just clearer for average developer to use then doing cahing in variable etc in widget’s field or variable.

1 Like

Aside from any performance considerations I like separating child from the builder to isolate it conceptually.

3 Likes

Fantastic analysis, thank you!

Yeah, being “clearer for the average developer” is a good reason to keep things the way they are.

2 Likes

Awesome thread, I was wondering about that myself many times in the past.
So ideally the content of the child parameter won’t get necessarily get rebuild when the builders content is rebuild?

Yeah, any time a widget instance is identical to the previous instance (either by using const constructors or by caching the value somewhere) the widget won’t be rebuilt.

Using an AnimatedBuilder makes it easy to cache the widget value!

There is one downside for assigning a widget to a variable: you can put it in multiple places by mistake, and this is NOT allowed (especially for a non-const widget, such as Container).

Also, it is not the declaration that counts (there is no difference whatsoever to pass a variable or a widget to a child property: it run build when needed for both cases). This is only not true for static widgets (with const constructors). They will be rendered only once (but can still be composed on screen rotated, etc.)

Adding the same non-const widget instance to multiple places usually won’t cause any problems. The only exception is when it leads to “duplicate key” errors, but even with identical Key configurations, it usually won’t be an issue.

@override
Widget build(BuildContext context) {
  final container = Container(
    child: Container(key: const ValueKey(0)),
  );

  // no errors!
  return Column(
    children: [container, container, container],
  );
}

It’s true that the child parameter can be convenient for reducing the scope of the child widget. Seeing as it’s too late to change the signature without breakages, I guess the scoping is a nice little thing to be grateful for :slight_smile:


I apologize, I’m having a bit of trouble understanding this paragraph… if it’s not the declaration that counts, then what is it?

Adding the same non-const widget instance to multiple places usually won’t cause any problems.

“Usually” is the problem. If it CAN create problems, best practice is don’t do it. If, instead of a Container, it’s a StreamBuilder, it will give you an exception. Other widgets also can lead to problems, such as FutureBuilder or even InheritedWidget/InheritedModel (and you never know what is deep in the tree of that widget, if it is a custom one). So, better not use them this way, to be safe (especially when you can clearly avoid it, in the example).

I apologize, I’m having a bit of trouble understanding this paragraph… if it’s not the declaration that counts, then what is it?

Declarations (new) only runs the object’s constructor. And Dart constructors are pretty useless (because they can’t write final variables). I think I never saw any class in Dart using constructors to date.

In the case of Widgets, constructor is empty, so they don’t do anything. They will only do work in the build method so, in this specific case, it doesn’t matter where it is declared (in a variable or in a child property). The “problem” is the build method.

I assume you’re talking about constructor bodies here; it’s true that they can’t initialize non-late member variables.

I hadn’t considered the StreamBuilder example, thanks!

1 Like