Skip to content

Bab 7 — Navigasi & Multi-Screen

Aplikasi sungguhan punya banyak layar. Pengguna mengetuk kategori, masuk ke daftar, memilih item, membuka detail, lalu kembali. Bab ini membahas bagaimana Flutter mengelola perpindahan itu — dan bagaimana data mengalir bolak-balik di antaranya.

Model mental yang perlu kamu pegang: Flutter menyimpan layar sebagai tumpukan (stack). Layar terakhir yang didorong berada di atas dan itulah yang terlihat.

     push(Detail)              pop()
          │                      │
          ▼                      ▼
   ┌─────────────┐        ┌─────────────┐
   │   Detail    │ ◄─ terlihat          │
   ├─────────────┤        ├─────────────┤
   │   Daftar    │        │   Daftar    │ ◄─ terlihat
   ├─────────────┤        ├─────────────┤
   │   Beranda   │        │   Beranda   │
   └─────────────┘        └─────────────┘

Dua operasi dasarnya:

  • push — menaruh layar baru di atas tumpukan.
  • pop — mengangkat layar teratas, memperlihatkan yang di bawahnya.

Tombol kembali di perangkat dan panah kembali di AppBar keduanya memanggil pop di belakang layar. Itulah sebabnya keduanya bekerja otomatis tanpa kamu menulis apa pun.

Push: berpindah ke layar baru

dart
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (ctx) => const LayarDetail(),
  ),
);

MaterialPageRoute membungkus layarmu dan menyediakan animasi transisi yang sesuai platform — geser dari kanan di iOS, muncul dari bawah di Android.

Bentuk yang lebih ringkas dan lebih sering dipakai:

dart
Navigator.of(context).push(
  MaterialPageRoute(builder: (ctx) => const LayarDetail()),
);

Keduanya identik. Navigator.of(context) mencari Navigator terdekat di atas widget ini pada widget tree.

Pop: kembali

dart
Navigator.pop(context);

Kamu jarang perlu menuliskannya untuk tombol kembali biasa — Flutter sudah menanganinya. Kamu memakainya ketika ingin menutup layar dari tombol khusus, misalnya "Batal" atau setelah menyimpan data.

Jangan pop layar terakhir

Memanggil pop ketika hanya ada satu layar di tumpukan akan menutup aplikasi atau meninggalkan layar hitam. Kalau kamu tidak yakin, periksa dulu:

dart
if (Navigator.canPop(context)) {
  Navigator.pop(context);
}

Mengirim data ke layar tujuan

Ini bagian yang sering membuat pemula bingung, padahal jawabannya sangat sederhana: layar adalah widget biasa, jadi kirim datanya lewat konstruktor.

dart
// lib/screens/layar_meal.dart
import 'package:flutter/material.dart';
import '../models/meal.dart';

class LayarMeal extends StatelessWidget {
  const LayarMeal({
    super.key,
    required this.judul,
    required this.daftarMeal,
  });

  final String judul;
  final List<Meal> daftarMeal;

  void _pilihMeal(BuildContext context, Meal meal) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (ctx) => LayarDetailMeal(meal: meal),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    Widget isi = ListView.builder(
      itemCount: daftarMeal.length,
      itemBuilder: (ctx, indeks) => ItemMeal(
        meal: daftarMeal[indeks],
        saatDipilih: (meal) => _pilihMeal(context, meal),
      ),
    );

    if (daftarMeal.isEmpty) {
      isi = Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              'Belum ada apa-apa di sini...',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 12),
            Text(
              'Coba pilih kategori lain.',
              style: Theme.of(context).textTheme.bodyLarge,
            ),
          ],
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(title: Text(judul)),
      body: isi,
    );
  }
}

Dan pemanggilnya:

dart
void _pilihKategori(BuildContext context, Kategori kategori) {
  final mealTersaring = daftarMealTersedia
      .where((meal) => meal.kategori.contains(kategori.id))
      .toList();

  Navigator.of(context).push(
    MaterialPageRoute(
      builder: (ctx) => LayarMeal(
        judul: kategori.judul,
        daftarMeal: mealTersaring,   // ← data dikirim lewat konstruktor
      ),
    ),
  );
}

Tidak ada sihir. Layar tujuan hanyalah widget yang menerima properti seperti widget lain.

Menerima data saat kembali

