Skip to content

Bab 11 — Animasi

Animasi bukan hiasan. Ketika sebuah elemen muncul tiba-tiba, otak pengguna kehilangan jejak — dari mana benda itu datang, apa hubungannya dengan yang tadi diketuk. Animasi yang baik menjawab pertanyaan itu tanpa kata-kata.

Flutter membagi animasi menjadi dua kelompok, dan memilih yang tepat menghemat banyak pekerjaan.

Implisit versus eksplisit

Animasi implisit — kamu mengubah sebuah nilai, Flutter menganimasikan perpindahannya secara otomatis. Kamu tidak menulis satu baris pun kode animasi.

Animasi eksplisit — kamu mengendalikan jalannya animasi sendiri: kapan mulai, kapan berhenti, mundur, berulang, dan seterusnya.

IMPLISIT                            EKSPLISIT
────────────────────────            ────────────────────────
AnimatedContainer                   AnimationController
AnimatedOpacity                     AnimatedBuilder
AnimatedPositioned                  Tween
AnimatedPadding                     CurvedAnimation
AnimatedSwitcher

Ubah nilai → animasi jalan          Kamu yang menyetir

✅ Mudah, cukup untuk 90% kasus      ✅ Kendali penuh
❌ Tidak bisa diulang/dibalik        ❌ Butuh StatefulWidget + dispose
   secara terprogram

Aturan praktisnya: mulai dari implisit. Naik ke eksplisit hanya kalau kamu benar-benar butuh kendali.

Animasi implisit

AnimatedContainer

Container yang menganimasikan setiap perubahan propertinya:

dart
class KotakBerubah extends StatefulWidget {
  const KotakBerubah({super.key});

  @override
  State<KotakBerubah> createState() => _KotakBerubahState();
}

class _KotakBerubahState extends State<KotakBerubah> {
  bool _diperbesar = false;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        onTap: () => setState(() => _diperbesar = !_diperbesar),
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 400),
          curve: Curves.easeInOut,

          // Setiap properti di bawah ini akan dianimasikan
          width: _diperbesar ? 280 : 140,
          height: _diperbesar ? 280 : 140,
          decoration: BoxDecoration(
            color: _diperbesar ? Colors.indigo : Colors.teal,
            borderRadius: BorderRadius.circular(_diperbesar ? 140 : 16),
          ),
          alignment: Alignment.center,
          child: Text(
            _diperbesar ? 'Ketuk untuk kecil' : 'Ketuk untuk besar',
            style: const TextStyle(color: Colors.white),
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );
  }
}

Itu seluruh kodenya. Kamu hanya mengubah nilai lewat setState, dan AnimatedContainer menghitung sendiri seluruh langkah di antaranya.

AnimatedOpacity

Untuk memudarkan masuk dan keluar:

dart
AnimatedOpacity(
  opacity: _terlihat ? 1.0 : 0.0,
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeOut,
  child: const Text('Muncul dan menghilang dengan halus'),
)

Opacity nol tetap memakan ruang

Widget dengan opacity: 0 masih menempati tempatnya di tata letak dan masih bisa diketuk. Kalau kamu ingin ia benar-benar hilang setelah memudar, pakai AnimatedSwitcher atau kombinasikan dengan Visibility.

AnimatedSwitcher

Menganimasikan pergantian antara dua widget yang berbeda:

dart
AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  transitionBuilder: (child, animation) {
    return FadeTransition(
      opacity: animation,
      child: ScaleTransition(scale: animation, child: child),
    );
  },
  child: _memuat
      ? const CircularProgressIndicator(key: ValueKey('memuat'))
      : Text('Selesai', key: const ValueKey('selesai')),
)

Key wajib pada AnimatedSwitcher

AnimatedSwitcher mendeteksi pergantian dengan membandingkan key anaknya. Tanpa key yang berbeda, ia mengira widgetnya sama dan tidak menganimasikan apa pun. Alasan lengkapnya di Bab 12.

Widget implisit lainnya

