Flutter Animations: From Beginner Basics to Heroic Transitions

Animations can transform a good Flutter app into a great one, making your UI feel alive and intuitive. Whether you’re new to Flutter or ready to level up, this tutorial will guide you from simple animations to advanced transitions. We’ll start with beginner-friendly implicit animations, move to explicit animations for more control, and finish with the stunning Hero animation that ties screens together like magic. By the end, you’ll have the skills to animate anything in Flutter. We are going to group this into Part (From basic animations and advanced and Hero animations) For a more visual tutorial, check out my youtube channel : Ready to get moving? Let’s jump in! Part 1: Basic Implicit Animations Implicit animations are perfect for beginners because Flutter handles the animation logic for you. You define the start and end states, and Flutter smoothly transitions between them. Let’s begin with AnimatedContainer. Example 1: Coloring and Growing a Box with AnimatedContainer The AnimatedContainer widget animates changes to properties like size, color, or padding. Let’s make a box that grows and changes color when tapped. import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp(home: AnimatedContainerDemo()); } } class AnimatedContainerDemo extends StatefulWidget { @override _AnimatedContainerDemoState createState() => _AnimatedContainerDemoState(); } class _AnimatedContainerDemoState extends State { bool _isExpanded = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('AnimatedContainer Demo')), body: Center( child: GestureDetector( onTap: () { setState(() { _isExpanded = !_isExpanded; }); }, child: AnimatedContainer( duration: Duration(seconds: 1), width: _isExpanded ? 200 : 100, height: _isExpanded ? 200 : 100, color: _isExpanded ? Colors.blue : Colors.red, curve: Curves.easeInOut, ), ), ), ); } } How It Works: AnimatedContainer transitions its properties over a duration (1 second here). Tapping toggles _isExpanded, changing the width, height, and color. curve: Curves.easeInOut ensures a smooth start and stop. If you run this and tap the box. It grows from 100x100 (red) to 200x200 (blue) with a silky-smooth animation. Part 2: Intermediate Implicit Animations Now that you’ve got the basics, let’s explore more implicit animations: AnimatedOpacity for fading effects and AnimatedCrossFade for switching widgets. Example 2: Fading Text with AnimatedOpacity Let’s fade a text widget in and out with a button press. class AnimatedOpacityDemo extends StatefulWidget { @override _AnimatedOpacityDemoState createState() => _AnimatedOpacityDemoState(); } class _AnimatedOpacityDemoState extends State { bool _isVisible = true; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('AnimatedOpacity Demo')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ AnimatedOpacity( opacity: _isVisible ? 1.0 : 0.0, duration: Duration(seconds: 1), child: Text( 'Hello, Flutter!', style: TextStyle(fontSize: 30), ), ), SizedBox(height: 20), ElevatedButton( onPressed: () { setState(() { _isVisible = !_isVisible; }); }, child: Text('Toggle Fade'), ), ], ), ), ); } } How It Works: AnimatedOpacity changes the opacity from 0.0 (invisible) to 1.0 (visible). The button toggles _isVisible, triggering the fade over 1 second. Example 3: Switching Widgets with AnimatedCrossFade AnimatedCrossFade lets you transition between two widgets with a crossfade effect. Let’s switch between two colored boxes in this example. class AnimatedCrossFadeDemo extends StatefulWidget { @override _AnimatedCrossFadeDemoState createState() => _AnimatedCrossFadeDemoState(); } class _AnimatedCrossFadeDemoState extends State { bool _showFirst = true; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('AnimatedCrossFade Demo')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ AnimatedCrossFade( firstChild: Container( width: 100, height: 100, color: Colors.green, ), secondChild: Container( width: 10

Apr 7, 2025 - 19:36
 0
Flutter Animations: From Beginner Basics to Heroic Transitions

Animations can transform a good Flutter app into a great one, making your UI feel alive and intuitive. Whether you’re new to Flutter or ready to level up, this tutorial will guide you from simple animations to advanced transitions. We’ll start with beginner-friendly implicit animations, move to explicit animations for more control, and finish with the stunning Hero animation that ties screens together like magic. By the end, you’ll have the skills to animate anything in Flutter.

We are going to group this into Part (From basic animations and advanced and Hero animations)

For a more visual tutorial, check out my youtube channel :

Ready to get moving? Let’s jump in!

Part 1: Basic Implicit Animations

Implicit animations are perfect for beginners because Flutter handles the animation logic for you. You define the start and end states, and Flutter smoothly transitions between them. Let’s begin with AnimatedContainer.

Example 1: Coloring and Growing a Box with AnimatedContainer

The AnimatedContainer widget animates changes to properties like size, color, or padding. Let’s make a box that grows and changes color when tapped.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: AnimatedContainerDemo());
  }
}