Arah sebaliknya lebih menarik. Ketika pengguna menutup layar, kamu sering butuh tahu apa yang mereka lakukan di sana — item apa yang mereka tambahkan, filter apa yang mereka ubah.

Kuncinya: push mengembalikan sebuah Future yang selesai ketika layar itu di-pop. Dan pop bisa membawa nilai.

dart
// Di layar yang membuka
void _tambahItem() async {
  final itemBaru = await Navigator.of(context).push<Item>(
    MaterialPageRoute(builder: (ctx) => const LayarTambahItem()),
  );

  // Bernilai null kalau pengguna menekan tombol kembali tanpa menyimpan
  if (itemBaru == null) return;

  setState(() {
    _daftarItem.add(itemBaru);
  });
}
dart
// Di layar tujuan, saat menyimpan
void _simpan() {
  if (!_kunciForm.currentState!.validate()) return;
  _kunciForm.currentState!.save();

  Navigator.of(context).pop(
    Item(
      nama: _namaMasukan,
      jumlah: _jumlahMasukan,
      kategori: _kategoriTerpilih,
    ),
  );   // ← nilai ini menjadi hasil dari Future di atas
}

Perhatikan push<Item> dengan parameter tipe. Ini memberi tahu Dart bahwa hasil yang diharapkan bertipe Item? — sehingga kamu mendapat pemeriksaan tipe alih-alih dynamic.

   Layar Daftar                    Layar Tambah
        │                                │
        │  push<Item>()                  │
        ├───────────────────────────────►│
        │                                │
        │        (menunggu await)        │  pengguna mengisi form
        │                                │
        │              pop(item)         │
        │◄───────────────────────────────┤
        │                                │
   itemBaru terisi
   setState → daftar diperbarui

Pola yang sama dipakai untuk halaman filter:

dart
void _bukaFilter() async {
  final hasilFilter = await Navigator.of(context).push<Map<Filter, bool>>(
    MaterialPageRoute(
      builder: (ctx) => LayarFilter(filterSaatIni: _filterTerpilih),
    ),
  );

  setState(() {
    _filterTerpilih = hasilFilter ?? filterBawaan;
  });
}

Perhatikan ?? filterBawaan. Kalau pengguna menekan tombol kembali alih-alih menyimpan, hasilnya null dan kita jatuh ke nilai bawaan.

pushReplacement: mengganti, bukan menumpuk

Kadang kamu tidak ingin pengguna bisa kembali. Contoh klasik: setelah berhasil masuk, layar login tidak boleh muncul lagi ketika tombol kembali ditekan.

dart
Navigator.of(context).pushReplacement(
  MaterialPageRoute(builder: (ctx) => const LayarBeranda()),
);
   push                         pushReplacement
   ┌──────────┐                 ┌──────────┐
   │  Beranda │ ← baru          │  Beranda │ ← baru
   ├──────────┤                 └──────────┘
   │  Login   │ ← masih ada        Login dibuang
   └──────────┘

Untuk membersihkan seluruh tumpukan dan mulai dari awal:

dart
Navigator.of(context).pushAndRemoveUntil(
  MaterialPageRoute(builder: (ctx) => const LayarBeranda()),
  (route) => false,   // false untuk semua rute = hapus semuanya
);

Ini yang biasanya dipakai setelah keluar dari akun.

Untuk beberapa bagian utama aplikasi yang setara, tab lebih cocok daripada tumpukan. Pola paling umum memakai BottomNavigationBar dengan state indeks.

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

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

  @override
  State<LayarTab> createState() => _LayarTabState();
}

class _LayarTabState extends State<LayarTab> {
  int _indeksHalaman = 0;

  void _pilihHalaman(int indeks) {
    setState(() {
      _indeksHalaman = indeks;
    });
  }

  @override
  Widget build(BuildContext context) {
    // Tentukan isi dan judul berdasarkan tab aktif
    Widget halamanAktif = const LayarKategori();
    var judulHalaman = 'Kategori';

    if (_indeksHalaman == 1) {
      halamanAktif = const LayarFavorit();
      judulHalaman = 'Favorit Saya';
    } else if (_indeksHalaman == 2) {
      halamanAktif = const LayarProfil();
      judulHalaman = 'Profil';
    }

    return Scaffold(
      appBar: AppBar(title: Text(judulHalaman)),
      body: halamanAktif,
      bottomNavigationBar: BottomNavigationBar(
        onTap: _pilihHalaman,
        currentIndex: _indeksHalaman,
        type: BottomNavigationBarType.fixed,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.set_meal),
            label: 'Kategori',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.star),
            label: 'Favorit',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profil',
          ),
        ],
      ),
    );
  }
}

