Skip to main content

Command Palette

Search for a command to run...

flutter_animate: Say Goodbye to AnimationController Boilerplate

How one package changed the way I write Flutter animations

Updated
6 min readView as Markdown

flutter_animate: Say Goodbye to AnimationController Boilerplate

I spent 30 minutes writing a fade-in animation last week. Not because the effect was complex—just a simple opacity transition. But by the time I set up the AnimationController, mixed in SingleTickerProviderStateMixin, created the Tween, and remembered to dispose(), I questioned my life choices.

Then I discovered flutter_animate. Now that same animation is one line. Let me show you.

Table of Contents


The Problem: Animation Boilerplate

Here's what a simple fade-in looks like with vanilla Flutter:

// Before: ~30 lines for a fade-in
class FadeInWidget extends StatefulWidget {
  @override
  _FadeInWidgetState createState() => _FadeInWidgetState();
}

class _FadeInWidgetState extends State<FadeInWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 500),
      vsync: this,
    );
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _animation,
      child: Text("Hello World!"),
    );
  }
}

Now with flutter_animate:

// After: 1 line
Text("Hello World!").animate().fadeIn(duration: 500.ms)

That's it. One line. No StatefulWidget, no dispose(), no boilerplate.


What is flutter_animate?

Package Info

flutter_animate is a package by gskinner.com that provides a unified API for adding animations to any widget. It has:

  • 4k+ likes and 524k+ downloads on pub.dev

  • Pre-built effects: fade, scale, slide, blur, shake, shimmer, flip, and more

  • Chaining syntax: combine effects with a fluent API

  • Zero AnimationController management: it handles all the lifecycle for you

The core idea? Any widget can be animated by adding .animate().


Getting Started

Install the package:

# pubspec.yaml
dependencies:
  flutter_animate: ^4.5.2

Import it:

import 'package:flutter_animate/flutter_animate.dart';

That's all the setup you need.


Basic Usage: The Chaining Magic

Single Effect

// Fade in
Text("Hello").animate().fadeIn()

// Scale up
Icon(Icons.star).animate().scale()

// Slide from left
Card(child: content).animate().slideX(begin: -1)

Combining Effects (Simultaneous)

When effects share the same timing, they run together:

// Fade AND scale at the same time
Text("Hello")
  .animate()
  .fadeIn(duration: 600.ms)
  .scale(begin: Offset(0.8, 0.8))

Two Syntax Options

You can also use the declarative style if you prefer:

// Declarative syntax
Animate(
  effects: [
    FadeEffect(duration: 600.ms),
    ScaleEffect(begin: Offset(0.8, 0.8)),
  ],
  child: Text("Hello"),
)

I personally prefer the chaining syntax—it reads more naturally.


Timing Control

Duration Helpers

The package adds extension methods to num for cleaner duration syntax:

// These are equivalent
Duration(milliseconds: 500)  // Vanilla Flutter
500.ms                       // flutter_animate

// More examples
2.seconds
0.1.minutes

Sequential Animations with then()

Use then() to run effects in sequence:

Text("Hello")
  .animate()
  .fadeIn(duration: 300.ms)
  .then(delay: 100.ms)  // Wait 100ms after fade completes
  .slideY(begin: 0.2, end: 0)  // Then slide up

Property Inheritance

Here's something clever: if you don't specify a property, it inherits from the previous effect.

Text("Hello")
  .animate()
  .fadeIn(duration: 500.ms, curve: Curves.easeOut)
  .slideY()  // Inherits duration: 500.ms, curve: Curves.easeOut

This keeps your code DRY when multiple effects share timing.


Real-World Examples

Example 1: Login Screen Entrance

Elements appearing one after another:

class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Logo: fade + scale
            FlutterLogo(size: 80)
              .animate()
              .fadeIn(duration: 600.ms)
              .scale(begin: Offset(0.8, 0.8)),

            SizedBox(height: 48),

            // Email: slide from left
            TextField(decoration: InputDecoration(labelText: "Email"))
              .animate()
              .fadeIn(delay: 200.ms, duration: 400.ms)
              .slideX(begin: -0.2),

            SizedBox(height: 16),

            // Password: slide from right
            TextField(decoration: InputDecoration(labelText: "Password"))
              .animate()
              .fadeIn(delay: 400.ms, duration: 400.ms)
              .slideX(begin: 0.2),

            SizedBox(height: 32),

            // Button: slide up
            ElevatedButton(onPressed: () {}, child: Text("Login"))
              .animate()
              .fadeIn(delay: 600.ms, duration: 400.ms)
              .slideY(begin: 0.3),
          ],
        ),
      ),
    );
  }
}

