AnimatedWidget

If you follow the traditional AnimationController approach where you use setState method to update the UI to show the animation, things might get slow when your build function is very large. When your build functon is very large and contains a lot of widgets, calling setState() very frequently downgrade the performance as it rebuilds that large widget tree with each setState call.

Woun't it be better if you take the animation part of your UI out of the main build function so that rest of the widget doesn't get rebuilt unnecessarily? AnimatedWidget is here to help you up.

To use AnimatedWidget you need to extend this class and create another widget that is only reponsible for doing the animation. This class takes a Listenable object as a required parameter. And you must overwrite the build function. The mechanism of this widget is as it takes a Listenable object, it will rebuilds its UI automatically whenever the widget get notified by that Listenable object. Luckily the AnimationController and Animation<T> objects are derived from Listenable, so you can use them.

Here is an example of animations from the previous article. Leter we will implement the same example with AnimatedWidget.

class _TestState extends State<Test> with SingleTickerProviderStateMixin{

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

  @override
  initState(){
    super.initState();
    _controller = new AnimationController(
      duration: const Duration(milliseconds: 2000), 
      vsync: this,
    );
    opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.0, 1.0, curve: Curves.easeIn)
      )
    );
    widthAnimation = Tween<double>(begin: 0.0, end: 100).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.1, 0.4, curve: Curves.easeOut)
      )
    );
    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();
  
  }

  @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
                    ),
                  )
                ),
              ),
            )
          ),
        ]
      ),
    );
  }
}

The above example animates three property, width, opacity and then the color of the box. We will need three class that will extend AnimatedWidget. Each class to animate each property. So lets first consider the width of the box. Here is the implementation:

class WidthAnimation extends AnimatedWidget{
  Widget child;

  WidthAnimation({Key key, AnimationController controller, this.child}) : super(
    key: key, 
    listenable: Tween<double>(begin: 0.0, end: 100).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(0.1, 0.4, curve: Curves.bounceIn),
      )
    )
  );

  Animation<double> get _progress => listenable;

  @override
  Widget build(BuildContext context){
    return SizedBox(
      width: _progress.value,
      height: 100,
      child: child
    );
  }
}

The subclass WidthAnimation extends AnimatedWidget class. The constructor this subclass must initialize its parent constructor and must pass listenable object. Here are are creating a new Animation<double> object using Tween constructor and passing it as listenable.

Now you should get the Animation object using the following statement:

Animation<double> get _progress => listenable;

Next, you need to override the build method that will return a widget. That's all about implementing a custom AnimatedWidget.

How it works?

When you start the animation using the controller (using forward() or repeat()), the base class AnimatedWidget will start generating values depending on the Listenable object that was provided through the constructor. In our example, we have provided a Tween as a listenable object, and that Tween will start generating values from 0 to 100, and will start at interval 0.1 and ends at 0.4 of the animation duration. When it generates a new value, it updates the listenable object and calls build method. So for each new value, the build function will be called and return the new UI. You do not need to call setState() method here. It is done by the base class AnimatedWidget automatically.

Now, we will do the same thing to animate other two property. Here is the code:

class OpacityAnimation extends AnimatedWidget{
  Widget child;

  OpacityAnimation({Key key, AnimationController controller, this.child}) : super(
    key: key, 
    listenable: Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(0.0, 1.0, curve: Curves.easeIn)
      )
    )
  );

  Animation<double> get _progress => listenable;

  @override
  Widget build(BuildContext context){
    return Opacity(
      opacity: _progress.value,
      child: child
    );
  }
}

class ColorAnimation extends AnimatedWidget{
  Widget child;

  ColorAnimation({Key key, AnimationController controller, this.child}) : super(
    key: key, 
    listenable: ColorTween(begin: Colors.yellow, end: Colors.red).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(0.4, 1.0, curve: Curves.easeOut)
      )
    )
  );

  Animation<Color> get _progress => listenable;

  @override
  Widget build(BuildContext context){
    return DecoratedBox(
      decoration: BoxDecoration(
        color: _progress.value
      ),
      child: child
    );
  }
}

We have now seperated out the animation part out of the main class _TestState method. Here is the code that shows how you can combine the above three AnimatedWidget to perform the same thing:

class _TestState extends State<Test> with SingleTickerProviderStateMixin{

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

    _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: WidthAnimation(
                controller: _controller,
                child: OpacityAnimation(
                  controller: _controller,
                  child: ColorAnimation(
                    controller: _controller,
                    child: SizedBox(
                      width: double.infinity,
                      height: double.infinity,
                    ),
                  )
                ),
              ),
            )
          ),
        ]
      ),
    );
  }
}

Now your main class _TestState looks more elegent and clean as we have totally moved the Animation declaration part out of the class and moved to its specific AnimatedWidget class. Now if you run the code your animation will be played but without calling the build method by using setState(). So no unncessary rebuild in the _TestState class.

Don't Implement Nested AnimatedWidget

An important point you should know that, you should never put another AnimatedWidget inside the build method of a AnimatedWidget. Becuase AnimatedWidget calls build method automatically and each time it calls build method, it returns a new version of the UI. If you put another AnimatedWidget inside the build method, it will repeteadly create a new instance of the inner AnimatedWidget every time the build method is called. Here is the demonstration:

class WidthAnimation extends AnimatedWidget{
  Widget child;
  AnimationController controller;

  WidthAnimation({Key key, this.controller, this.child}) : super(
    key: key, 
    listenable: Tween<double>(begin: 0.0, end: 100).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(0.1, 0.4, curve: Curves.bounceIn),
      )
    )
  );

  Animation<double> get _progress => listenable;

  @override
  Widget build(BuildContext context){
    return SizedBox(
      width: _progress.value,
      height: 100,
      child: OpacityAnimation(
        controller: controller,
        child: child,
      )
    );
  }
}


class OpacityAnimation extends AnimatedWidget{
  Widget child;

  OpacityAnimation({Key key, AnimationController controller, this.child}) : super(
    key: key, 
    listenable: Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(0.0, 1.0, curve: Curves.easeIn)
      )
    )
  ){
    print("OpacityAnimation Initialized");
  }

  Animation<double> get _progress => listenable;

  @override
  Widget build(BuildContext context){
    return Opacity(
      opacity: _progress.value,
      child: child
    );
  }
}

In the above example, in the build method the WidthAnimation class, we have used OpacityAnimation and passed the same controller that was given to the WidthAnimation and passes the child as well. We have added a print statement in the OpacityAnimated constructor to see what happens when animation runs.

Now in the _TestState class you will be able to use the code in the following way:

child: Center(
  child: WidthAnimation(
    controller: _controller,
    child: ColorAnimation(
      controller: _controller,
      child: SizedBox(
        width: double.infinity,
        height: double.infinity,
      ),
    )
  ),
)

Now the WidthAnimation will be responsible for animating both width and opacity. The above code will work perfectly fine. But in the console, it will keep printing OpacityAnimation Initialized during the whole time animation runs. You can guess why. Whenever the WidthAnimation gets rebuilt, the OpacityAnimation is being created anew. This will downgrade your application performace if you structure your animation like this way. So always avoid putting a AnimatedWidget inside the build method of another AnimatedWidget.

So it is always best practice to pass an AnimatedWidget as a child to another AnimatedWidget. Because when you pass the child, you passing an reference, so it doesn't get rebuilt.

The AnimatedWidget doesn't require a child at all, if your child is relatively small, you can place it within the build method of AnimatedWidget. In this case you will only need to pass the animation controller to the AnimatedWidget.