Tab lebih dari tiga

Secara bawaan, BottomNavigationBar dengan empat item atau lebih beralih ke mode shifting — hanya label tab aktif yang terlihat, dan latarnya berubah warna. Kalau kamu ingin semua label selalu tampak, setel type: BottomNavigationBarType.fixed seperti pada contoh di atas.

Alternatif Material 3 yang lebih modern:

dart
NavigationBar(
  selectedIndex: _indeksHalaman,
  onDestinationSelected: _pilihHalaman,
  destinations: const [
    NavigationDestination(
      icon: Icon(Icons.set_meal_outlined),
      selectedIcon: Icon(Icons.set_meal),
      label: 'Kategori',
    ),
    NavigationDestination(
      icon: Icon(Icons.star_outline),
      selectedIcon: Icon(Icons.star),
      label: 'Favorit',
    ),
  ],
)

Tab di bagian atas

Untuk tab di dalam satu halaman — misalnya "Semua", "Terbaru", "Populer" — gunakan TabBar bersama DefaultTabController:

dart
DefaultTabController(
  length: 3,
  child: Scaffold(
    appBar: AppBar(
      title: const Text('Jajak Pendapat'),
      bottom: const TabBar(
        tabs: [
          Tab(text: 'Semua'),
          Tab(text: 'Milik Saya'),
          Tab(text: 'Terpilih'),
        ],
      ),
    ),
    body: const TabBarView(
      children: [
        TabSemua(),
        TabMilikSaya(),
        TabTerpilih(),
      ],
    ),
  ),
)

Jumlah tabs dan jumlah children di TabBarView harus sama persis dengan nilai length. Kalau tidak, aplikasi crash.

Drawer: menu samping

dart
Scaffold(
  appBar: AppBar(title: const Text('Beranda')),
  drawer: MenuSamping(
    saatPilihHalaman: _pilihHalamanDariDrawer,
  ),
  body: halamanAktif,
)

Kehadiran properti drawer otomatis menambahkan tombol hamburger di AppBar.

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

class MenuSamping extends StatelessWidget {
  const MenuSamping({super.key, required this.saatPilihHalaman});

  final void Function(String identifier) saatPilihHalaman;

  @override
  Widget build(BuildContext context) {
    final skema = Theme.of(context).colorScheme;

    return Drawer(
      child: Column(
        children: [
          // Kepala drawer dengan gradasi
          DrawerHeader(
            padding: const EdgeInsets.all(20),
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [
                  skema.primaryContainer,
                  skema.primaryContainer.withValues(alpha: 0.7),
                ],
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
              ),
            ),
            child: Row(
              children: [
                Icon(Icons.fastfood, size: 44, color: skema.primary),
                const SizedBox(width: 18),
                Text(
                  'Menu Cepat',
                  style: Theme.of(context).textTheme.titleLarge!.copyWith(
                        color: skema.primary,
                        fontWeight: FontWeight.bold,
                      ),
                ),
              ],
            ),
          ),

          ListTile(
            leading: Icon(Icons.restaurant, size: 26, color: skema.onSurface),
            title: Text(
              'Menu Makanan',
              style: Theme.of(context).textTheme.titleSmall!.copyWith(
                    color: skema.onSurface,
                    fontSize: 20,
                  ),
            ),
            onTap: () => saatPilihHalaman('meals'),
          ),

          ListTile(
            leading: Icon(Icons.settings, size: 26, color: skema.onSurface),
            title: Text(
              'Filter',
              style: Theme.of(context).textTheme.titleSmall!.copyWith(
                    color: skema.onSurface,
                    fontSize: 20,
                  ),
            ),
            onTap: () => saatPilihHalaman('filters'),
          ),
        ],
      ),
    );
  }
}

Ini penting dan sering terlewat. Ketika pengguna mengetuk item drawer, drawer tidak tertutup sendiri — kamu harus menutupnya.

