Flutter Docs
FlutterIntermediate

Pengantar

Animasi membuat UI hidup, responsif, dan profesional. Flutter memiliki dua pendekatan:

  • Implicit Animations: Anda ubah properti, Flutter yang menganimasikan transisi.
  • Explicit Animations: Anda kontrol penuh — durasi, curve, arah, status.
SkenarioPendekatan
Hover, color/size changeImplicit
Shared element (Hero)Implicit / Explicit
Custom spinnerExplicit
Multi-step animationExplicit (Staggered)
Drag gestureExplicit

Rule of thumb: Mulai dengan implicit. Jika perlu repeat/reverse/status callback, beralih ke explicit.

1. AnimatedContainer

Otomatis menganimasikan properti apapun saat diubah via setState.

import 'package:flutter/material.dart';

class AnimatedBox extends StatefulWidget {
  const AnimatedBox({super.key});
  @override
  State<AnimatedBox> createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedContainer')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            AnimatedContainer(
              duration: const Duration(milliseconds: 400),
              curve: Curves.easeInOutCubic,
              width: _expanded ? 280 : 120,
              height: _expanded ? 200 : 120,
              decoration: BoxDecoration(
                color: _expanded ? Colors.deepPurple : Colors.amber,
                borderRadius: BorderRadius.circular(_expanded ? 24 : 60),
              ),
              child: Center(
                child: Text(
                  _expanded ? 'Expanded' : 'Tap Me',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: _expanded ? 24 : 18,
                  ),
                ),
              ),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () => setState(() => _expanded = !_expanded),
              child: Text(_expanded ? 'Collapse' : 'Expand'),
            ),
          ],
        ),
      ),
    );
  }
}

Properti animatable: width, height, color, decoration, borderRadius, margin, padding, transform.

2. AnimatedOpacity

AnimatedOpacity(
  opacity: _visible ? 1.0 : 0.0,
  duration: const Duration(milliseconds: 500),
  child: Container(width: 200, height: 200, color: Colors.teal),
)

Performance: AnimatedOpacity lebih efisien daripada Opacity widget.

3. AnimatedAlign / AnimatedPadding / AnimatedPositioned

AnimatedAlign(
  alignment: _alignment,
  duration: const Duration(milliseconds: 600),
  curve: Curves.easeInOutBack,
  child: Container(width: 80, height: 80, color: Colors.indigo),
)

4. TweenAnimationBuilder — Custom Tween

TweenAnimationBuilder<Color?>(
  tween: ColorTween(begin: Colors.orange, end: Colors.purple),
  duration: const Duration(seconds: 2),
  curve: Curves.easeInOut,
  builder: (context, color, child) {
    return Container(
      width: 200, height: 200,
      color: color,
      child: child,
    );
  },
  child: const Center(
    child: Text('Color Tween', style: TextStyle(color: Colors.white)),
  ),
)

Tentukan Tween + builder untuk kustomisasi penuh.

5. Curve Types

Curves.easeInOut       // lambat-cepat-lambat
Curves.easeIn          // lambat ke cepat
Curves.easeOut         // cepat ke lambat
Curves.bounceOut       // overshoot + pantul
Curves.elasticOut      // overshoot + osilasi
Curves.fastOutSlowIn   // Material default

6. Hero — Shared Element Transition

// Screen 1
Hero(
  tag: 'product_${product.id}',
  child: Image.network(product.imageUrl, width: 50, height: 50),
)

// Screen 2 (tag harus SAMA)
Hero(
  tag: 'product_${product.id}',
  child: Image.network(product.imageUrl, width: double.infinity, height: 300),
)

Aturan: Tag unique dalam satu route. Hero harus ada di kedua screen. Flight hanya saat push/pop.

7. AnimationController — Explicit Animation

class ControllerDemo extends StatefulWidget {
  const ControllerDemo({super.key});
  @override
  State<ControllerDemo> createState() => _ControllerDemoState();
}

class _ControllerDemoState extends State<ControllerDemo>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  late final Animation<double> _anim;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );
    _anim = Tween<double>(begin: 0, end: 300).animate(
      CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut),
    );
  }

  @override
  void dispose() {
    _ctrl.dispose(); // WAJIB
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimationController')),
      body: Center(
        child: AnimatedBuilder(
          animation: _anim,
          builder: (context, child) => Container(
            width: _anim.value, height: _anim.value,
            decoration: BoxDecoration(
              color: Colors.amber,
              borderRadius: BorderRadius.circular(16),
            ),
            child: child,
          ),
          child: const Center(
            child: Text('Animated', style: TextStyle(color: Colors.white)),
          ),
        ),
      ),
      floatingActionButton: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(onPressed: () => _ctrl.forward(),
              child: const Icon(Icons.play_arrow)),
          const SizedBox(width: 4),
          FloatingActionButton(onPressed: () => _ctrl.reverse(),
              child: const Icon(Icons.replay)),
          const SizedBox(width: 4),
          FloatingActionButton(onPressed: () => _ctrl.repeat(reverse: true),
              child: const Icon(Icons.loop)),
          const SizedBox(width: 4),
          FloatingActionButton(onPressed: () => _ctrl.stop(),
              child: const Icon(Icons.stop)),
        ],
      ),
    );
  }
}

