iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Animations

Flutter has three layers of animation: AnimatedFoo widgets for one-shot tween transitions, AnimationController + Tween for custom curves, and the Flutter animations packages Hero / shared-axis / fade-through transitions for screen-level polish. Default to implicit widgets, drop to controllers when timing matters, reach for the package for transitions.

Implicit, explicit, and hero animations

EXAMPLE
import 'package:flutter/material.dart';

// 1) AnimatedContainer — easiest possible animation
class ImplicitBox extends StatefulWidget {
  const ImplicitBox({super.key});
  @override
  State<ImplicitBox> createState() => _ImplicitBoxState();
}
class _ImplicitBoxState extends State<ImplicitBox> {
  bool big = false;
  @override
  Widget build(BuildContext c) {
    return GestureDetector(
      onTap: () => setState(() => big = !big),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 220),
        curve: Curves.easeOutCubic,
        width:  big ? 240 : 120,
        height: big ? 240 : 120,
        decoration: BoxDecoration(
          color: big ? Colors.indigo : Colors.indigoAccent,
          borderRadius: BorderRadius.circular(big ? 24 : 8),
        ),
      ),
    );
  }
}

// 2) AnimationController — full control of timing
class PulseRing extends StatefulWidget {
  const PulseRing({super.key});
  @override
  State<PulseRing> createState() => _PulseRingState();
}
class _PulseRingState extends State<PulseRing> with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  late final Animation<double> _scale;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(vsync: this, duration: const Duration(seconds: 2))
      ..repeat(reverse: true);
    _scale = CurvedAnimation(parent: _ctrl, curve: Curves.easeInOutSine);
  }
  @override
  void dispose() { _ctrl.dispose(); super.dispose(); }

  @override
  Widget build(BuildContext c) {
    return ScaleTransition(
      scale: Tween<double>(begin: 1, end: 1.15).animate(_scale),
      child: const CircleAvatar(radius: 40, child: Icon(Icons.favorite)),
    );
  }
}

// 3) Tweens + Listener — drive arbitrary values
class CountUp extends StatefulWidget {
  const CountUp({super.key, required this.to});
  final int to;
  @override
  State<CountUp> createState() => _CountUpState();
}
class _CountUpState extends State<CountUp> with SingleTickerProviderStateMixin {
  late final AnimationController _c;
  @override
  void initState() {
    super.initState();
    _c = AnimationController(vsync: this, duration: const Duration(seconds: 2))..forward();
  }
  @override
  void dispose() { _c.dispose(); super.dispose(); }
  @override
  Widget build(BuildContext ctx) {
    return AnimatedBuilder(
      animation: _c,
      builder: (_, __) => Text('${(widget.to * _c.value).round()}', style: const TextStyle(fontSize: 48)),
    );
  }
}

// 4) Hero — shared element across routes
class ListPage extends StatelessWidget {
  const ListPage({super.key});
  @override
  Widget build(BuildContext c) => Scaffold(
    body: ListView(children: [
      for (final id in ['a','b','c']) ListTile(
        leading: Hero(tag: 'avatar-$id',
          child: CircleAvatar(child: Text(id.toUpperCase()))),
        title: Text('Item $id'),
        onTap: () => Navigator.of(c).push(MaterialPageRoute(
          builder: (_) => DetailPage(id: id))),
      ),
    ]),
  );
}

class DetailPage extends StatelessWidget {
  const DetailPage({super.key, required this.id});
  final String id;
  @override
  Widget build(BuildContext c) => Scaffold(
    appBar: AppBar(),
    body: Center(child: Hero(tag: 'avatar-$id',
      child: CircleAvatar(radius: 80, child: Text(id.toUpperCase(), style: const TextStyle(fontSize: 48))))),
  );
}

// 5) Page transitions via the 'animations' package
// pubspec.yaml -> animations: ^2.0.0
// import 'package:animations/animations.dart';
// Navigator.of(c).push(PageRouteBuilder(
//   transitionsBuilder: (_, anim, sec, child) => SharedAxisTransition(
//       animation: anim, secondaryAnimation: sec,
//       transitionType: SharedAxisTransitionType.horizontal,
//       child: child),
//   pageBuilder: (_, __, ___) => const DetailPage(id: 'a'),
// ));

Why it matters

Always pass `vsync: this` (a TickerProvider) to an AnimationController and call `dispose()` in dispose(). The vsync prevents ticking when the route is off-screen; forgetting it drains battery and produces "the dial keeps spinning even though the user navigated away" reports. Dispose stops the controller cleanly — without it, you leak the ticker per route push.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
AnimatedContainer(
    duration: const Duration(milliseconds: 400),
    curve: Curves.easeOut,
    width: open ? 200 : 100,
    height: open ? 200 : 100,
    color: open ? Colors.green : Colors.blue,
)
Try it Yourself »

Discussion

Loading…