dart
void _pilihHalamanDariDrawer(String identifier) {
  // Tutup drawer terlebih dahulu
  Navigator.of(context).pop();

  if (identifier == 'filters') {
    Navigator.of(context).push(
      MaterialPageRoute(builder: (ctx) => const LayarFilter()),
    );
  }
}

Kenapa Navigator.pop menutup drawer? Karena drawer sendiri didorong ke Navigator sebagai rute. Menutupnya sama dengan mem-pop rute teratas.

Perhatikan urutannya: tutup drawer dulu, baru dorong layar baru. Kalau terbalik, drawer akan menutup layar yang baru saja kamu buka.

PopScope: mencegat tombol kembali

Kadang kamu perlu melakukan sesuatu ketika pengguna menekan tombol kembali — misalnya meminta konfirmasi kalau ada perubahan yang belum disimpan.

Dulu ini memakai WillPopScope, yang kini sudah usang. Penggantinya adalah PopScope:

dart
@override
Widget build(BuildContext context) {
  return PopScope(
    canPop: false,   // cegah pop otomatis
    onPopInvokedWithResult: (sudahPop, hasil) async {
      if (sudahPop) return;

      final konfirmasi = await showDialog<bool>(
        context: context,
        builder: (ctx) => AlertDialog(
          title: const Text('Buang perubahan?'),
          content: const Text('Perubahanmu belum disimpan.'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(ctx, false),
              child: const Text('Lanjut mengedit'),
            ),
            TextButton(
              onPressed: () => Navigator.pop(ctx, true),
              child: const Text('Buang'),
            ),
          ],
        ),
      );

      if (konfirmasi == true && context.mounted) {
        Navigator.of(context).pop();
      }
    },
    child: Scaffold(
      appBar: AppBar(title: const Text('Edit Item')),
      body: /* ... */,
    ),
  );
}

Perbedaan penting dari WillPopScope: PopScope tidak "membatalkan" pop berdasarkan nilai kembalian. Kamu menyetel canPop: false untuk mencegahnya, lalu memanggil pop secara manual ketika sudah yakin.

PopScope juga bisa dipakai untuk mengirim data ketika pengguna menekan tombol kembali sistem — bukan hanya tombol simpan:

dart
PopScope(
  canPop: false,
  onPopInvokedWithResult: (sudahPop, hasil) {
    if (sudahPop) return;
    Navigator.of(context).pop(_filterTerpilih);   // kirim state saat ini
  },
  child: /* ... */,
)

Named routes: pola alternatif

Untuk aplikasi dengan banyak layar, mendaftarkan rute dengan nama bisa lebih rapi daripada menulis MaterialPageRoute di mana-mana.

dart
MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (ctx) => const LayarBeranda(),
    '/kategori': (ctx) => const LayarKategori(),
    '/detail': (ctx) => const LayarDetail(),
    '/pengaturan': (ctx) => const LayarPengaturan(),
  },
)

Berpindah:

dart
Navigator.of(context).pushNamed('/kategori');
Navigator.of(context).pushReplacementNamed('/beranda');

Mengirim argumen:

dart
Navigator.of(context).pushNamed('/detail', arguments: meal);

Dan membacanya di layar tujuan:

dart
@override
Widget build(BuildContext context) {
  final meal = ModalRoute.of(context)!.settings.arguments as Meal;
  return Scaffold(
    appBar: AppBar(title: Text(meal.judul)),
    body: /* ... */,
  );
}

Kelemahan named routes

Perhatikan as Meal pada kode di atas. Argumen bertipe Object?, jadi kamu harus melakukan cast manual — dan kalau tipenya salah, error baru muncul saat runtime, bukan saat kompilasi.

Karena itu, untuk aplikasi kecil sampai menengah, MaterialPageRoute dengan konstruktor biasa lebih disarankan. Ia memberi keamanan tipe penuh. Named routes berguna ketika kamu butuh deep linking atau punya puluhan layar.

Untuk kebutuhan navigasi yang benar-benar kompleks — deep link, guard, nested navigation — pertimbangkan paket go_router yang kini menjadi rekomendasi resmi tim Flutter.

Hero: transisi antar layar

Hero membuat sebuah elemen tampak "terbang" dari satu layar ke layar berikutnya. Efeknya mahal secara visual tetapi sangat murah untuk diterapkan.