vsync

SingleTickerProviderStateMixin = 1 controller, TickerProviderStateMixin = multiple:

class MultiCtrl extends State<MyWidget> with TickerProviderStateMixin {
  late final _c1 = AnimationController(vsync: this, duration: Duration(seconds: 1));
  late final _c2 = AnimationController(vsync: this, duration: Duration(seconds: 2));
}

8. Tween Types + Chaining

final sizeTween = Tween<Size>(begin: Size(100, 100), end: Size(200, 300));
final colorTween = ColorTween(begin: Colors.blue, end: Colors.red);
final offsetTween = Tween<Offset>(begin: Offset.zero, end: Offset(100, 200));

final curved = CurvedAnimation(
  parent: controller,
  curve: Curves.easeInOutCubic,
  reverseCurve: Curves.easeOutCubic, // opsional
);

Chaining Multiple Animations

class ChainAnim extends StatefulWidget {
  const ChainAnim({super.key});
  @override
  State<ChainAnim> createState() => _ChainAnimState();
}

class _ChainAnimState extends State<ChainAnim>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  late final Animation<Offset> _slide;
  late final Animation<double> _fade;
  late final Animation<double> _scale;

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

    _slide = Tween<Offset>(begin: const Offset(0, -1), end: Offset.zero)
        .animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
    _fade = Tween<double>(begin: 0, end: 1)
        .animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeIn));
    _scale = Tween<double>(begin: 0.5, end: 1)
        .animate(CurvedAnimation(parent: _ctrl, curve: Curves.elasticOut));
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Chain')),
      body: Center(
        child: AnimatedBuilder(
          animation: _ctrl,
          builder: (context, _) => Transform.scale(
            scale: _scale.value,
            child: SlideTransition(
              position: _slide,
              child: FadeTransition(
                opacity: _fade,
                child: Container(width: 200, height: 200,
                  decoration: BoxDecoration(
                    color: Colors.deepOrange,
                    borderRadius: BorderRadius.circular(20),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _ctrl.isCompleted ? _ctrl.reverse() : _ctrl.forward(),
        child: Icon(_ctrl.isCompleted ? Icons.replay : Icons.play_arrow),
      ),
    );
  }
}

9. addListener + addStatusListener

// Setiap frame
_animation.addListener(() { /* akses _animation.value */ });

// Status berubah
_controller.addStatusListener((AnimationStatus status) {
  switch (status) {
    case AnimationStatus.completed:
      _controller.reverse(); // auto reverse
    case AnimationStatus.dismissed:
      _controller.forward(); // auto forward (loop)
    default: break;
  }
});

// Loop patterns
_controller.repeat();
_controller.repeat(reverse: true);

Gunakan AnimatedBuilder daripada addListener + setState. AnimatedBuilder hanya rebuild widget dalam builder, bukan seluruh tree.

10. Staggered Animation — Cascade Multi-Step

Gunakan Interval(0.0, 0.4) untuk menentukan kapan animasi jalan dalam rentang durasi.

class StaggeredAnim extends StatefulWidget {
  const StaggeredAnim({super.key});
  @override
  State<StaggeredAnim> createState() => _StaggeredAnimState();
}

class _StaggeredAnimState extends State<StaggeredAnim>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  late final Animation<Offset> _slide1;
  late final Animation<Offset> _slide2;
  late final Animation<double> _scale;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 2000));

    _slide1 = Tween<Offset>(begin: const Offset(-1, 0), end: Offset.zero).animate(
      CurvedAnimation(parent: _ctrl,
          curve: const Interval(0.0, 0.4, curve: Curves.easeOut)));
    _slide2 = Tween<Offset>(begin: const Offset(1, 0), end: Offset.zero).animate(
      CurvedAnimation(parent: _ctrl,
          curve: const Interval(0.2, 0.6, curve: Curves.easeOut)));
    _scale = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _ctrl,
          curve: const Interval(0.5, 1.0, curve: Curves.elasticOut)));
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Staggered')),
      body: Center(
        child: Column(mainAxisSize: MainAxisSize.min, children: [
          SlideTransition(position: _slide1, child: _box('Item 1', Colors.blue)),
          const SizedBox(height: 12),
          SlideTransition(position: _slide2, child: _box('Item 2', Colors.green)),
          const SizedBox(height: 12),
          AnimatedBuilder(
            animation: _ctrl,
            builder: (context, child) =>
                Transform.scale(scale: _scale.value, child: child),
            child: _box('Item 3', Colors.orange),
          ),
        ]),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _ctrl.isCompleted ? _ctrl.reverse() : _ctrl.forward(),
        child: Icon(_ctrl.isCompleted ? Icons.replay : Icons.play_arrow),
      ),
    );
  }

  Widget _box(String t, Color c) => Container(width: 200, height: 60,
      color: c, alignment: Alignment.center,
      child: Text(t, style: const TextStyle(color: Colors.white, fontSize: 18)));
}