class AnimatedContainerDemo extends StatefulWidget {
  @override
  _AnimatedContainerDemoState createState() => _AnimatedContainerDemoState();
}

class _AnimatedContainerDemoState extends State {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('AnimatedContainer Demo')),
      body: Center(
        child: GestureDetector(
          onTap: () {
            setState(() {
              _isExpanded = !_isExpanded;
            });
          },
          child: AnimatedContainer(
            duration: Duration(seconds: 1),
            width: _isExpanded ? 200 : 100,
            height: _isExpanded ? 200 : 100,
            color: _isExpanded ? Colors.blue : Colors.red,
            curve: Curves.easeInOut,
          ),
        ),
      ),
    );
  }
}

How It Works:

AnimatedContainer transitions its properties over a duration (1 second here).

Tapping toggles _isExpanded, changing the width, height, and color.

curve: Curves.easeInOut ensures a smooth start and stop.

If you run this and tap the box. It grows from 100x100 (red) to 200x200 (blue) with a silky-smooth animation.

Part 2: Intermediate Implicit Animations

Now that you’ve got the basics, let’s explore more implicit animations: AnimatedOpacity for fading effects and AnimatedCrossFade for switching widgets.

Example 2: Fading Text with AnimatedOpacity

Let’s fade a text widget in and out with a button press.

class AnimatedOpacityDemo extends StatefulWidget {
  @override
  _AnimatedOpacityDemoState createState() => _AnimatedOpacityDemoState();
}

class _AnimatedOpacityDemoState extends State {
  bool _isVisible = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('AnimatedOpacity Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedOpacity(
              opacity: _isVisible ? 1.0 : 0.0,
              duration: Duration(seconds: 1),
              child: Text(
                'Hello, Flutter!',
                style: TextStyle(fontSize: 30),
              ),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _isVisible = !_isVisible;
                });
              },
              child: Text('Toggle Fade'),
            ),
          ],
        ),
      ),
    );
  }
}

How It Works:

AnimatedOpacity changes the opacity from 0.0 (invisible) to 1.0 (visible).

The button toggles _isVisible, triggering the fade over 1 second.

Example 3: Switching Widgets with AnimatedCrossFade

AnimatedCrossFade lets you transition between two widgets with a crossfade effect. Let’s switch between two colored boxes in this example.

class AnimatedCrossFadeDemo extends StatefulWidget {
  @override
  _AnimatedCrossFadeDemoState createState() => _AnimatedCrossFadeDemoState();
}

class _AnimatedCrossFadeDemoState extends State {
  bool _showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('AnimatedCrossFade Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedCrossFade(
              firstChild: Container(
                width: 100,
                height: 100,
                color: Colors.green,
              ),
              secondChild: Container(
                width: 100,
                height: 100,
                color: Colors.purple,
              ),
              crossFadeState: _showFirst
                  ? CrossFadeState.showFirst
                  : CrossFadeState.showSecond,
              duration: Duration(seconds: 1),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _showFirst = !_showFirst;
                });
              },
              child: Text('Switch'),
            ),
          ],
        ),
      ),
    );
  }
}

How It Works:

AnimatedCrossFade fades between firstChild (green box) and secondChild (purple box).

crossFadeState determines which child is visible, with a 1-second transition.

Part 3: Explicit Animations

Implicit animations are easy, but explicit animations give you full control using AnimationController and Tween. Let’s create a rotating icon.

Example 4: Spinning Icon

We’ll make a star icon spin 360 degrees repeatedly.

