Let’s be honest: a static app feels like a legacy app. In my experience building production-ready mobile interfaces, the difference between a ‘good’ app and a ‘premium’ app usually comes down to motion. Whether it’s a subtle button bounce or a complex page transition, animations provide the tactile feedback users crave.

In this flutter animations tutorial for mobile, I’m going to strip away the academic jargon and show you how to actually implement motion that enhances UX without tanking your frame rate. We’ll cover the two main pillars of Flutter motion: Implicit and Explicit animations.

Prerequisites

Before we dive into the code, make sure you have the following set up in your environment:

Step 1: Mastering Implicit Animations

Implicit animations are the ‘easy win’ of Flutter. They are essentially widgets that automatically animate when their properties change. You don’t need a controller; you just change a value and call setState().

The most common implicit widgets are AnimatedContainer, AnimatedOpacity, and AnimatedAlign. Here is a practical example of an AnimatedContainer that changes size and color based on a user tap:

class ImplicitDemo extends StatefulWidget {
  @override
  _ImplicitDemoState createState() => _ImplicitDemoState();
}

class _ImplicitDemoState extends State<ImplicitDemo> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _expanded = !_expanded),
      child: AnimatedContainer(
        duration: Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        width: _expanded ? 200.0 : 100.0,
        height: _expanded ? 200.0 : 100.0,
        color: _expanded ? Colors.blue : Colors.red,
        child: Center(child: Text(_expanded ? "Expanded!" : "Tap Me")),
      ),
    );
  }
}

Notice how we didn’t have to write a single line of logic to handle the transition between 100px and 200px. Flutter handles the interpolation for us. If you’re looking to scale this across a larger app, I recommend checking out my guide on flutter responsive ui best practices to ensure your animations look great on all screen sizes.

Step 2: Taking Control with Explicit Animations

Implicit animations are great, but what if you want an animation to loop infinitely? Or what if you need to reverse it precisely when a specific event occurs? That’s where Explicit animations come in. These require an AnimationController.

To use an AnimationController, your widget must use a TickerProviderStateMixin. Here is how I typically implement a rotating loading icon or a pulsing button:

class ExplicitDemo extends StatefulWidget {
  @override
  _ExplicitDemoState createState() => _ExplicitDemoState();
}

class _ExplicitDemoState extends State<ExplicitDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..repeat(reverse: true);

    _animation = Tween<double>(begin: 1.0, end: 1.2).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

  @override
  void dispose() {
    _controller.dispose(); // Crucial to avoid memory leaks!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _animation,
      child: FlutterLogo(size: 100),
    );
  }
}

As shown in the image below, the key difference here is the manual lifecycle management. You must initialize the controller and, more importantly, dispose of it to prevent memory leaks in your mobile application.

Comparison of Flutter AnimationController setup vs Implicit widget setup
Comparison of Flutter AnimationController setup vs Implicit widget setup

Pro Tips for High-Performance Motion

Troubleshooting Common Animation Issues

Problem: My animation is stuttering (Jank).
I’ve found that this is usually caused by doing too much work in the build() method. Ensure you aren’t performing heavy logic or API calls inside a widget that is animating 60 times per second.

Problem: Memory leaks after navigating away.
This happens if you forget to call _controller.dispose(). Always override the dispose() method in your State class when using explicit animations.

What’s Next?

Once you’ve mastered basic transitions, I suggest exploring Rive for complex vector animations or Hero animations for seamless page transitions. If your app involves location-based services, you might want to learn how to integrate google maps in flutter guide and then animate the camera movement for a smoother user experience.

Ready to scale? If you’re building a complex enterprise app, don’t just animate for beauty—animate for usability. Focus on motion that guides the user’s eye to the next action.