dart
// Posisi di dalam Stack
AnimatedPositioned(
  duration: const Duration(milliseconds: 300),
  top: _naik ? 20 : 200,
  left: 20,
  child: const Icon(Icons.star, size: 48),
)

// Padding
AnimatedPadding(
  duration: const Duration(milliseconds: 250),
  padding: EdgeInsets.all(_renggang ? 32 : 8),
  child: const Text('Berjarak'),
)

// Perataan
AnimatedAlign(
  duration: const Duration(milliseconds: 400),
  alignment: _kanan ? Alignment.centerRight : Alignment.centerLeft,
  child: const Icon(Icons.circle),
)

// Gaya teks
AnimatedDefaultTextStyle(
  duration: const Duration(milliseconds: 300),
  style: TextStyle(
    fontSize: _besar ? 32 : 16,
    color: _besar ? Colors.indigo : Colors.grey,
    fontWeight: _besar ? FontWeight.bold : FontWeight.normal,
  ),
  child: const Text('Teks yang tumbuh'),
)

TweenAnimationBuilder

Jembatan antara implisit dan eksplisit — menganimasikan nilai apa pun tanpa AnimationController:

dart
TweenAnimationBuilder<double>(
  tween: Tween(begin: 0, end: _nilaiTarget),
  duration: const Duration(milliseconds: 600),
  curve: Curves.easeOutCubic,
  builder: (context, nilai, child) {
    return Column(
      children: [
        LinearProgressIndicator(value: nilai),
        const SizedBox(height: 8),
        Text('${(nilai * 100).toInt()}%'),
      ],
    );
  },
)

Setiap kali _nilaiTarget berubah, animasinya berjalan dari nilai saat ini ke nilai baru. Ini sangat berguna untuk penghitung angka, bilah progres, dan grafik yang tumbuh.

Curves: bentuk gerakan

Curve menentukan bagaimana kecepatan animasi berubah sepanjang durasinya. Memilih kurva yang tepat adalah perbedaan antara animasi yang terasa mahal dan yang terasa kaku.

dart
Curves.linear          // kecepatan tetap — jarang terasa alami
Curves.easeIn          // mulai lambat, makin cepat
Curves.easeOut         // mulai cepat, melambat di akhir
Curves.easeInOut       // lambat–cepat–lambat (paling serbaguna)
Curves.fastOutSlowIn   // kurva standar Material
Curves.elasticOut      // memantul melewati target
Curves.bounceOut       // memantul seperti bola jatuh
linear      ────────────────────  kecepatan tetap
easeIn      ──────────▁▂▃▅▇████  lambat lalu cepat
easeOut     ████▇▅▃▂▁──────────  cepat lalu lambat
easeInOut   ──▂▄▆████▆▄▂──────  lambat–cepat–lambat
elasticOut  ────████▆█▇█▇█████  memantul melewati target

Panduan praktis:

  • Sesuatu masuk ke layareaseOut. Datang cepat, mendarat lembut.
  • Sesuatu keluar dari layareaseIn. Berangkat pelan, pergi cepat.
  • Berpindah antar keadaaneaseInOut atau fastOutSlowIn.
  • Ingin terasa main-mainelasticOut atau bounceOut, tetapi hemat.

Durasi yang umum dipakai:

DurasiUntuk
100–200 msUmpan balik mikro — riak, sorotan
200–300 msTransisi kecil — memudar, menggeser
300–500 msTransisi halaman, perubahan tata letak
> 500 msAnimasi dekoratif; jarang cocok untuk interaksi

Animasi di atas 500 ms untuk interaksi biasa akan terasa lambat, bukan elegan.

Animasi eksplisit

Ketika kamu butuh mengulang, membalik, atau menghentikan animasi di tengah jalan, kamu butuh AnimationController.

Tiga bagian yang bekerja sama

┌─────────────────────────────────────────────────────────────┐
│  AnimationController                                        │
│  Menghasilkan nilai 0.0 → 1.0 selama durasi tertentu        │
│  Punya .forward(), .reverse(), .repeat(), .stop()           │
└──────────────────────────┬──────────────────────────────────┘
                           │ 0.0 ... 1.0