Di layar asal:

dart
Hero(
  tag: 'meal-${meal.id}',   // tag harus unik dan sama di kedua layar
  child: ClipRRect(
    borderRadius: BorderRadius.circular(12),
    child: Image.network(
      meal.urlGambar,
      height: 120,
      width: double.infinity,
      fit: BoxFit.cover,
    ),
  ),
)

Di layar tujuan:

dart
Hero(
  tag: 'meal-${meal.id}',   // tag yang sama persis
  child: Image.network(
    meal.urlGambar,
    height: 300,
    width: double.infinity,
    fit: BoxFit.cover,
  ),
)

Flutter mengurus animasinya. Yang perlu kamu jamin: tag harus unik dalam satu layar dan identik di kedua layar. Memakai ID data adalah cara paling aman; memakai tag yang sama untuk dua item berbeda akan menyebabkan error.

Struktur aplikasi multi-screen

Menggabungkan semuanya, berikut struktur khas aplikasi dengan tab, drawer, dan navigasi bertumpuk.

lib/
├── main.dart
├── models/
│   ├── meal.dart
│   └── kategori.dart
├── data/
│   └── data_contoh.dart
├── screens/
│   ├── layar_tab.dart          ← pembungkus tab + drawer
│   ├── layar_kategori.dart     ← tab 1
│   ├── layar_meal.dart         ← didorong dari kategori
│   ├── layar_detail_meal.dart  ← didorong dari daftar meal
│   ├── layar_favorit.dart      ← tab 2
│   └── layar_filter.dart       ← didorong dari drawer
└── widgets/
    ├── item_kategori.dart
    ├── item_meal.dart
    └── menu_samping.dart

Dan alur navigasinya:

Garis putus-putus menunjukkan arah kembali, dan perhatikan bahwa LayarFilter mengembalikan data ke LayarTab — pola yang kita bahas di awal bab.

Latihan Mandiri

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

Variasi 1: Tiga Layar Catatan — ⭐⭐ · 45–60 menit

Tantangan: Bangun alur tiga layar: daftar catatan → detail catatan → layar edit. Mengubah catatan di layar edit harus terlihat di layar daftar setelah kamu kembali dua kali, dan menutup layar edit dengan perubahan yang belum disimpan harus meminta konfirmasi dulu.

Bayangan alur layar:

text
┌──────────┐ tap catatan  ┌──────────┐  tap Edit   ┌──────────┐
│ Daftar   │─────────────▶│ Detail   │────────────▶│ Edit     │
│ Catatan  │              │ Catatan  │              │ Catatan  │
│ • Catatan A             │ "Judul A"               │[________]│
│ • Catatan B             │ "Isi..."                │[Simpan & │
│ • Catatan C             │ [Edit]                  │  Tutup]  │
└──────────┘◀─────────── pop(hasil) ────────────────┘
     ↑ kembali 2× dari Edit → isi catatan sudah terlihat berubah di sini

Kriteria selesai:

  • Data dikirim ke layar tujuan lewat konstruktor, bukan variabel global.
  • Layar edit mengembalikan hasilnya lewat pop(nilai), dan pemanggilnya memakai push<T>() dengan tipe yang jelas serta memeriksa null.
  • Tombol kembali perangkat pun ikut memicu konfirmasi — bukan hanya tombol batal di layar.
  • Ada satu tombol "Simpan dan tutup" yang menutup dua layar sekaligus, langsung kembali ke daftar.

Petunjuk: Hasil push adalah Future, jadi kamu menunggunya dengan await — dan setelah await itu, periksa mounted sebelum menyentuh context atau setState. Untuk mencegat tombol kembali perangkat, PopScope menggantikan WillPopScope yang sudah usang: setel canPop: false, lalu lakukan pop sendiri setelah pengguna mengonfirmasi. Menutup dua layar sekaligus bisa dilakukan dengan memanggil pop dua kali berturut-turut, tetapi ada cara yang lebih jujur menyatakan maksudnya — cari metode Navigator yang menerima kondisi berhenti.

Variasi 2: Onboarding dan Masuk — ⭐⭐ · 30–45 menit

Tantangan: Bangun alur pembuka aplikasi: tiga halaman perkenalan yang bisa digeser, lalu layar masuk, lalu beranda. Setelah pengguna masuk, tombol kembali perangkat tidak boleh membawanya kembali ke layar masuk atau perkenalan.

