If you've been working with animations in Flutter, you may have noticed that some widgets take both child
and builder
arguments.
Here's one example that uses TweenAnimationBuilder
to animate a Container
's color from red to green:
TweenAnimationBuilder<Color>(
tween: Tween<Color>(begin: Colors.red, end: Colors.green),
duration: const Duration(seconds: 1),
// child is *optional* so we can pass null or omit it
child: null,
// builder is *required*
// note that the third argument is an (optional) child
builder: (BuildContext context, Color color, Widget? child) {
return Container(color: color);
},
)
If we want we can omit the child
argument entirely, and things will still work as long as we return a widget inside the builder
.
So, why and when should we pass a child?
Answer: Performance optimizations
Here's what the official AnimatedBuilder documentation says:
If your builder function contains a subtree that does not depend on the animation, it's more efficient to build that subtree once instead of rebuilding it on every animation tick.
If you pass the pre-built subtree as the child parameter, the
AnimatedBuilder
will pass it back to your builder function so that you can incorporate it into your build.
Using this pre-built child is entirely optional, but can improve performance significantly in some cases and is therefore a good practice.
In other words, by passing a child widget using it inside the builder, we guarantee that the child is only built once rather than on every animation tick.
Here's another example that returns a rotating Container
:
AnimatedBuilder(
animation: someAnimation,
// pass a child widget
child: Container(color: Colors.red),
// get the child back as a (nullable) Widget object
builder: (BuildContext context, Widget? child) {
// this rebuilds on every animation tick
return Transform(
alignment: Alignment.center,
transform: Matrix4.rotationZ(2 * pi * someAnimation.value),
// this is only built once
child: child,
);
},
)
In this case, we use an AnimatedBuilder
because the transform
argument depends on the animation's value.
But the child of the Transform
widget only needs to be built once, so we can pass it to the AnimatedBuilder
directly and reuse it in the builder
callback.
So when can we use the child argument?
The answer is: when you have a widget that does not depend on the animation value.
Let's check this again:
child: Container(color: Colors.red)
In this case the Container
never changes, so it doesn't need to be rebuilt inside the builder
.
Let's revisit the original TweenAnimationBuilder
example:
TweenAnimationBuilder<Color>(
tween: Tween<Color>(begin: Colors.red, end: Colors.green),
duration: const Duration(seconds: 1),
builder: (BuildContext context, Color color, Widget? child) {
return Container(color: color);
},
)
In this case, we cannot create the Container
upfront because its color
changes as part of the animation. So we have to create it on-the-fly inside the builder
.
In addition to
TweenAnimationBuilder
andAnimatedBuilder
, other Flutter widgets follow the same convention. For exampleValueListenableBuilder
automatically rebuilds when itsvalueListenable
argument changes. If you're working with some kind of FooBuilder widget, check if it has achild
argument.
Wrap Up
We now know when to use the child
argument, and when not to.
Once again, this is a performance optimization.
Very often, it makes sense to measure performance first and only optimize the code if needed.
But the Flutter animation APIs are quite easy to use, so we may as well optimize upfront and use the child
argument when we can.
Happy coding!