┌─────────────────────────────────────────────────────────────┐
│  Tween  (opsional)                                          │
│  Memetakan 0.0–1.0 ke rentang yang kamu mau                 │
│  Tween(begin: 0, end: 300)  →  0.0 jadi 0, 1.0 jadi 300     │
└──────────────────────────┬──────────────────────────────────┘
                           │ nilai sesungguhnya

┌─────────────────────────────────────────────────────────────┐
│  AnimatedBuilder                                            │
│  Membangun ulang bagian tertentu setiap frame               │
└─────────────────────────────────────────────────────────────┘

Contoh: daftar yang meluncur masuk

dart
// lib/screens/layar_meal_beranimasi.dart
import 'package:flutter/material.dart';

class LayarMealBeranimasi extends StatefulWidget {
  const LayarMealBeranimasi({super.key, required this.daftarMeal});

  final List<Meal> daftarMeal;

  @override
  State<LayarMealBeranimasi> createState() => _LayarMealBeranimasiState();
}

class _LayarMealBeranimasiState extends State<LayarMealBeranimasi>
    with SingleTickerProviderStateMixin {
  late AnimationController _pengendali;
  late Animation<Offset> _pergeseran;

  @override
  void initState() {
    super.initState();

    _pengendali = AnimationController(
      vsync: this,                                    // sinkron dengan layar
      duration: const Duration(milliseconds: 400),
      lowerBound: 0,
      upperBound: 1,
    );

    // Petakan 0.0–1.0 menjadi pergeseran dari bawah ke posisi normal
    _pergeseran = Tween<Offset>(
      begin: const Offset(0, 0.3),   // 30% tinggi widget ke bawah
      end: Offset.zero,
    ).animate(
      CurvedAnimation(
        parent: _pengendali,
        curve: Curves.easeInOut,
      ),
    );

    _pengendali.forward();   // mulai animasinya
  }

  @override
  void dispose() {
    _pengendali.dispose();   // ⚠️ wajib, kalau tidak memori bocor
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _pengendali,
      // `child` dibangun SEKALI dan dipakai ulang setiap frame
      child: ListView.builder(
        itemCount: widget.daftarMeal.length,
        itemBuilder: (ctx, i) => ItemMeal(meal: widget.daftarMeal[i]),
      ),
      builder: (context, child) {
        return SlideTransition(
          position: _pergeseran,
          child: child,
        );
      },
    );
  }
}

Beberapa bagian yang perlu dibedah:

with SingleTickerProviderStateMixin — mixin ini menyediakan vsync, yang menyinkronkan animasi dengan laju penyegaran layar. Tanpa itu, animasi tetap berjalan meskipun layar mati, membuang baterai. Kalau kamu punya lebih dari satu controller, pakai TickerProviderStateMixin (tanpa Single).

_pengendali.dispose() — wajib. Ini salah satu penyebab kebocoran memori paling umum di aplikasi Flutter.

Parameter child pada AnimatedBuilder — ini optimasi yang penting. Widget yang diberikan sebagai child dibangun sekali, lalu diteruskan ke builder setiap frame. Tanpa itu, seluruh ListView dibangun ulang enam puluh kali per detik.

Mengendalikan animasi

dart
_pengendali.forward();     // maju ke 1.0
_pengendali.reverse();     // mundur ke 0.0
_pengendali.repeat();      // berulang terus
_pengendali.repeat(reverse: true);   // maju-mundur bergantian
_pengendali.stop();        // berhenti di tempat
_pengendali.reset();       // kembali ke 0.0 tanpa animasi

// Bereaksi terhadap penyelesaian
_pengendali.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    _pengendali.reverse();
  }
});

Widget transisi bawaan

Alih-alih menulis AnimatedBuilder sendiri, sering ada widget siap pakai:

dart
// Bergeser
SlideTransition(
  position: Tween<Offset>(
    begin: const Offset(-1, 0),
    end: Offset.zero,
  ).animate(_pengendali),
  child: const KartuSaya(),
)