List Staggered Entry

ListView.builder(
  itemCount: 5,
  itemBuilder: (context, i) {
    final delay = i / 5;
    return SlideTransition(
      position: Tween<Offset>(begin: const Offset(1, 0), end: Offset.zero).animate(
        CurvedAnimation(parent: _controller,
            curve: Interval(delay, delay + 0.2, curve: Curves.easeOut)),
      ),
      child: Card(child: ListTile(title: Text('Item ${i + 1}'))),
    );
  },
)

11. Performance Tips

// 1. Gunakan RepaintBoundary
RepaintBoundary(
  child: AnimatedBuilder(animation: animation, builder: ...),
)

// 2. Hindari animasi layout — pakai transform
Transform.translate(offset: Offset(value, 0), child: Container(width: 100))

// 3. AnimatedBuilder > addListener + setState
// AnimatedBuilder hanya rebuild widget dalam builder

// 4. Dispose AnimationController di dispose()
@override
void dispose() { _controller.dispose(); super.dispose(); }

12. Common Animation Patterns

Pulse Effect

class PulseAnim extends StatefulWidget {
  final Widget child;
  const PulseAnim({super.key, required this.child});
  @override
  State<PulseAnim> createState() => _PulseAnimState();
}

class _PulseAnimState extends State<PulseAnim>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  late final Animation<double> _anim;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(vsync: this,
        duration: const Duration(milliseconds: 1200))..repeat(reverse: true);
    _anim = Tween<double>(begin: 0.95, end: 1.05)
        .animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut));
  }

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

  @override
  Widget build(BuildContext context) => AnimatedBuilder(
        animation: _anim,
        builder: (context, child) =>
            Transform.scale(scale: _anim.value, child: child),
        child: widget.child,
      );
}

Shake Effect

class ShakeAnim extends StatefulWidget {
  final Widget child;
  const ShakeAnim({super.key, required this.child});
  @override
  State<ShakeAnim> createState() => _ShakeAnimState();
}

class _ShakeAnimState extends State<ShakeAnim>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  late final Animation<Offset> _anim;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(vsync: this,
        duration: const Duration(milliseconds: 400));
    _anim = TweenSequence<Offset>([
      TweenSequenceItem(
          tween: Tween(begin: Offset.zero, end: const Offset(-10, 0)), weight: 1),
      TweenSequenceItem(
          tween: Tween(begin: const Offset(-10, 0), end: const Offset(10, 0)), weight: 1),
      TweenSequenceItem(
          tween: Tween(begin: const Offset(10, 0), end: Offset.zero), weight: 1),
    ]).animate(_ctrl);
  }

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

  void shake() => _ctrl.forward();

  @override
  Widget build(BuildContext context) => GestureDetector(
        onTap: shake,
        child: SlideTransition(position: _anim, child: widget.child),
      );
}

Loading Dots

class LoadingDots extends StatefulWidget {
  const LoadingDots({super.key});
  @override
  State<LoadingDots> createState() => _LoadingDotsState();
}

class _LoadingDotsState extends State<LoadingDots>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(vsync: this,
        duration: const Duration(milliseconds: 1200))..repeat(reverse: true);
  }

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

  @override
  Widget build(BuildContext context) => Row(
        mainAxisSize: MainAxisSize.min,
        children: List.generate(3, (i) => AnimatedBuilder(
          animation: _ctrl,
          builder: (context, _) {
            final v = ((_ctrl.value - i * 0.15) % 1.0).clamp(0.0, 1.0);
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 4),
              child: Container(
                width: 8 + v * 12, height: 8 + v * 12,
                decoration: BoxDecoration(
                  color: Colors.blue.withOpacity(0.3 + v * 0.7),
                  shape: BoxShape.circle,
                ),
              ),
            );
          },
        )),
      );
}

Common Mistakes

  1. Lupa dispose AnimationController — Memory leak paling umum. Selalu panggil di dispose().
  2. setState di addListener — Pakai AnimatedBuilder yang hanya rebuild widget spesifik.
  3. Tidak pakai RepaintBoundary — Animasi kompleks sebabkan layout shock.
  4. Terlalu banyak properti dianimasikan — Pilih 2-3 properti paling penting.
  5. Hero tag tidak unique — Prefix seperti 'product_${product.id}'.
  6. Implicit untuk animasi kompleks — Migrasi ke explicit saat butuh repeat/reverse.

Summary

AnimasiWidgetUse Case
Hover/TapAnimatedContainerButton, Card
FadeAnimatedOpacityLoading, visibility
Page transitionHeroNavigation
LoadingAnimationController.repeat()Spinner
List enterStaggeredList items
Gesture-drivenAnimationController + GestureDetectorSwipe, drag

Apa Selanjutnya?

  • Architecture Patterns — Riverpod, Bloc, Clean Architecture
  • CustomPainter — Animasi kustom dengan canvas
  • Rive / Lottie — Animasi dari design tools
  • Navigation 2.0 — Router + animated transitions

On this page