Bayangan alur layar:

text
Perkenalan (●○○→○●○→○○●) ──pushReplacement──▶ Masuk ──pushAndRemoveUntil──▶ Beranda

                                                              tombol kembali di sini
                                                              = KELUAR APLIKASI,
                                                              bukan balik ke Masuk

Kriteria selesai:

  • Perpindahan dari perkenalan ke layar masuk memakai pushReplacement.
  • Perpindahan dari masuk ke beranda membersihkan seluruh tumpukan.
  • Menekan tombol kembali di beranda keluar dari aplikasi, bukan kembali ke layar sebelumnya.
  • Halaman perkenalan punya indikator titik yang menunjukkan posisi saat ini.

Petunjuk: Perbedaan antara push, pushReplacement, dan pushAndRemoveUntil adalah perbedaan tentang apa yang tersisa di tumpukan. Cara termudah memeriksanya adalah membayangkan tumpukan kartu: push menaruh di atas, pushReplacement menukar kartu teratas, dan pushAndRemoveUntil membuang kartu sampai kondisi tertentu terpenuhi. Untuk halaman geser, PageView dengan PageController memberimu posisi saat ini untuk menggambar indikatornya.

Variasi 3: Aplikasi Berita — ⭐⭐⭐ · 45–60 menit

Tantangan: Bangun kerangka aplikasi berita: bottom navigation dengan lima tab, drawer berisi menu kategori, dan layar detail artikel yang bisa dibuka dari tab mana pun. Drawer harus menutup dengan benar sebelum layar baru muncul.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ ☰   Berita Utama                   │
│                                     │
│  [ ...daftar artikel... ]          │
│                                     │
│ ─────────────────────────────────  │
│  🏠     🔥     ⭐     🔔     👤    │
│ Home  Trend   Fav   Notif  Profil  │
╰───────────────────────────────────╯

...tap → drawer terbuka → pilih kategori → drawer tutup dulu, baru layar detail muncul (bukan menumpuk di belakangnya):

text
┌─ Drawer ──────┐         ┌──────────────┐
│ Politik       │  pilih  │ Detail       │
│ Olahraga  ────┼────────▶│ Artikel      │
│ Teknologi     │         │              │
└───────────────┘         └──────────────┘

Kriteria selesai:

  • Lima tab semuanya menampilkan labelnya — tidak ada yang hilang atau hanya muncul saat aktif.
  • Memilih kategori dari drawer menutup drawer lebih dulu, baru mendorong layar baru.
  • Berpindah tab tidak mengatur ulang posisi gulir tab yang ditinggalkan.
  • Layar detail artikel punya tombol kembali yang berfungsi dari tab mana pun.

Petunjuk: Bottom navigation dengan empat item atau lebih menyembunyikan label secara bawaan kecuali kamu menyetel tipenya menjadi tetap — perilaku yang mengejutkan banyak orang. Drawer tidak menutup sendiri ketika kamu mendorong layar baru dari dalamnya, sehingga ia akan terlihat menempel di belakang layar baru; panggil pop untuk drawer-nya dulu. Untuk posisi gulir yang bertahan, lihat cara menjaga widget tab tetap hidup meski sedang tidak ditampilkan.

Variasi 4: Checkout Bertahap — ⭐⭐⭐ · 45–60 menit

Tantangan: Bangun alur checkout empat layar: keranjang → alamat → pembayaran → konfirmasi. Pengguna bisa mundur untuk mengubah pilihan, dan data dari layar sebelumnya ikut terbawa. Dari layar konfirmasi, tombol "Selesai" kembali langsung ke beranda tanpa melewati layar mana pun.

Bayangan alur layar:

text
Keranjang ──▶ Alamat ──▶ Pembayaran ──▶ Konfirmasi
    │            │            │              │
    └── data terkumpul & terbawa tiap langkah (bukan direset) ──┘

                                              [Selesai] ──▶ langsung Beranda
                                              (lompat, bukan pop 4×)

Kriteria selesai:

  • Data terkumpul bertahap dan tiap layar menerima apa yang sudah terkumpul sejauh ini lewat konstruktor.
  • Mundur dari layar pembayaran ke alamat mempertahankan alamat yang sudah diisi.
  • Keluar dari alur di tengah jalan meminta konfirmasi.
  • "Selesai" membersihkan seluruh tumpukan alur checkout sekaligus.