// Memudar
FadeTransition(
  opacity: _pengendali,
  child: const KartuSaya(),
)

// Membesar
ScaleTransition(
  scale: CurvedAnimation(parent: _pengendali, curve: Curves.elasticOut),
  child: const Icon(Icons.favorite, size: 64),
)

// Berputar
RotationTransition(
  turns: _pengendali,
  child: const Icon(Icons.refresh, size: 48),
)

// Ukuran
SizeTransition(
  sizeFactor: _pengendali,
  axis: Axis.vertical,
  child: const PanelDetail(),
)

Contoh: tombol favorit yang berdenyut

Pola yang sangat umum — ikon hati yang membesar sesaat ketika ditekan.

dart
// lib/widgets/tombol_favorit.dart
import 'package:flutter/material.dart';

class TombolFavorit extends StatefulWidget {
  const TombolFavorit({
    super.key,
    required this.favorit,
    required this.saatDitekan,
  });

  final bool favorit;
  final VoidCallback saatDitekan;

  @override
  State<TombolFavorit> createState() => _TombolFavoritState();
}

class _TombolFavoritState extends State<TombolFavorit>
    with SingleTickerProviderStateMixin {
  late final AnimationController _pengendali;
  late final Animation<double> _skala;

  @override
  void initState() {
    super.initState();

    _pengendali = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    // Membesar sampai 1.4 di tengah, lalu kembali ke 1.0
    _skala = TweenSequence<double>([
      TweenSequenceItem(
        tween: Tween(begin: 1.0, end: 1.4)
            .chain(CurveTween(curve: Curves.easeOut)),
        weight: 50,
      ),
      TweenSequenceItem(
        tween: Tween(begin: 1.4, end: 1.0)
            .chain(CurveTween(curve: Curves.easeIn)),
        weight: 50,
      ),
    ]).animate(_pengendali);
  }

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

  void _tangani() {
    // Jalankan animasi dari awal setiap kali ditekan
    _pengendali.forward(from: 0);
    widget.saatDitekan();
  }

  @override
  Widget build(BuildContext context) {
    return IconButton(
      onPressed: _tangani,
      icon: ScaleTransition(
        scale: _skala,
        child: Icon(
          widget.favorit ? Icons.star : Icons.star_border,
          color: widget.favorit ? Colors.amber : null,
        ),
      ),
    );
  }
}

TweenSequence memungkinkan merangkai beberapa tahap animasi dengan bobot masing-masing. Di sini: 50% pertama membesar, 50% berikutnya mengecil kembali.

forward(from: 0) memastikan animasi selalu mulai dari awal — kalau pengguna menekan berkali-kali dengan cepat, tiap tekanan menghasilkan denyut baru.

Transisi antar halaman

Secara bawaan MaterialPageRoute memberi animasi sesuai platform. Untuk transisi kustom, gunakan PageRouteBuilder:

dart
Navigator.of(context).push(
  PageRouteBuilder(
    transitionDuration: const Duration(milliseconds: 400),
    reverseTransitionDuration: const Duration(milliseconds: 300),
    pageBuilder: (ctx, animation, secondaryAnimation) {
      return const LayarDetail();
    },
    transitionsBuilder: (ctx, animation, secondaryAnimation, child) {
      // Meluncur dari kanan sambil memudar masuk
      const mulai = Offset(1.0, 0.0);
      const selesai = Offset.zero;

      final tween = Tween(begin: mulai, end: selesai)
          .chain(CurveTween(curve: Curves.easeInOut));

      return SlideTransition(
        position: animation.drive(tween),
        child: FadeTransition(opacity: animation, child: child),
      );
    },
  ),
);

Untuk transisi yang berlaku di seluruh aplikasi, setel di tema:

dart
MaterialApp(
  theme: ThemeData(
    pageTransitionsTheme: const PageTransitionsTheme(
      builders: {
        TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
        TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
      },
    ),
  ),
)

Hero

Sudah disinggung di Bab 7, tetapi layak diulang karena inilah animasi navigasi dengan rasio hasil terhadap usaha yang tertinggi:

dart
// Di layar daftar
Hero(
  tag: 'gambar-${meal.id}',
  child: ClipRRect(
    borderRadius: BorderRadius.circular(12),
    child: Image.network(meal.urlGambar, height: 120, fit: BoxFit.cover),
  ),
)

// Di layar detail — tag identik
Hero(
  tag: 'gambar-${meal.id}',
  child: Image.network(meal.urlGambar, height: 300, fit: BoxFit.cover),
)

Flutter menghitung sendiri jalur, ukuran, dan durasi transisinya.

Menganimasikan perubahan daftar

Untuk daftar yang itemnya bisa ditambah dan dihapus, AnimatedList memberikan animasi otomatis:

dart
class _DaftarBeranimasiState extends State<DaftarBeranimasi> {
  final _kunciDaftar = GlobalKey<AnimatedListState>();
  final List<String> _item = [];

  void _tambah(String teks) {
    _item.insert(0, teks);
    _kunciDaftar.currentState?.insertItem(
      0,
      duration: const Duration(milliseconds: 300),
    );
  }

  void _hapus(int indeks) {
    final terhapus = _item.removeAt(indeks);

    _kunciDaftar.currentState?.removeItem(
      indeks,
      (context, animation) => SizeTransition(
        sizeFactor: animation,
        child: FadeTransition(
          opacity: animation,
          child: ListTile(title: Text(terhapus)),
        ),
      ),
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedList(
      key: _kunciDaftar,
      initialItemCount: _item.length,
      itemBuilder: (context, indeks, animation) {
        return SlideTransition(
          position: Tween<Offset>(
            begin: const Offset(1, 0),
            end: Offset.zero,
          ).animate(
            CurvedAnimation(parent: animation, curve: Curves.easeOut),
          ),
          child: ListTile(
            title: Text(_item[indeks]),
            trailing: IconButton(
              icon: const Icon(Icons.delete),
              onPressed: () => _hapus(indeks),
            ),
          ),
        );
      },
    );
  }
}

Kuncinya: daftar data dan AnimatedList harus diperbarui bersamaan. Kalau kamu menghapus dari _item tanpa memanggil removeItem, keduanya menjadi tidak sinkron dan aplikasi crash.

Alternatif yang lebih sederhana

Paket flutter_animate memungkinkan menganimasikan widget apa pun dengan sintaks berantai yang sangat ringkas:

dart
Text('Halo')
  .animate()
  .fadeIn(duration: 300.ms)
  .slideY(begin: 0.2, end: 0);

Untuk animasi dekoratif yang tidak butuh kendali rumit, ini menghemat banyak kode boilerplate.

Kesalahan yang sering terjadi

Lupa dispose(). Setiap AnimationController yang tidak dibuang akan terus berjalan dan memakan memori.

Memakai setState untuk animasi. Memanggil setState enam puluh kali per detik untuk menggerakkan sesuatu adalah cara yang boros. Pakai AnimatedBuilder yang hanya membangun ulang bagian yang berubah.

Tidak memakai parameter child. Widget berat di dalam builder dibangun ulang setiap frame. Pindahkan ke parameter child.

Durasi terlalu panjang. Animasi 800 ms terasa lambat, bukan mewah.

Terlalu banyak animasi. Kalau semuanya bergerak, tidak ada yang menonjol. Animasikan hal yang penting saja.

Latihan Mandiri

Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.

Variasi 1: Tombol Tambah ke Keranjang — ⭐⭐ · 45–60 menit

Tantangan: Buat tombol "tambah ke keranjang" yang beranimasi: saat ditekan, ia menyusut sesaat, berubah menjadi lingkaran dengan tanda centang, lalu kembali ke bentuk semula setelah satu detik. Tambahkan juga penghitung isi keranjang yang angkanya berganti dengan transisi, bukan melompat.

Bayangan tampilan (urutan animasi):

text
 Sebelum tekan        Saat ditekan          Setelah (1 detik)
┌────────────────┐    ┌───────┐             ┌───┐
│ + Tambah        │ ──▶│   +   │────────────▶│ ✓ │──▶ kembali ke awal
│  ke Keranjang   │    └───────┘             └───┘
└────────────────┘     (menyusut sesaat)

Keranjang: 2  ──▶  3   (angka berganti dengan transisi, bukan lompat)

Kriteria selesai:

