Navigation #
Navigasi adalah tulang punggung dari setiap aplikasi multi-halaman. Flutter menyediakan dua sistem navigasi: Navigator 1.0 yang imperatif dan sudah lama ada, serta Navigator 2.0 yang deklaratif dan URL-aware — fondasi dari GoRouter. Memahami keduanya membuat kamu bisa memilih yang tepat untuk kebutuhan aplikasimu.
Navigator 1.0 — Navigasi Imperatif #
Navigator 1.0 bekerja seperti tumpukan kartu (stack) — kamu push halaman baru di atas tumpukan dan pop untuk kembali:
// Push halaman baru (bisa kembali ke halaman sebelumnya)
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const DetailScreen()),
);
// Push dan ganti halaman saat ini (tidak bisa kembali)
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
// Push dan hapus semua halaman sebelumnya (untuk logout/onboarding selesai)
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const HomeScreen()),
(route) => false, // false = hapus semua
);
// Kembali ke halaman sebelumnya
Navigator.of(context).pop();
// Kembali dengan mengembalikan data
Navigator.of(context).pop('data yang dikembalikan');
// Cek apakah bisa pop (ada halaman di bawahnya)
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
Mengirim dan Menerima Data #
// Kirim data saat push
final hasil = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (_) => EditScreen(initialValue: 'nilai awal'),
),
);
// hasil berisi nilai yang di-pop dari EditScreen (bisa null jika back button)
if (hasil != null) {
setState(() => _nilai = hasil);
}
// Di EditScreen: kembalikan data saat pop
ElevatedButton(
onPressed: () => Navigator.of(context).pop('nilai baru'),
child: const Text('Simpan'),
)
Named Routes — Navigator 1.0 #
// Definisikan routes di MaterialApp
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/detail': (context) => const DetailScreen(),
'/settings': (context) => const SettingsScreen(),
},
)
// Navigasi dengan nama
Navigator.of(context).pushNamed('/detail');
// Kirim argumen dengan named routes
Navigator.of(context).pushNamed('/detail', arguments: produk);
// Terima argumen di halaman tujuan
final produk = ModalRoute.of(context)!.settings.arguments as Produk;
Named routes Navigator 1.0 tidak lagi direkomendasikan untuk aplikasi baru. Penggunaannya memiliki keterbatasan dalam mendukung deep linking dan web. Untuk aplikasi baru, gunakan GoRouter yang dibahas di bawah.
Transisi Kustom #
// PageRouteBuilder untuk transisi yang dikustomisasi
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const DetailScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// Slide dari kanan
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
final tween = Tween(begin: begin, end: end)
.chain(CurveTween(curve: Curves.easeInOut));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
transitionDuration: const Duration(milliseconds: 300),
),
);
Navigator 2.0 dan GoRouter #
Navigator 1.0 bersifat imperatif — kamu perintah apa yang harus terjadi. Ini tidak cocok untuk URL-based navigation (web, deep links) karena state navigasi tidak terwakili sebagai URL.
Navigator 2.0 memperkenalkan model deklaratif di mana state navigasi (termasuk URL) menentukan halaman apa yang ditampilkan. GoRouter, yang didukung resmi oleh tim Flutter, menyederhanakan Navigator 2.0 menjadi API yang intuitif.
Setup GoRouter #
# pubspec.yaml
dependencies:
go_router: ^14.0.0
import 'package:go_router/go_router.dart';
// Definisikan router -- letakkan sebagai global atau provider
// GoRouter instance harus dideklarasikan sebagai global variable agar
// tidak di-rebuild saat hot reload
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/produk',
builder: (context, state) => const ProdukListScreen(),
),
GoRoute(
path: '/produk/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProdukDetailScreen(produkId: id);
},
),
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsScreen(),
),
],
// Halaman error untuk route yang tidak ditemukan
errorBuilder: (context, state) => const NotFoundScreen(),
);
// Gunakan MaterialApp.router
void main() {
runApp(MaterialApp.router(routerConfig: router));
}
Navigasi dengan GoRouter #
// context.go() -- navigasi dan GANTI history (tidak bisa back)
context.go('/produk');
context.go('/produk/123');
// context.push() -- navigasi dan TAMBAH ke stack (bisa back)
context.push('/settings');
// context.pop() -- kembali ke halaman sebelumnya
context.pop();
// context.pushReplacement() -- ganti halaman saat ini
context.pushReplacement('/login');
// context.goNamed() -- navigasi dengan nama route
context.goNamed('produkDetail', pathParameters: {'id': '123'});
// context.pushNamed() -- push dengan nama route
context.pushNamed('settings');
go() vs push() — Kapan Memilih? #
context.go('/halaman'):
✓ Navigasi deklaratif -- seperti mengubah URL browser
✓ Tidak menambah ke back stack
✓ Untuk navigasi tab, bottom nav, atau redirect
✗ Pengguna tidak bisa "back" ke halaman sebelumnya
context.push('/halaman'):
✓ Navigasi stack -- menambah ke history
✓ Pengguna bisa back
✓ Untuk navigasi modal, detail screen, form
Path Parameter dan Query Parameter #
// Path parameter -- bagian dari URL yang dinamis (:nama)
GoRoute(
path: '/produk/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProdukDetailScreen(produkId: id);
},
),
// Navigasi ke path dengan parameter
context.go('/produk/abc123');
// Query parameter -- ?key=value di URL
GoRoute(
path: '/produk',
builder: (context, state) {
final kategori = state.uri.queryParameters['kategori'];
final sort = state.uri.queryParameters['sort'] ?? 'terbaru';
return ProdukListScreen(kategori: kategori, sort: sort);
},
),
// Navigasi dengan query parameter
context.go('/produk?kategori=elektronik&sort=harga');
// Path + query sekaligus
GoRoute(
path: '/produk/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
final tab = state.uri.queryParameters['tab'] ?? 'deskripsi';
return ProdukDetailScreen(produkId: id, initialTab: tab);
},
),
context.go('/produk/abc123?tab=ulasan');
Melewatkan Objek Kompleks (Extra) #
// Untuk objek yang tidak bisa di-encode ke URL, gunakan 'extra'
context.go('/produk/detail', extra: produkObject);
// Terima di halaman tujuan
GoRoute(
path: '/produk/detail',
builder: (context, state) {
final produk = state.extra as Produk;
return ProdukDetailScreen(produk: produk);
},
),
extra tidak mendukung deep linking — objek Dart tidak bisa di-serialize ke URL. Untuk halaman yang perlu di-deep-link, kirim ID via path parameter dan fetch data di halaman tujuan.Named Routes di GoRouter #
final router = GoRouter(
routes: [
GoRoute(
name: 'home', // nama route
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
name: 'produkDetail',
path: '/produk/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProdukDetailScreen(produkId: id);
},
),
],
);
// Navigasi dengan nama
context.goNamed('produkDetail', pathParameters: {'id': '123'});
context.goNamed(
'produkDetail',
pathParameters: {'id': '123'},
queryParameters: {'tab': 'ulasan'},
);
Redirect dan Route Guard #
GoRouter mendukung redirect berbasis state — sangat berguna untuk auth guard:
final router = GoRouter(
initialLocation: '/',
// Global redirect -- dipanggil setiap navigasi
redirect: (context, state) {
final isLoggedIn = authService.isLoggedIn;
final isLoginRoute = state.matchedLocation == '/login';
// Belum login dan bukan di halaman login --> redirect ke login
if (!isLoggedIn && !isLoginRoute) return '/login';
// Sudah login tapi di halaman login --> redirect ke home
if (isLoggedIn && isLoginRoute) return '/';
// Tidak perlu redirect
return null;
},
routes: [
GoRoute(path: '/', builder: (_, __) => const HomeScreen()),
GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),
GoRoute(
path: '/admin',
// Redirect per-route
redirect: (context, state) {
if (!authService.isAdmin) return '/';
return null;
},
builder: (_, __) => const AdminScreen(),
),
],
)
Redirect Reaktif dengan refreshListenable #
// GoRouter secara otomatis re-evaluate redirect saat notifier berubah
GoRouter(
refreshListenable: authService, // ChangeNotifier
redirect: (context, state) {
if (!authService.isLoggedIn) return '/login';
return null;
},
routes: [...],
)
// authService.notifyListeners() --> GoRouter otomatis cek redirect lagi
class AuthService extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
void login() {
_isLoggedIn = true;
notifyListeners(); // router otomatis re-evaluate dan navigasi ke '/'
}
void logout() {
_isLoggedIn = false;
notifyListeners(); // router otomatis redirect ke '/login'
}
}
Nested Routes (Sub-Routes) #
GoRoute(
path: '/produk',
builder: (context, state) => const ProdukListScreen(),
routes: [
// Sub-route: /produk/:id
GoRoute(
path: ':id', // path relatif -- tidak perlu /
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProdukDetailScreen(produkId: id);
},
routes: [
// Sub-sub-route: /produk/:id/ulasan
GoRoute(
path: 'ulasan',
builder: (context, state) {
final id = state.pathParameters['id']!;
return UlasanScreen(produkId: id);
},
),
],
),
],
),
ShellRoute dan StatefulShellRoute — Navigasi Tab #
ShellRoute memungkinkan persistent UI shell (seperti BottomNavigationBar) yang tetap terlihat saat berpindah antar tab. StatefulShellRoute menjaga state masing-masing tab:
final router = GoRouter(
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
// Shell UI yang persistent -- BottomNavigationBar tetap ada
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
// Branch 1: Beranda
StatefulShellBranch(
routes: [
GoRoute(
path: '/beranda',
builder: (_, __) => const BerandaScreen(),
routes: [
GoRoute(
path: 'detail/:id',
builder: (context, state) => DetailScreen(
id: state.pathParameters['id']!,
),
),
],
),
],
),
// Branch 2: Eksplorasi
StatefulShellBranch(
routes: [
GoRoute(
path: '/eksplorasi',
builder: (_, __) => const Eksplorasi Screen(),
),
],
),
// Branch 3: Profil
StatefulShellBranch(
routes: [
GoRoute(
path: '/profil',
builder: (_, __) => const ProfilScreen(),
),
],
),
],
),
],
);
// Shell widget dengan BottomNavigationBar
class ScaffoldWithNavBar extends StatelessWidget {
final StatefulNavigationShell navigationShell;
const ScaffoldWithNavBar({super.key, required this.navigationShell});
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell, // konten tab yang aktif
bottomNavigationBar: NavigationBar(
selectedIndex: navigationShell.currentIndex,
onDestinationSelected: (index) {
// Pindah tab -- state tab yang lama dipertahankan
navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
);
},
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Beranda'),
NavigationDestination(icon: Icon(Icons.explore), label: 'Eksplorasi'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profil'),
],
),
);
}
}
Deep Linking #
Flutter mendukung deep linking di iOS, Android, dan web. Membuka URL akan menampilkan halaman yang sesuai di aplikasimu. GoRouter secara otomatis menangani deep link selama route didefinisikan dengan benar.
// GoRouter menangani deep link secara otomatis
// URL: myapp://produk/123 --> ProdukDetailScreen(produkId: '123')
GoRoute(
path: '/produk/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProdukDetailScreen(produkId: id);
},
),
// Konfigurasi di Android (AndroidManifest.xml):
// <intent-filter android:autoVerify="true">
// <action android:name="android.intent.action.VIEW" />
// <category android:name="android.intent.category.DEFAULT" />
// <category android:name="android.intent.category.BROWSABLE" />
// <data android:scheme="https" android:host="app.contoh.com" />
// </intent-filter>
Ringkasan #
- Navigator 1.0 bekerja seperti stack:
pushmenambah halaman,popmenghapus. Gunakan untuk navigasi sederhana yang tidak memerlukan deep linking.- Named routes Navigator 1.0 tidak lagi direkomendasikan — gunakan GoRouter untuk aplikasi baru.
- GoRouter adalah router resmi Flutter berbasis Navigator 2.0 — deklaratif, URL-aware, mendukung deep linking, redirect, dan nested navigation.
- Gunakan
context.go()untuk navigasi yang mengganti URL (tab, redirect) dancontext.push()untuk navigasi stack yang bisa di-back.- Path parameter (
:id) untuk data yang wajib ada di URL. Query parameter (?key=value) untuk data opsional.extrauntuk objek Dart yang tidak perlu di-URL-kan (tapi tidak mendukung deep link).redirectdanrefreshListenablememungkinkan route guard berbasis state — contohnya auth check yang otomatis redirect saat status login berubah.StatefulShellRoute.indexedStackuntuk BottomNavigationBar dengan state yang dipertahankan per tab — masing-masing branch punya Navigator dan history sendiri.- GoRouter menangani deep linking secara otomatis selama route didefinisikan dengan benar dan platform dikonfigurasi dengan intent filter yang tepat.