Example 2: List Stagger Effect

Use interval to stagger animations across list items:

Column(
  children: [
    Text("Item 1"),
    Text("Item 2"),
    Text("Item 3"),
  ].animate(interval: 200.ms)  // 200ms between each
   .fadeIn(duration: 300.ms)
   .slideX(begin: -0.1),
)

Each item animates 200ms after the previous one. No manual delay calculations needed.

Example 3: Hover Effect with target

For state-driven animations, use the target parameter:

class HoverButton extends StatefulWidget {
  @override
  _HoverButtonState createState() => _HoverButtonState();
}

class _HoverButtonState extends State<HoverButton> {
  bool _isHovered = false;

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => setState(() => _isHovered = true),
      onExit: (_) => setState(() => _isHovered = false),
      child: ElevatedButton(
        onPressed: () {},
        child: Text("Hover Me"),
      )
        .animate(target: _isHovered ? 1 : 0)
        .scaleXY(end: 1.1)
        .fade(end: 0.8),
    );
  }
}

When target changes, the animation smoothly transitions to the new state.


Pro Tips

1. Looping Animations

Use onPlay to repeat:

Icon(Icons.notifications)
  .animate(onPlay: (controller) => controller.repeat(reverse: true))
  .scale(end: Offset(1.2, 1.2), duration: 500.ms)

2. Shimmer for Loading States

Perfect for skeleton screens:

Container(
  width: 200,
  height: 20,
  decoration: BoxDecoration(
    color: Colors.grey[300],
    borderRadius: BorderRadius.circular(4),
  ),
)
  .animate(onPlay: (c) => c.repeat())
  .shimmer(duration: 1200.ms)

3. Custom Effects

When built-in effects aren't enough:

Container()
  .animate()
  .custom(
    duration: 300.ms,
    builder: (context, value, child) {
      // value goes from 0.0 to 1.0
      return Transform.rotate(
        angle: value * 2 * 3.14159,  // Full rotation
        child: child,
      );
    },
  )

4. Scroll-Based Animations

Use ScrollAdapter to tie animations to scroll position:

ListView(
  controller: scrollController,
  children: items.map((item) =>
    ItemCard(item)
      .animate(adapter: ScrollAdapter(scrollController))
      .fadeIn()
      .slideX(begin: 0.2)
  ).toList(),
)

When NOT to Use

Large Lists

Avoid complex animations on lists with hundreds of items:

// Potentially problematic
ListView.builder(
  itemCount: 1000,
  itemBuilder: (_, i) => Item()
    .animate()
    .fadeIn()
    .scale()
    .blur()
    .shimmer()  // Too many effects
)

For large lists, consider animating only visible items or using simpler effects.

When You Need Fine-Grained Control

If you need to:

  • Sync multiple animations to a single controller

  • Build complex physics-based animations

  • Implement gesture-driven animations with velocity

...you might still want the native AnimationController approach.

Alternatives to Consider

Use CaseConsider
Simple entry effectsanimate_do (zero dependencies)
Material motionanimations (official package)
Complex timelinesNative AnimationController

Wrapping Up

flutter_animate removes the friction from Flutter animations. Instead of wrestling with controllers and state management, you can focus on what matters: making your UI feel alive.

Key takeaways:

  1. .animate() turns any widget into an animated widget

  2. Chain effects for simultaneous animations

  3. Use then() for sequential animations

  4. Use interval for list stagger effects

  5. Use target for state-driven animations

Next time you think "I should add a subtle animation here," you no longer have an excuse to skip it. It's literally one line.


That's it for Flutter Packages Weekly #1! Next week, I'll introduce another package that might change how you work.

Questions or suggestions? Drop a comment below.

— woogi


Sources & References:

ResourceLink
flutter_animate (pub.dev)https://pub.dev/packages/flutter_animate
GitHub Repositoryhttps://github.com/gskinner/flutter_animate
gskinner.com (Publisher)https://gskinner.com
Flutter Animation Docshttps://docs.flutter.dev/ui/animations
animate_do (Alternative)https://pub.dev/packages/animate_do
animations (Official)https://pub.dev/packages/animations

Flutter Packages Weekly

Part 1 of 1

A weekly deep dive into useful Flutter packages. Each post covers core features, practical usage examples, and real-world tips you can apply immediately.