  • Perubahan bentuk memakai animasi implisit — kamu hanya mengubah nilai dan Flutter yang menganimasikannya.
  • Efek menyusut saat ditekan memakai animasi eksplisit dengan AnimationController yang di-dispose().
  • Angka penghitung berganti dengan transisi karena setiap nilai punya key yang berbeda.
  • Seluruh durasi berada di rentang 200–400 milidetik, dan kurvanya dipilih sadar — bukan Curves.linear yang tersisa dari contoh.

Petunjuk: Bentuk, warna, dan ukuran yang berubah sekaligus adalah pekerjaan AnimatedContainer: ubah decoration dan width lewat setState, dan ia menangani sisanya. Untuk efek tekan, kamu butuh kendali dua arah — forward() saat jari turun, reverse() saat jari naik — dan itulah alasan animasi eksplisit ada. Untuk angka yang berganti, AnimatedSwitcher mendeteksi pergantian lewat key; kalau kedua Text punya key yang sama, atau tidak punya sama sekali, ia menganggap tidak ada yang berubah dan tidak menganimasikan apa pun. Soal kurva: easeOut untuk sesuatu yang masuk, easeIn untuk yang keluar.

Variasi 2: Kartu yang Membalik — ⭐⭐ · 30–45 menit

Tantangan: Buat kartu hafalan yang berbalik saat diketuk, menampilkan pertanyaan di satu sisi dan jawaban di sisi lain. Balikannya harus terasa seperti benda tiga dimensi — sisi belakang tidak boleh terlihat menembus dari depan di tengah putaran.

Bayangan tampilan (urutan animasi):

text
 Depan (pertanyaan)    Tengah putaran        Belakang (jawaban)
┌──────────────┐       ┌───┐                 ┌──────────────┐
│ Apa ibukota   │  ──▶ │▐▐▐│  ──▶            │  Jakarta      │
│ Indonesia?    │       └───┘                 │               │
└──────────────┘    (menyempit jadi garis;    └──────────────┘
                      konten berganti PERSIS   (tidak tampil terbalik
                      di titik tengah ini)      cermin)

Kriteria selesai:

  • Putaran memakai AnimationController yang bisa dibalik arah, dan di-dispose().
  • Konten berganti tepat di titik tengah putaran, bukan di awal atau akhir.
  • Sisi belakang tidak tampil terbalik cermin.
  • Mengetuk saat animasi sedang berjalan tidak membuat kartu tersendat atau terjebak di posisi setengah.

Petunjuk: Putaran tiga dimensi memakai Transform dengan Matrix4 yang diberi sedikit perspektif; tanpa perspektif, hasilnya terlihat seperti kartu yang menyempit, bukan berputar. Konten yang berganti di titik tengah berarti kamu memeriksa apakah nilai animasi sudah melewati setengah — dan sisi belakang perlu diputar setengah lingkaran lagi supaya tidak tampil sebagai cermin.

Variasi 3: Menu Melingkar — ⭐⭐⭐ · 45–60 menit

Tantangan: Buat tombol aksi mengambang yang, saat ditekan, mengembang menjadi empat tombol kecil yang tersebar melingkar dengan jeda berurutan (tombol kedua muncul sedikit setelah yang pertama). Menekannya lagi menutup mereka dalam urutan terbalik.

Bayangan tampilan (urutan animasi):

text
  Tertutup          Membuka (berurutan)        Terbuka penuh
                                                    ●(4)
     ⊕      ──▶     ⊕ ↗●(1)         ──▶       ●(3) ⊕ ●(1)
                       ↗●(2, muncul                 ●(2)
                          sedikit setelah 1)

Menekan lagi → keempat tombol menutup dalam urutan terbalik (4, 3, 2, 1), bukan menghilang serentak.

Kriteria selesai:

