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.
4 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
As it is mentioned in the flutter docs, each build parameter serves to isolate the subtree from the animation, thus reducing its internal components rebuilds. Yes you can separately cache your widgets or annotate them as const or use RepaintBoundary widget to localize your rebuilds. But please keep in mind guys: the Flutter Guys [I mean the blue birds that we all love, maintaining the framework we all fond of] do and recommend things for a reason ! so it is most of the time wise to do things by the book.
one more thing, years ago, we discussed the usage of widget functions being a false paradigm, meaning either use them with const to prevent their rebuilds( not very fancy way of writing code if you ask me, at least from a declarative/imperative stand point ), or convert them into stateless widgets, and we all agreed since day one that :
UIwidgets.fold(
(when you need to mutate)=>stful,
(when you need to draw static components)=>stlss);
one more solid reason why you should stop that:
Let us assume you have a very expensive widget in the form of a widget returning function, and let us assume that you are using it inside of another stateful widget, keep in mind that it will always be a part of your big wrapper widget, thus affected by its lifecycle !
yes you can still explicitly annotate them as const , but, its just not the right way to do proper widget exportation, and it is just bad for scalability later on! because in the future you would want to convert stateless widgets to hookwidgets or stateful widgets to add explicit advanced animations. of some cool stuff, like overlays or players, or scrollcontrollers that need to be initialized … and the list goes on!
Just stop using them already, because even for readability, they are bad! With all due respect to your preferences of course
Flutter makes a whole lot of sense, if you are familiar with game engines, especially XNA (which is defunct, but MonoGame is the same).
Game engines are practically all the same, with 3 stages:
- Initialize/LoadContent: Pretty self-explanatory. Flutter has this in many shapes: the
initState
of a State<StatefulWidget>
, a FutureBuilder
that load stuff, etc. And, obviously, you don’t always need to initialize or load anything.
- Update: this code runs as fast as it can and it only set properties to classes. All your game is actually a state, a bunch of classes that have primitive values (like coordinates, or textures names, etc.). Here, an input (for example, a X button on a controller is pressed?) will change the properties of your classes (such as ship accelleration to positive X). NOTHING else is done in this method other than update properties on pure classes (that have absolutely no code whatsoever, they are just value holders). Seems familiar? This is the
StatelessWidget
AND the StatefulWidget
(yes, the latter is immutable as well, only it’s Element
is mutable).
- Draw This nasty method gets the current state of the game as it is right now and dumps into shaders, textures, etc., sending them to the GPU so, basically, a snapshot of your entire game state is stamped on the screen (it’s like having a room full of motion and a hole in the wall that only opens for one moment and take a picture). This is the Flutter’s render tree. And this is the code that should take, at most, 16ms to get a 60FPS output.
Why is this important and relevant for the current discussion: Widgets are only the current expression of your app’s state. They only change if your app state changes (and, yes, this includes StatefulWidget
: a Text("Hello world!")
is a state (one that never changes, but still…). So, it doesn’t make any sense to change or reconstruct something that is the same as the previous state (remembering that this check is made hundreds of times per second).
Another important part of what I’m trying to convey is garbage collection. GC is a very slow process (that, somehow, Dart accomplishes with mastery - very different than, for example, old .net, where GC could cause very visible lags, especially in games, while GCing). How this is/was solvable in .net? Creating object pools: creating objects but actually not releasing them to GC, so they can be reused. How Dart reuses objects and avoid GC? const
ctor.
So, if you don’t want to add another variable to memory/GC and you are not mutating your app state for that specific object, then it makes sense to pass the child to something that changes a lot: for example: a widget that draws a ship using CustomPaint
don’t need to redraw itself at all, if it is inside an animation that spins that ship around.
Makes sense?