Skip to content
Blog

Flutter Animation Fundamentals: From Implicit to Explicit

Master Flutter animations by understanding implicit animations, explicit AnimationController, and when to use each approach. Code examples for real-world scenarios.

Published on September 15, 2026

AI Assistant

Flutter animations fall into two categories: implicit (automatic) and explicit (controlled). Understanding when to use each is the difference between buttery-smooth UIs and janky, broken transitions.

Implicit animations: change a property, get an animation

Implicit animations animate property changes automatically. No controllers, no ticks — just set the new value:

AnimatedContainer(
  duration: Duration(milliseconds: 300),
  curve: Curves.easeInOut,
  width: _expanded ? 200 : 100,
  color: _expanded ? Colors.blue : Colors.red,
  child: Center(child: Text('Tap')),
)

When _expanded changes, AnimatedContainer interpolates between old and new values over 300ms.

Common implicit animation widgets

WidgetAnimates
AnimatedContainerSize, color, padding, margin, decoration
AnimatedOpacityOpacity
AnimatedPaddingPadding
AnimatedPositionedPosition (inside Stack)
AnimatedAlignAlignment
AnimatedSwitcherCross-fade between children
AnimatedDefaultTextStyleText style
AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  child: _isLoading
      ? CircularProgressIndicator(key: ValueKey('loading'))
      : Icon(Icons.check, key: ValueKey('done')),
)

AnimatedSwitcher cross-fades between children when the key changes.

Explicit animations: AnimationController

For complex, sequenced, or interactive animations, use AnimationController:

class PulseAnimation extends StatefulWidget {
  @override
  _PulseAnimationState createState() => _PulseAnimationState();
}

class _PulseAnimationState extends State<PulseAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 600),
      vsync: this,
    );
    _scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _scaleAnimation,
      child: GestureDetector(
        onTap: () => _controller.forward(from: 0),
        child: Container(
          width: 100,
          height: 100,
          color: Colors.blue,
        ),
      ),
    );
  }
}

AnimationController fundamentals

AnimationController drives animations frame by frame:

// Forward: 0.0 → 1.0
_controller.forward();

// Reverse: 1.0 → 0.0
_controller.reverse();

// Repeat: loop forward
_controller.repeat();

// Animate to specific value
_controller.animateTo(0.5);

// Set value directly (no animation)
_controller.value = 0.5;

Curves: controlling animation feel

Curves determine how values interpolate over time:

// Linear: constant speed
Curves.linear

// Ease-in: slow start, fast end
Curves.easeIn

// Ease-out: fast start, slow end
Curves.easeOut

// Bounce: playful overshoot
Curves.bounceOut

// Elastic: spring-like overshoot
Curves.elasticOut

// Custom curve
CurvedAnimation(
  parent: _controller,
  curve: Interval(0.0, 0.5, curve: Curves.easeIn),
)

Staggered animations

Sequence multiple animations with Interval:

late AnimationController _controller;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;

@override
void initState() {
  super.initState();
  _controller = AnimationController(
    duration: Duration(milliseconds: 1000),
    vsync: this,
  );

  _fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Interval(0.0, 0.5, curve: Curves.easeIn),
    ),
  );

  _slideAnimation = Tween<Offset>(
    begin: Offset(0, 0.5),
    end: Offset.zero,
  ).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Interval(0.3, 1.0, curve: Curves.easeOut),
    ),
  );
}

@override
Widget build(BuildContext context) {
  return FadeTransition(
    opacity: _fadeAnimation,
    child: SlideTransition(
      position: _slideAnimation,
      child: Text('Staggered'),
    ),
  );
}

The text fades in during the first 50% of the animation, then slides up from 30% to 100%.

Hero animations

Hero animations create seamless transitions between screens:

// Screen 1
Hero(
  tag: 'product-123',
  child: Image.network('https://example.com/product.jpg'),
)

// Screen 2
Hero(
  tag: 'product-123',
  child: Image.network('https://example.com/product.jpg'),
)

When navigating, Flutter automatically animates the image from its position on Screen 1 to Screen 2.

Implicit vs. explicit: when to use each

ScenarioUse
Simple property change (size, color, opacity)Implicit
Tap-triggered pulseExplicit
Staggered entranceExplicit
Screen transitionHero + implicit
Continuous loop (spinner)Explicit + repeat
Interactive dragExplicit + gesture

Performance tips

// 1. Use Transform instead of positioning for GPU-accelerated animation
Transform.scale(
  scale: _animation.value,
  child: widget,
)

// 2. Avoid animating layout properties (width, height, padding)
// Use Transform.scale instead

// 3. Use RepaintBoundary for complex custom painters
RepaintBoundary(
  child: CustomPaint(painter: MyPainter()),
)

// 4. Dispose controllers to prevent memory leaks
@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

AnimationController with vsync

Always pass this as vsync to AnimationController:

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin { // Required for vsync
  late AnimationController _controller;
  // ...
}

The TickerProviderStateMixin provides vsync callbacks that sync animations with screen refresh rates.

Flutter animations are powerful once you understand the two approaches. Start with implicit animations for simple cases, and reach for AnimationController when you need control over timing, sequencing, or interactivity.