  • Keempat tombol memakai satu AnimationController dengan Interval yang berbeda, bukan empat controller terpisah.
  • Ikon tombol utama ikut berputar dan berubah saat menu terbuka.
  • Menutup menu menjalankan animasi mundur, bukan menghilang seketika.
  • Menekan berkali-kali dengan cepat tidak meninggalkan tombol di posisi aneh.

Petunjuk: Interval sebagai kurva membiarkan satu controller mengendalikan banyak animasi yang bergeser waktunya — anak pertama memakai rentang 0,0 sampai 0,6, anak kedua 0,1 sampai 0,7, dan seterusnya. Ini jauh lebih mudah dikelola daripada empat controller yang harus kamu sinkronkan, dan ia otomatis benar saat dijalankan mundur.

Variasi 4: Indikator Unduhan — ⭐⭐⭐ · 45–60 menit

Tantangan: Buat indikator unduhan melingkar yang mengisi dari nol sampai seratus persen, berubah warna sesuai kemajuan, dan berubah menjadi tanda centang dengan animasi ketika selesai. Persentasenya ditampilkan di tengah dan ikut berubah dengan halus.

Bayangan tampilan (urutan animasi):

text
    0%                45%                100%
   ╭───╮             ╭───╮              ╭───╮
  │ 0%  │    ──▶    │ 45% │    ──▶     │  ✓  │
   ╰───╯             ╰──●──╯            ╰───╯
  (merah)          (kuning, warna       (hijau, jadi
                    berinterpolasi)      tanda centang)

Kriteria selesai:

  • Lingkaran progres digambar sendiri, bukan memakai indikator bawaan.
  • Widget berat di dalamnya tidak dibangun ulang setiap frame — kamu memakai parameter child pada AnimatedBuilder.
  • Perubahan warna sepanjang progres memakai interpolasi, bukan lompatan antar tiga warna.
  • Transisi ke tanda centang terasa menyatu, bukan seperti dua animasi terpisah.

Petunjuk: CustomPainter adalah alat untuk menggambar busur, dan AnimatedBuilder menghubungkannya ke controller. Parameter child pada AnimatedBuilder sering diabaikan padahal ia gratis: apa pun yang kamu taruh di sana dibangun sekali dan dioper ke builder, sehingga teks persentase yang mahal tidak ikut dibangun ulang enam puluh kali per detik. Untuk warna, Color.lerp memberi peralihan yang mulus.

Variasi 5: Transisi Halaman Kustom — ⭐⭐⭐⭐ · 60–90 menit

Tantangan: Ganti transisi bawaan pada tiga perpindahan layar dengan transisi yang kamu rancang sendiri: satu yang menggeser dan memudar, satu yang membesar dari titik sentuh, dan satu yang menggabungkan Hero dengan transisi latar yang menyatu.

Bayangan tampilan (tiga jenis transisi):

text
1) Geser + memudar:    [Layar A] ──slide+fade──▶ [Layar B]

2) Membesar dari titik sentuh:
      [tombol]•              ┌──────────────┐
          ╲                  │  Layar Baru   │
           ╲──────────────▶  │  (mengembang  │
                              │  dari titik   │
                              │  sentuh itu)  │
                              └──────────────┘

3) Hero:  [Thumbnail kecil] ══════▶ [Gambar besar di layar detail]
          (posisi & ukuran menyatu mulus, tanpa kedipan di akhir)

Kriteria selesai:

  • Ketiganya memakai PageRouteBuilder dengan durasi masuk dan keluar yang dipilih sadar.
  • Transisi yang membesar dari titik sentuh benar-benar berasal dari posisi jari, bukan dari tengah layar.
  • Transisi Hero bekerja dua arah tanpa kedipan di akhir.
  • Menekan kembali di tengah transisi tidak meninggalkan layar setengah tampil.

Petunjuk: Untuk membesar dari titik sentuh, kamu butuh posisi global widget yang ditekan — RenderBox dari context-nya bisa memberikan itu lewat localToGlobal. Kedipan di akhir transisi Hero hampir selalu berarti widget di kedua layar tidak benar-benar identik bentuknya; bungkus keduanya dengan Material bertipe transparan kalau yang berkedip adalah latar teksnya.

