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