Enhance your Flutter applications with animations

Feb 1, 2025

This article is part of the series:

Animations are a key element in modern application development. They not only make an interface visually more appealing, but also enhance the user experience by providing smooth transitions and intuitive effects, as long as they are well-designed and do not interfere with the application’s usability.

Animations are a key element in modern application development. They not only make an interface visually more appealing, but also enhance the user experience by providing smooth transitions and intuitive effects, as long as they are well-designed and do not interfere with the application’s usability.

Throughout the Flutter series, we have explored different concepts, from the Dart language,creating user interfaces,the most common widgets, to asynchrony.This time, we will learn how to create animations to give our applications a more dynamic and engaging look.

Flutter, thanks to its highly optimized graphics engine, makes it easy to implement animations without affecting application performance. In this article, we will explore three basic types of animations in Flutter, with practical and easy-to-implement examples:

  • Implicit animations – Flutter automatically handles value interpolation.
  • Explicit animations – Manual animation control using AnimationController.
  • Hero animation – Smooth transitions between screens using a shared element.

Next, we will develop an application that will allow us to showcase
each of these types of animations in a single app while analyzing the most common use cases and their possible applications.

Basic types of animations in Flutter and their uses

Flutter offers a powerful and flexible animation system that adapts to different needs.
To begin with, it is essential to understand the three most basic animations that will allow us to bring our UI to life in a simple and effective way.

Below, we will explore how they work, how they differ, and when it is advisable to use them.

Implicit animations

Implicit animations are the easiest to implement in Flutter. They are used when we want certain
state changes in a widget to be animated automatically without the need to manually control timing or interpolation.

How do they work?

  • Flutter detects changes in the widget’s properties and animates the transition between the previous and new values.
  • An AnimationController is not required, as the animation is managed internally.
  • They are defined using specific widgets such as:
    • AnimatedContainer – Animates properties like size, color, and borders.
    • AnimatedOpacity – Changes the transparency of a widget.
    • AnimatedAlign – Smoothly modifies a widget’s alignment.
    • AnimatedSwitcher – Animates the transition between widgets when they are replaced.

When to use them?

  • When a simple and automatic animation is needed without much control.
  • For UI effects such as size, color, or alignment changes in response to user interaction.
  • To create smooth transitions between widgets without writing complex code.

Common use cases

  • A button that changes color when pressed.
  • A container that grows or shrinks when the user interacts with it.
  • An icon that fades in or out with a transition effect.

Explicit animations

Explicit animations provide greater control over timing, speed, and animation behavior.
They are useful when we want to define more precise transitions or when animations depend on more complex interactions.

How do they work?

  • An AnimationController is used to manage the animation timing.
  • Interpolation ranges are defined using Tween, and animation curves are applied with CurvedAnimation.
  • AnimatedBuilder can be used to optimize performance by rebuilding only the necessary parts of the widget.

When to use them?

  • When precise control over the animation’s duration and state is needed.
  • For animations that depend on user gestures or multiple states.
  • For more advanced effects such as looping animations, custom accelerations, or complex transformations.

Common use cases

  • A button that gradually grows and shrinks in size.
  • A manually controlled animated progress bar.
  • A widget that bounces at the end of an animation using a custom curve.

Hero animation

Hero animations allow smooth transitions between screens when an element maintains its visual appearance in both views.
They are ideal for improving the navigation experience in an app.

How do they work?

  • The Hero widget is used, assigning a shared tag (tag) to a widget on both screens.
  • When navigating between screens, Flutter automatically animates the widget’s transformation between its original and final position.

We noticed a small issue in our practical example where the text size does not adjust correctly during the Hero animation.
It would be worth investigating the cause and fixing it, as it is likely related to how the widget’s size is being managed during the transition.

When to use them?

  • To create smoother transitions between screens.
  • When highlighting an element on a new screen (e.g., opening an image in detail).
  • To improve navigation between related views.

Common use cases

  • A user selects an image from a gallery, and it expands into a new screen with more details.
  • A product in a list animates into a detailed view when tapped.
  • A contact avatar enlarges when navigating to the user’s profile screen.

In the next part of the article, we will create a Flutter application that demonstrates each of these animations with practical examples.

Example application with animations

This Flutter application allows us to explore the three types of animations mentioned earlier:

  • Implicit animations – Smooth transitions for properties like size, color, and alignment.
  • Explicit animations – Manual control of animation using AnimationController.
  • Hero animation – A seamless transition of a widget between screens.

Next, we will explain the purpose of each screen and the widgets used.

Main screen

The main screen serves as a menu to access the different animations. It contains:

  • A title introducing the animations.
  • Three cards (Card) that act as buttons to navigate to each animation screen.
  • Each card includes an icon, a title, and a brief description.

