Flutter Animation Tutorial – Refactoring with AnimatedWidget & AnimatedBuilder

Animation undoubtedly creates great looking apps. Unless done correctly though, it can also create great pile of mess in your codebase. Having covered the core principles of animation in the previous part, we can move towards a much cleaner UI code and also its reusability by utilizing AnimatedWidget and AnimatedBuilder.

Moving Away from setState

While it's true that animation is simply done by rebuilding the UI multiple times per second, doing this naively by calling setState is not the best option. At the very least, this creates unnecessary boilerplate. What's even worse, it tightly ties the animation with the widget which is being animated.

Be sure you understand the basics though before jumping straight to this tutorial.

AnimatedWidget

AnimatedWidget is perfect when you don't care about the widget being tied to an animation, but you want to keep your code clean. Actually, you should never animate by calling setState directly from the animation listener. We did so just to demonstrate a point in the previous part. Whenever you want to animate, start with an AnimatedWidget.

Much like when you create a Stateless or a StatefulWidget, the process of making an AnimatedWidget is similar but with one difference - its constructor takes in a Listenable and, of course, Animation<T> is a subclass of this. Behind the scenes, a listener which calls setState gets created.

animated_widget_page.dart

class ResocoderImage extends AnimatedWidget {
  ResocoderImage({
    Key key,
    @required Animation<double> animation,
  }) : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;

    return Transform.rotate(
      angle: animation.value,
      child: Container(
        alignment: Alignment.center,
        padding: EdgeInsets.all(30),
        child: Image.asset(
          'assets/resocoder.png',
        ),
      ),
    );
  }
}

This approach allows you to move the animated UI away from the StatefulWidget containing all the animation logic. It also facilitates a certain amount of code reuse - the ResocoderImage widget can now be reused and animated throughout the whole app.

The AnimationPage widget holding the AnimationController remains almost unchanged from the previous part.

animated_widget_page.dart

import 'package:flutter/material.dart';
import 'dart:math' as math;

class AnimationPage extends StatefulWidget {
  _AnimationPageState createState() => _AnimationPageState();
}

class _AnimationPageState extends State<AnimationPage>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController animController;

  @override
  void initState() {
    super.initState();
    animController =
        AnimationController(duration: Duration(seconds: 5), vsync: this);

    final curvedAnimation = CurvedAnimation(
      parent: animController,
      curve: Curves.easeIn,
      reverseCurve: Curves.easeOut,
    );

    animation =
        Tween<double>(begin: 0, end: 2 * math.pi).animate(curvedAnimation)
          ..addStatusListener((status) {
            if (status == AnimationStatus.completed) {
              animController.reverse();
            } else if (status == AnimationStatus.dismissed) {
              animController.forward();
            }
          });

    animController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ResocoderImage(
        animation: animation,
      ),
    );
  }

  @override
  void dispose() {
    animController.dispose();
    super.dispose();
  }
}

AnimatedBuilder

AnimatedWidget allows you to reuse the UI which is tied together with its motion. AnimatedBuilder, on the other hand, is usually used to separate even the motion into its own widget which, by convention, is called a Transition.

AnimatedBuilder comes with performance optimizations to not rebuild the UI every time the animation's value changes, so it's perfect to use with large animated widget sub-trees.

Let's create a RotatingTransition widget which will do nothing more than apply an animation on a Transform. This will make this transition usable with any widget sub-tree, not just the ResocoderImage

animated_builder_page.dart

class RotatingTransition extends StatelessWidget {
  RotatingTransition({
    // Give the animation a better fitting name - we're animating the angle of rotation.
    @required this.angle,
    @required this.child,
  });

  final Widget child;
  final Animation<double> angle;

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: angle,
      // This child will be pre-built by the AnimatedBuilder for performance optimizations
      // since it won't be rebuilt on every "animation tick".
      child: child,
      builder: (context, child) {
        return Transform.rotate(
          angle: angle.value,
          child: child,
        );
      },
    );
  }
}

Now that we've separated the "animation motion" into a RotatingTransition, what's going to happen with the ResocoderImage widget? Technically, it no longer needs to be a separate class, but let's leave it as that. Of course, it will no longer extend an AnimatedWidget but rather a StatelessWidget.

animated_builder_page.dart

class ResocoderImage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      padding: EdgeInsets.all(30),
      child: Image.asset(
        'assets/resocoder.png',
      ),
    );
  }
}

With this, we've accomplished the ultimate code separation and reuse, at least when it comes to 1st party Flutter animations. We can freely reuse the RotatingTransition to animate anything throughout the whole app.

The updated build function of the AnimationPage will look like this:

file_name.dart

class _AnimationPageState extends State<AnimationPage>
    with SingleTickerProviderStateMixin {
  ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      //* Then also show FadeTransition here - Flutter uses this "transition pattern" itself
      body: RotatingTransition(
        angle: animation,
        child: ResocoderImage(),
      ),
    );
  }
  
  ...
}

Pre-built Transitions

Flutter actually provides a few transitions itself - FadeTransitionSizeTransitionRotationTransition (much like the one we've built). The difference between our RotatingTransition and the pre-built RotationTransition is that our transition expects to receive values in range 0 to 2*PI, while the pre-built receives the number of full turns around the circle.

Try experimenting with the different Transitions. Make sure you change the value range generated by the Tween in order to make the animations look just right. You probably don't want a SizeTransition to be animated from 0 to 2*PI, after all.

Other Animation Options

This tutorial covered the AnimatedWidget and AnimatedBuilder as 1st party options to achieve code reuse and cleanliness. Animation setup is still far from desirable though. Creating the AnimationController, adding listeners... Thankfully, there are a few 3rd party packages which can abstract all of this away. Stay tuned for more tutorials in the near future.

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

>