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
AnimatedBuilderwill 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
TweenAnimationBuilderandAnimatedBuilder, other Flutter widgets follow the same convention. For exampleValueListenableBuilderautomatically rebuilds when itsvalueListenableargument changes. If you're working with some kind of FooBuilder widget, check if it has achildargument.
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!