class RotationDemo extends StatefulWidget {
  @override
  _RotationDemoState createState() => _RotationDemoState();
}

class _RotationDemoState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
    _animation = Tween(begin: 0, end: 2 * 3.14159).animate(_controller)
      ..addListener(() {
        setState(() {});
      });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Rotation Demo')),
      body: Center(
        child: Transform.rotate(
          angle: _animation.value,
          child: Icon(Icons.star, size: 100),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.isAnimating) {
            _controller.stop();
          } else {
            _controller.repeat();
          }
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

How It Works:

AnimationController manages the animation’s timing (2 seconds per cycle).

Tween maps values from 0 to 2π (a full rotation).

vsync: this syncs the animation to the screen refresh rate, requiring SingleTickerProviderStateMixin.

The floating action button toggles the spinning.

Part 4: Advanced Custom Animation

In this part, we are going to combine multiple animations for a more complex effect: a ball that bounces up and down while changing size.

Example 5: Bouncing Ball

This example uses AnimatedBuilder for efficiency and combines position and size animations.

class BounceDemo extends StatefulWidget {
  @override
  _BounceDemoState createState() => _BounceDemoState();
}

class _BounceDemoState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _height;
  late Animation _size;

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

    _height = Tween(begin: 0, end: 200).animate(
      CurvedAnimation(parent: _controller, curve: Curves.bounceOut),
    );
    _size = Tween(begin: 50, end: 30).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bouncing Ball Demo')),
      body: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return Stack(
            children: [
              Positioned(
                bottom: _height.value,
                left: MediaQuery.of(context).size.width / 2 - _size.value / 2,
                child: Container(
                  width: _size.value,
                  height: _size.value,
                  decoration: BoxDecoration(
                    color: Colors.orange,
                    shape: BoxShape.circle,
                  ),
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

How It Works:

AnimatedBuilder rebuilds only the animated part, improving performance.

_height uses Curves.bounceOut for a realistic bounce effect.

_size shrinks the ball as it rises, adding depth.

repeat(reverse: true) loops the animation back and forth.

Part 5: Hero Animation

For the final part, let’s create a Hero animation—a seamless transition between two screens, often used for images or icons in galleries or detail views.

Example 6: Image Transition with Hero

We’ll build a list screen with an image that, when tapped, transitions smoothly to a larger version on a detail screen.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: HeroListScreen());
  }
}

class HeroListScreen extends StatelessWidget {
  final image = "https://picsum.photos/200/300";
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Hero Animation - List')),
      body: ListView(
        children: [
          GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => HeroDetailScreen(
imageUrl : image,
heroTag : "hero_id"
)),
              );
            },
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Hero(
                tag: 'hero_id',
                child: Image.network(
                  image,
                  width: 150,
                  height: 150,
                  fit: BoxFit.cover,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class HeroDetailScreen extends StatelessWidget {
  final String heroTag; 
  final String imageUrl;
  HeroDetailsScreen({required this.heroTag, required this.imageUrl});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Hero Animation - Detail')),
      body: Center(
        child: Hero(
          tag: heroTag,
          child: Image.network(
            imageUrl,
            width: 300,
            height: 300,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

How It Works:

The Hero widget wraps the image on both screens, linked by a unique tag (hero_id).

Tapping the image navigates to HeroDetailScreen, and Flutter animates the image from 150x150 to 300x300, adjusting position and scale.

The back button reverses the animation seamlessly.

Congratulations if you go to this part!

You’ve just unlocked the power of Flutter animations! Here’s how to keep going:

Experiment and try other implicit widgets like AnimatedPadding or AnimatedPositioned.

Customize the Hero animation by adding a flightShuttleBuilder property to the Hero widget for effects like rotation or fade during transitions.

Explore advanced topics by looking into PageRouteBuilder for custom page transitions or Rive for pre-designed animations.

There you have it!

You've successfully completed your crash course in Flutter animations! You’ve gone from basic boxes that grow and fade, to spinning icons, bouncing balls, and even the show-stopping Hero transition.

These skills are your toolkit to make apps that don’t just work, but wow.

Thank you for reading. Keep animating with Flutter!