Widgets used

  • Scaffold: Base structure of the screen.
  • Column: Organizes elements in a vertical list.
  • Card: Creates visually appealing buttons.
  • InkWell: Detects taps on each card.
  • Navigator: Handles navigation to the animation screens.

Source code

  
  class HomeScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Animation Demo'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                'Explore Different Types of Animations',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              SizedBox(height: 30),
              _buildAnimationButton(
                context,
                'Implicit Animations',
                'Automatically animate size, color, opacity, and alignment.',
                Icons.animation,
                ImplicitAnimationsScreen(),
              ),
              SizedBox(height: 20),
              _buildAnimationButton(
                context,
                'Explicit Animations',
                'Manually control scaling animations with start/reverse buttons.',
                Icons.touch_app,
                ExplicitAnimationsScreen(),
              ),
              SizedBox(height: 20),
              _buildAnimationButton(
                context,
                'Hero Animation',
                'Tap the widget to see a smooth transition between screens.',
                Icons.flight_takeoff,
                HeroAnimationScreen(),
              ),
            ],
          ),
        ),
      );
    }
  
    Widget _buildAnimationButton(BuildContext context, String title, String description, IconData icon, Widget screen) {
      return Card(
        elevation: 4,
        child: InkWell(
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => screen),
            );
          },
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                Icon(icon, size: 40, color: Colors.blue),
                SizedBox(height: 10),
                Text(
                  title,
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 5),
                Text(
                  description,
                  style: TextStyle(fontSize: 12, color: Colors.grey),
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        ),
      );
    }
  }
    

Implicit animations screen

This screen demonstrates how to use implicit animations in Flutter.
State changes in widgets automatically trigger transitions in size, color, and alignment.

It includes three animation examples:

  1. AnimatedContainer – Changes size and color when a button is pressed.
  2. AnimatedOpacity – Adjusts a widget’s transparency.
  3. AnimatedAlign – Modifies a widget’s alignment within its container.

Each animation is triggered by pressing a button, changing the screen’s state.

Widgets used

  • AnimatedContainer: For smooth transitions in visual properties.
  • AnimatedOpacity: For fade-in and fade-out effects.
  • AnimatedAlign: To animate a widget’s positioning.
  • FloatingActionButton: To navigate back to the main screen.