Petunjuk: Mengoper data yang terus bertambah lewat konstruktor akan terasa makin berat di layar keempat, dan rasa berat itu memang bagian dari pelajaran — inilah gejala yang diselesaikan state management di Bagian III. Untuk sekarang, satu objek DataCheckout yang di-copyWith di tiap langkah jauh lebih rapi daripada mengoper enam parameter terpisah.

Variasi 5: Rute Bernama dengan Argumen — ⭐⭐⭐⭐ · 60–90 menit

Tantangan: Bangun ulang aplikasi kecil berisi lima layar seluruhnya dengan named routes, termasuk layar yang menerima argumen. Lalu tulis satu paragraf untuk dirimu sendiri: apa yang hilang dibandingkan MaterialPageRoute, dan kapan kehilangan itu sepadan.

Bayangan alur layar:

text
Navigator.pushNamed('/detail', arguments: Produk(...))

        ▼                              rute salah/typo, mis. '/detial'
┌─────────────────┐                    │
│ Detail Produk    │      ┌────────────▼────────────┐
│ (argumen valid)  │      │ 404: Halaman tidak       │
└─────────────────┘      │ ditemukan (bukan crash)  │
                          └─────────────────────────┘

Kriteria selesai:

  • Seluruh rute terdaftar di satu tempat, dan navigasi memakai nama rute.
  • Layar yang menerima argumen menanganinya dengan aman ketika argumennya bertipe salah atau tidak ada sama sekali.
  • Ada satu rute yang tidak terdaftar, dan aplikasi menanganinya dengan layar "halaman tidak ditemukan" alih-alih crash.
  • Objek router atau tabel rute dibuat sekali sebagai properti final, bukan di dalam build().

Petunjuk: Kriteria kedua adalah harga yang kamu bayar untuk named routes: argumennya bertipe Object?, sehingga compiler tidak bisa lagi menolongmu. Yang tadinya kesalahan saat kompilasi berubah menjadi kesalahan saat aplikasi berjalan. Untuk aplikasi kecil sampai menengah, itu pertukaran yang jarang menguntungkan — dan itulah kesimpulan yang seharusnya kamu tulis sendiri setelah merasakannya.

Ikhtisar

  • Navigator bekerja sebagai tumpukan: push menaruh di atas, pop mengangkat. Tombol kembali memanggil pop otomatis.
  • Kirim data ke layar tujuan lewat konstruktor. Layar hanyalah widget biasa.
  • Terima data saat kembali dengan await pada hasil push, dan pop(nilai) di layar tujuan. Hasilnya null kalau pengguna menekan tombol kembali — selalu periksa.
  • Pakai push<T>() dengan parameter tipe agar hasilnya bertipe aman.
  • pushReplacement mengganti layar saat ini — untuk alur login. pushAndRemoveUntil membersihkan seluruh tumpukan — untuk keluar akun.
  • BottomNavigationBar dengan empat item atau lebih butuh type: BottomNavigationBarType.fixed agar semua label terlihat.
  • Jumlah tabs dan children pada TabBar/TabBarView harus sama dengan length di DefaultTabController.
  • Drawer tidak menutup sendiri. Panggil Navigator.pop dulu, baru dorong layar baru.
  • PopScope menggantikan WillPopScope yang sudah usang. Setel canPop: false, lalu pop manual setelah konfirmasi.
  • Named routes kehilangan keamanan tipe karena argumennya Object?. Untuk aplikasi kecil-menengah, MaterialPageRoute lebih disarankan; go_router untuk kebutuhan kompleks.
  • Hero butuh tag yang unik dalam satu layar dan identik di kedua layar.

Berikutnya: Bab 8 — List, Grid & Data Berulang, tempat kita menampilkan banyak data secara efisien.

Transkrip asli

Disintesis dari 1_flutter_core/7_meals-app_multi-screen-n-navigating-app.md (28 video) dan 1_flutter_zero-to-hero/7_navigation-routing.md serta 8_clean-ui-widgets.md (Hero). Lihat PDF Flutter Core dan PDF Zero to Hero.

Rangkuman pembelajaran pribadi, disusun ulang dari beberapa kursus Flutter.