AnimatedBuilder

The AnimatedBuilder does the same job a AnimatedWidget does but in a different way. It has a animation property that takes a Listenable object. In this case, you can assign AnimationController or an Animation<T> object. It takes another property builder, which is a callback. The builder method should return the UI that is the part of the animation. Whenever the provided Listenable object changes its value, the AnimatedBuilder widget calls its builder callback to return a new UI.

Let's consider the same example. This time instead of AnimatedWidget, we will use AnimatedBuilder widget to achieve the same animation.

class _TestState extends State<Test> with SingleTickerProviderStateMixin{

  AnimationController _controller;
  Animation<double> widthAnimation;
  Animation<double> opacityAnimation;
  Animation<Color> colorAnimation;

  @override
  initState(){
    super.initState();
    _controller = new AnimationController(
      duration: const Duration(milliseconds: 2000), 
      vsync: this,
    );

    widthAnimation = Tween<double>(begin: 0.0, end: 100).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.1, 0.4, curve: Curves.bounceIn)
      )
    );
    opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.0, 1.0, curve: Curves.easeIn)
      )
    );
    colorAnimation = ColorTween(begin: Colors.yellow, end: Colors.red).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.4, 1.0, curve: Curves.easeOut)
      )
    );

    _controller.addListener((){
      setState((){});
    });

    _controller.repeat(reverse: true);
  
  }

  @override
  dispose(){
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return BeautyBackground(
      child: Column(
        children: <Widget>[
          BackButton(),
          Expanded(
            child: Center(
              child: SizedBox(
                width: (widthAnimation.value == null) ? 0.0 : widthAnimation.value,
                height: 100,
                child: Opacity(
                  opacity : (opacityAnimation.value == null) ? 0.0 : opacityAnimation.value,
                  child: DecoratedBox(
                    decoration: BoxDecoration(
                      color: (colorAnimation.value == null) ? Colors.red : colorAnimation.value,
                    ),
                    child: SizedBox(
                      width: double.infinity,
                      height: double.infinity
                    ),
                  )
                ),
              ),
            )
          ),
        ]
      ),
    );
  }
}

Now first thing we need to remove the following part:

_controller.addListener((){
  setState((){});
});

Second, we need update the build method. We should take the animation part within the AnimatedBuilder widget. Here is the build method:

@override
Widget build(BuildContext context){
  return BeautyBackground(
    child: Column(
      children: <Widget>[
        BackButton(),
        Expanded(
          child: Center(
            child: AnimatedBuilder(
              animation: _controller,
              builder: (BuildContext context, Widget child){
                return SizedBox(
                  width: (widthAnimation.value == null) ? 0.0 : widthAnimation.value,
                  height: 100,
                  child: Opacity(
                    opacity : (opacityAnimation.value == null) ? 0.0 : opacityAnimation.value,
                    child: DecoratedBox(
                      decoration: BoxDecoration(
                        color: (colorAnimation.value == null) ? Colors.red : colorAnimation.value,
                      ),
                      child: SizedBox(
                        width: double.infinity,
                        height: double.infinity
                      ),
                    )
                  ),
                );
              },
            ),
          )
        ),
      ]
    ),
  );
}

The animation property of the AnimatedWidget is assigned to the _controller. So whenever the controller changes its value, the widget calls its builder method. This is done automatically by the widget. The builder callback returns the new UI based on your current animations value. So your main build method which is in _TestState class doesn't get rebuilt with the setState() when controller changes its value. This is very efficient when your main build method is very large and you want only the animation part of the widget tree to be rebuilt.

The builder callback has two parameter, the first parameter is context and second parameter is child. We haven't used the child parameter yet. Now we will see how you can pass a child argument to the widget.

If some part of the widget tree that is returned from the builder callback doesn't rely on the animation values, you can build it only once and reuse its reference. In this case you should pass a child parameter which takes a Widget. This widget is passed to the builder method. The builder method uses that child to insert it in the widget tree and then return it. This way you will be able to avoid unncessary rebuilding a specific part of your animation widget that doesn't change with the animation values.

In our above example, the last SizedBox doesn't rely on the animation values. So we can move this part to the child parameter.

@override
Widget build(BuildContext context){
  return BeautyBackground(
    child: Column(
      children: <Widget>[
        BackButton(),
        Expanded(
          child: Center(
            child: AnimatedBuilder(
              animation: _controller,
              child: SizedBox(
                  width: double.infinity,
                  height: double.infinity
              ),
              builder: (BuildContext context, Widget child){
                return SizedBox(
                  width: (widthAnimation.value == null) ? 0.0 : widthAnimation.value,
                  height: 100,
                  child: Opacity(
                    opacity : (opacityAnimation.value == null) ? 0.0 : opacityAnimation.value,
                    child: DecoratedBox(
                      decoration: BoxDecoration(
                        color: (colorAnimation.value == null) ? Colors.red : colorAnimation.value,
                      ),
                      child: child,
                    )
                  ),
                );
              },
            ),
          )
        ),
      ]
    ),
  );
}

Using this pre-built child is entirely optional, but can improve performance significantly in some cases and is therefore a good practice.

So, this is all about AnimatedBuilder widget. It's usage purpose is same as AnimatedWidget, which is avoiding unncessary rebuilding of the whole widget tree. But the way they are used, are different.

AnimatedBuilder Vs AnimatedWidget

Even though their key purpose of usage is same, there is a difference where they suits best.

The AnimatedBuilder is helpful when you want to rebuild the animation part at once. In the above example, the width, opacity and the color all property updates together with a single call of the builder callback. That means you cannot seperate the animations out from each other if you apply the multiple animation on a single widget. You must have to rebuild the whole animated widget at once. In other words the AnimatedBuilder cannot be nested.

On the other hand AnimatedWidget allows you to define a single animation per class, you cannot use multiple animation instance values. One class will allow only one listenable object. You can nest these subclasses to create the same effect, but each class will update its animation seperately. That means with AnimatedWidget, animated property is updated seperately in seperated class.

Use either of them where they suits best.