Source code

  
  class _ImplicitAnimationsScreenState extends State<ImplicitAnimationsScreen> {
    bool _expanded = false;
    bool _visible = true;
    Alignment _alignment = Alignment.topLeft;
  
    void _toggleExpanded() {
      setState(() {
        _expanded = !_expanded;
      });
    }
  
    void _toggleVisibility() {
      setState(() {
        _visible = !_visible;
      });
    }
  
    void _changeAlignment() {
      setState(() {
        _alignment = _alignment == Alignment.topLeft
            ? Alignment.bottomRight
            : Alignment.topLeft;
      });
    }
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Implicit Animations'),
        ),
        body: SingleChildScrollView(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                '1. AnimatedContainer',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              Text('Tap the button to toggle size and color.'),
              SizedBox(height: 10),
              Center(
                child: AnimatedContainer(
                  duration: Duration(seconds: 1),
                  width: _expanded ? 200 : 100,
                  height: _expanded ? 200 : 100,
                  color: _expanded ? Colors.blue : Colors.red,
                  child: Center(
                    child: Text(
                      _expanded ? 'Expanded' : 'Small',
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: _toggleExpanded,
                child: Text('Toggle Container'),
              ),
              SizedBox(height: 30),
              Text(
                '2. AnimatedOpacity',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              Text('Tap the button to toggle visibility.'),
              SizedBox(height: 10),
              Center(
                child: AnimatedOpacity(
                  opacity: _visible ? 1.0 : 0.0,
                  duration: Duration(seconds: 1),
                  child: Container(
                    width: 100,
                    height: 100,
                    color: Colors.green,
                    child: Center(
                      child: Text(
                        _visible ? 'Visible' : 'Invisible',
                        style: TextStyle(color: Colors.white),
                      ),
                    ),
                  ),
                ),
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: _toggleVisibility,
                child: Text('Toggle Opacity'),
              ),
              SizedBox(height: 30),
              Text(
                '3. AnimatedAlign',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              Text('Tap the button to change alignment.'),
              SizedBox(height: 10),
              Container(
                height: 200,
                width: double.infinity,
                color: Colors.grey[200],
                child: AnimatedAlign(
                  alignment: _alignment,
                  duration: Duration(seconds: 1),
                  child: Container(
                    width: 100,
                    height: 100,
                    color: Colors.orange,
                    child: Center(
                      child: Text(
                        _alignment == Alignment.topLeft
                            ? 'Top Left'
                            : 'Bottom Right',
                        style: TextStyle(color: Colors.white),
                      ),
                    ),
                  ),
                ),
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: _changeAlignment,
                child: Text('Change Alignment'),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Icon(Icons.arrow_back),
        ),
      );
    }
  }
  

Explicit animations screen

This screen demonstrates the use of explicit animations, where the developer manually controls the animation using an AnimationController.

It includes an example where a Container smoothly changes size:

  • One button starts the animation.
  • Another button reverses the animation.

Widgets used

  • AnimationController: Controls the animation’s duration and state.
  • Tween<double>: Defines a range of values for interpolation.
  • CurvedAnimation: Applies an acceleration and deceleration curve to the animation.
  • AnimatedBuilder: Optimizes performance by avoiding unnecessary rebuilds.
  • FloatingActionButton: A standard floating button, used here to return to the main screen.

Source code

  
  class _ExplicitAnimationsScreenState extends State<ExplicitAnimationsScreen> with SingleTickerProviderStateMixin {
    late AnimationController _controller;
    late Animation<double> _animation;
    bool _isAnimating = false;
  
    @override
    void initState() {
      super.initState();
      _controller = AnimationController(
        duration: Duration(seconds: 1),
        vsync: this,
      );
      _animation = Tween<double>(begin: 100, end: 200).animate(
        CurvedAnimation(
          parent: _controller,
          curve: Curves.easeInOut,
        ),
      );
    }
  
    void _startAnimation() {
      _controller.forward();
      setState(() {
        _isAnimating = true;
      });
    }
  
    void _reverseAnimation() {
      _controller.reverse();
      setState(() {
        _isAnimating = false;
      });
    }
  
    @override
    void dispose() {
      _controller.dispose();
      super.dispose();
    }
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Explicit Animations'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                '1. Scaling Animation',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              Text('Use the buttons to start or reverse the animation.'),
              SizedBox(height: 20),
              Center(
                child: AnimatedBuilder(
                  animation: _animation,
                  builder: (context, child) {
                    return Container(
                      width: _animation.value,
                      height: _animation.value,
                      color: Colors.purple,
                      child: Center(
                        child: Text(
                          _isAnimating ? 'Expanding' : 'Shrinking',
                          style: TextStyle(color: Colors.white),
                        ),
                      ),
                    );
                  },
                ),
              ),
              SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: _startAnimation,
                    child: Text('Start Animation'),
                  ),
                  SizedBox(width: 20),
                  ElevatedButton(
                    onPressed: _reverseAnimation,
                    child: Text('Reverse Animation'),
                  ),
                ],
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Icon(Icons.arrow_back),
        ),
      );
    }
  }
    

Hero animation screen

To conclude, we implement a Hero animation for a smooth transition between screens.

  • A user taps a widget on the main screen.
  • The widget expands and moves smoothly to the second screen.
  • When returning, the widget shrinks back to its original position.

Widgets used

  • Hero: Defines the widget that will be animated between screens.
  • Navigator: Manages navigation between screens.
  • GestureDetector: Detects user taps to trigger the animation.

Source code

  
  class HeroAnimationScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Hero Animation'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                '1. Hero Transition',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              Text('Tap the widget to see a smooth transition to the next screen.'),
              SizedBox(height: 20),
              Center(
                child: GestureDetector(
                  onTap: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(builder: (context) => HeroDetailScreen()),
                    );
                  },
                  child: Hero(
                    tag: 'heroTag',
                    child: Container(
                      width: 100,
                      height: 100,
                      color: Colors.teal,
                      child: Center(
                        child: Text(
                          'Tap Me!',
                          style: TextStyle(color: Colors.white),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Icon(Icons.arrow_back),
        ),
      );
    }
  }
  

As we mentioned earlier, there is a small issue with the Hero animation where the text does not adjust correctly when resizing.
This could be a great starting point for further investigation and improving our understanding of this type of animation.

While we have highlighted the advantages of using animations in our applications, it is also important to consider that excessive animations
can lead to a confusing or unpleasant user experience. Balancing the amount and intensity of animations is key to ensuring a smooth and effective user experience.

We have covered the basic concepts of animations in Flutter and created an example application
that demonstrates each of these types of animations in action. Now, you can apply this knowledge to your own projects and bring your user interfaces to life
with engaging and effective animations. I hope you found this guide helpful, and most importantly… Happy Coding!

Related posts

That may interest you

September 15, 2024

Asynchrony in Flutter

Introduction to asynchrony in programming In the world of programming, one of the biggest challenges …

read more