Variasi 6: Daftar yang Hidup — ⭐⭐⭐⭐⭐ · 90–120 menit

Tantangan: Bangun daftar di mana item baru menyelinap masuk dari samping, item yang dihapus mengecil sambil memudar, dan item bisa disusun ulang dengan menyeret — semuanya beranimasi dengan mulus dan tidak pernah kehilangan sinkronisasi dengan datanya.

Bayangan tampilan (urutan animasi):

text
   Sebelum          Item baru masuk         Item dihapus
┌──────────┐       ┌──────────┐           ┌──────────┐
│ Item A   │       │ Item A   │           │ Item A   │
│ Item B   │       │ Item B   │           │ (mengecil │
│ Item C   │       │◀Item Baru│ (masuk    │  + memudar│
└──────────┘       │ Item C   │  dari kanan)│  di sini)│
                    └──────────┘           │ Item C   │
                                            └──────────┘

Menyeret "Item C" ke atas "Item A" → urutan data ikut berubah dan tetap tersimpan setelah layar ditutup lalu dibuka kembali.

Kriteria selesai:

  • Menambah dan menghapus beberapa item dengan cepat berturut-turut tidak pernah membuat aplikasi berhenti.
  • Menyeret item ke posisi baru memperbarui data yang mendasarinya, dan urutannya bertahan setelah layar dibuka ulang.
  • Animasi hapus menampilkan item yang benar, bukan item penggantinya.
  • Daftar tetap mulus ketika berisi dua ratus item.

Petunjuk: AnimatedList adalah bagian paling rawan crash di seluruh bab ini, dan penyebabnya selalu sama: daftar data dan daftar yang diketahui AnimatedList menjadi tidak sinkron. Setiap penambahan dan penghapusan harus memberi tahu keduanya dalam urutan yang benar, dan animasi hapus butuh salinan item yang sudah dibuang untuk digambar selama animasinya berjalan. Untuk menyeret, ReorderableListView sudah menangani sebagian besar pekerjaannya — perhatikan bahwa indeks tujuan yang ia berikan perlu disesuaikan ketika item dipindah ke bawah.

Ikhtisar

  • Mulai dari animasi implisit. AnimatedContainer, AnimatedOpacity, dan saudara-saudaranya cukup untuk sebagian besar kebutuhan — kamu hanya mengubah nilai lewat setState.
  • Naik ke eksplisit hanya ketika butuh mengulang, membalik, atau menghentikan animasi secara terprogram.
  • Animasi eksplisit butuh AnimationController + vsync dari SingleTickerProviderStateMixin, dan wajib di-dispose().
  • Tween memetakan rentang 0.0–1.0 ke nilai yang kamu inginkan; CurvedAnimation memberi bentuk gerakannya.
  • Pakai parameter child pada AnimatedBuilder agar widget berat tidak dibangun ulang setiap frame.
  • Kurva: easeOut untuk masuk, easeIn untuk keluar, easeInOut untuk berpindah keadaan.
  • Durasi 200–400 ms cocok untuk hampir semua interaksi. Di atas 500 ms terasa lambat.
  • AnimatedSwitcher butuh key yang berbeda untuk mendeteksi pergantian.
  • Hero memberi transisi antar halaman yang mengesankan dengan usaha paling sedikit — cukup tag yang identik.
  • AnimatedList butuh daftar data dan widget-nya diperbarui bersamaan, atau aplikasi akan crash.

Berikutnya: Bab 12 — Tiga Pohon & Keys, tempat kita membuka kap mesin dan melihat cara Flutter bekerja sebenarnya.

Transkrip asli

Disintesis dari 1_flutter_core/9_meals-app_adding-annimation.md (9 video) dan 1_flutter_zero-to-hero/12_next-steps.md (bagian Animations). Lihat PDF Flutter Core.

Rangkuman pembelajaran pribadi, disusun ulang dari beberapa kursus Flutter.