Belajar Flutter dari Awal: Navigasi Antar Halaman dengan Navigator (Part 6)
Rifqi An
Halo sobat ngoding dan pejuang kopi! Balik lagi nih di seri tutorial Belajar Flutter dari Awal yang udah sampai part ke-6! Gila, nggak kerasa ya? Udah sejauh ini kita bergulat sama widget-widget dan state management. Nah, di part kali ini, kita akan kembali menyelami dunia navigation yang kadang bikin pusing kepala tapi penting banget: Navigasi Antar Halaman dengan Navigator!
Kalian pasti udah nggak asing kan sama yang namanya pindah-pindah layar di aplikasi? Dari halaman login ke dashboard, dari daftar produk ke detail produk, atau dari chat list ke ruang obrolan. Nah, itu semua kerjaan si Navigator, gengs! Di part sebelumnya kita udah bahas sedikit tentang basic-nya, sekarang mari kita gas lebih dalam lagi biar nggak cuma bisa push sama pop doang, tapi bisa lebih dari itu. Siapin kopi atau teh hangatnya, mari kita selami dunia Navigator!
Daftar Isi
- Pengantar: Balik Lagi Ngomongin Navigator!
- Recap Singkat: Navigator Basics (Sekilas Pandang)
- Lebih Dalam dengan
Navigator.push() - Metode Lain:
Navigator.pushReplacement()danNavigator.popAndPushNamed()(Kapan Pakainya?) - Studi Kasus Ngoding: Aplikasi To-Do List Sederhana
- Latihan: Waktunya Nge-bug Bareng!
Pengantar: Balik Lagi Ngomongin Navigator!
Kalian mungkin udah ngerasain kan, gimana rasanya ngoding fitur navigasi di Flutter? Kadang gampang, kadang juga bikin garuk-garuk kepala mikirin gimana cara passing data antar halaman, atau gimana caranya nge-refresh data setelah balik dari halaman lain. Tenang, itu wajar banget kok! Kita semua pernah di fase itu, bahkan senior pun kadang masih suka bingung sama stack management di Navigator.
Di part ke-6 ini, kita akan deep dive lebih jauh. Nggak cuma sekadar pindah halaman doang, tapi gimana caranya kita bisa ngirim data pas pindah halaman, terus gimana caranya halaman yang tadi kita tuju bisa ngasih balasan data pas balik lagi. Seru kan? Ibarat kirim surat cinta, kita nggak cuma bisa ngirim doang, tapi juga bisa dapet balesannya!
Recap Singkat: Navigator Basics (Sekilas Pandang)
Oke, buat yang mungkin udah lupa-lupa ingat, atau yang baru gabung di part ini (walaupun disarankan ngikutin dari awal biar nyambung ya!), kita recap sebentar tentang dasar-dasar Navigator. Intinya, Navigator di Flutter itu kayak manajer tumpukan (stack) halaman di aplikasi kita. Ketika kita push sebuah halaman, halaman itu akan ditumpuk di atas halaman yang sekarang. Pas kita pop, halaman yang paling atas akan dibuang dan kita balik ke halaman sebelumnya.
Contoh paling basic-nya adalah kayak gini:
// Dari halaman A, mau ke halaman B
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
Dan buat baliknya dari halaman B ke A:
// Dari halaman B, mau balik ke halaman A
Navigator.pop(context);
Gampang kan? Nah, sekarang mari kita naik level!
Lebih Dalam dengan Navigator.push()
Metode push() ini paling sering kita pakai. Tapi, nggak cuma buat sekadar pindah halaman doang. Kita bisa ngelakuin dua hal keren lainnya bareng push():
- Meneruskan data ke halaman yang dituju.
- Menerima data kembali dari halaman yang dituju setelah dia di-
pop.
Yuk, kita bedah satu per satu!
Meneruskan Data ke Halaman Berikutnya
Sering banget kan kita butuh ngirim data dari satu halaman ke halaman lain? Misalnya, dari daftar produk ke halaman detail produk, kita butuh ngirim ID produknya. Ada beberapa cara, tapi yang paling umum dan bersih adalah lewat constructor halaman tujuan atau menggunakan arguments di named routes.
Menggunakan Constructor (Cara Paling Umum)
Ini cara yang paling sering dipakai dan paling straightforward. Kita tinggal tambahin parameter di constructor StatelessWidget atau StatefulWidget kita.
Misalnya kita punya halaman DetailPage yang butuh data itemTitle:
// detail_page.dart
import 'package:flutter/material.dart';
class DetailPage extends StatelessWidget {
final String itemTitle;
const DetailPage({Key? key, required this.itemTitle}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Detail Item')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Ini adalah detail untuk: ${itemTitle}', style: const TextStyle(fontSize: 20)),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// Balik ke halaman sebelumnya
Navigator.pop(context);
},
child: const Text('Kembali'),
),
],
),
),
);
}
}
Nah, pas kita mau ke DetailPage dari HomePage, tinggal kirim aja datanya lewat constructor:
// home_page.dart (contoh bagian tombol)
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(itemTitle: 'Celana Levis Nomor 30'), // Data dikirim di sini!
),
);
},
child: const Text('Lihat Detail Celana'),
)
Mengambil Data Kembali dari Halaman Berikutnya (Callback!)
Ini nih yang seru! Kadang kita butuh feedback atau data dari halaman yang baru aja kita tutup. Contoh paling sering: kita buka halaman untuk edit profile, setelah selesai edit dan di-save, kita mau data di halaman profile utama langsung ke-update. Gimana caranya?
Metode Navigator.push() mengembalikan sebuah Future. Jadi kita bisa menggunakan await untuk menunggu hasil dari halaman yang di-pop!
Pertama, di DetailPage, kita bisa mengembalikan data saat melakukan pop:
// detail_page.dart (lanjutan dari contoh sebelumnya)
// Misalkan ada tombol 'Selesai Edit' yang mengembalikan status
ElevatedButton(
onPressed: () {
// Balik ke halaman sebelumnya sambil membawa data "item telah diupdate!"
Navigator.pop(context, 'Data "${itemTitle}" berhasil diupdate!');
},
child: const Text('Selesai Edit'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
// Balik tanpa membawa data
Navigator.pop(context);
},
child: const Text('Batal'),
),
Kemudian, di HomePage, kita bisa "menangkap" data yang dikembalikan itu:
// home_page.dart (contoh bagian tombol yang memanggil DetailPage)
ElevatedButton(
onPressed: () async { // INGAT! Harus async
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(itemTitle: 'Celana Levis Nomor 30'),
),
);
// Cek apakah ada data yang dikembalikan
if (result != null && result is String) {
// Kita bisa menampilkan notifikasi atau update UI dengan data yang dikembalikan
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Info dari DetailPage: $result')),
);
print('Data dari detail: $result'); // Cetak di console juga boleh
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Kembali dari DetailPage tanpa data spesifik.')),
);
}
},
child: const Text('Lihat Detail & Tunggu Hasil'),
)
Gimana? Keren kan? Jadi nggak cuma satu arah doang, tapi bisa dua arah! Ini bakal sering banget kepake pas kalian ngoding aplikasi yang interaktif.
Metode Lain: Navigator.pushReplacement() dan Navigator.popAndPushNamed() (Kapan Pakainya?)
Selain push() dan pop() yang udah kita bahas, ada beberapa metode lain yang juga berguna banget dalam skenario tertentu. Mereka ini ibarat jurus rahasia yang bisa menyelamatkan kita dari stack yang kepenuhan atau navigasi yang nggak efisien.
Navigator.pushReplacement()
Metode ini berfungsi untuk mengganti halaman yang sedang aktif dengan halaman baru. Halaman lama akan di-pop (dibuang dari stack) dan halaman baru akan di-push (ditambahkan ke stack) sebagai gantinya. Jadi, pas kita pencet tombol back, kita nggak akan balik ke halaman yang baru saja diganti, tapi langsung ke halaman di bawahnya.
Kapan pakainya?
- Setelah login sukses, kita ingin langsung ke dashboard tanpa bisa balik lagi ke halaman login pakai tombol back. Ini penting banget buat keamanan dan pengalaman pengguna!
- Setelah selesai proses onboarding, langsung ke halaman utama.
- Mengganti halaman edit dengan halaman detail setelah data disimpan.
// Contoh setelah login berhasil
ElevatedButton(
onPressed: () {
// Anggap ini adalah fungsi login
bool loginSuccess = true; // Contoh, aslinya dari API/validasi
if (loginSuccess) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => DashboardPage()),
);
} else {
// Tampilkan error
}
},
child: const Text('Login'),
)
Dengan cara ini, halaman LoginPage akan hilang dari stack, sehingga tombol back di DashboardPage tidak akan kembali ke LoginPage.
Navigator.popAndPushNamed()
Metode ini melakukan dua hal sekaligus: pop halaman yang sedang aktif, lalu push sebuah named route baru. Ini mirip dengan pushReplacement tapi secara eksplisit menggunakan named routes yang sudah kita daftarkan di MaterialApp.
Kapan pakainya?
- Sama seperti
pushReplacement, ketika kita ingin mengganti halaman yang sedang aktif dengan halaman lain dan halaman yang diganti tidak bisa diakses lagi dengan tombol back. Bedanya, ini khusus untuk named routes. - Misalnya, dari halaman pengaturan profil, setelah selesai menyimpan, kita ingin kembali ke halaman beranda utama (yang sudah ada di bawah stack) dan me-refresh-nya. Tapi, ini lebih cocok jika halaman tujuan *bukan* halaman yang sudah ada di stack. Kalau halaman tujuan *sudah ada* di stack, mungkin
popUntillebih pas.
// Dengan asumsi kita punya named routes: '/' untuk HomePage, '/dashboard' untuk DashboardPage
Navigator.popAndPushNamed(context, '/dashboard');
Jadi, halaman yang sekarang akan di-pop, lalu halaman dengan nama '/dashboard' akan di-push. Efeknya mirip pushReplacement, tapi dengan penekanan pada penggunaan named routes.
Studi Kasus Ngoding: Aplikasi To-Do List Sederhana
Biar nggak cuma teori doang, mari kita bikin contoh sederhana yang mengaplikasikan semua yang udah kita pelajari. Kita akan bikin aplikasi To-Do List yang bisa menampilkan daftar tugas, lalu saat tugas diklik, kita bisa masuk ke halaman detail untuk melihat/mengedit tugas tersebut, dan setelah selesai, data bisa dikembalikan ke halaman utama.
Ini adalah struktur file yang akan kita pakai (secara logika, bukan beneran bikin file banyak):
main.dart: Setup aplikasi dan named routes.home_page.dart: Menampilkan daftar To-Do.todo_detail_page.dart: Halaman untuk melihat/mengedit detail To-Do.
Routing untuk Halaman Utama dan Detail
Pertama, kita siapkan MaterialApp dengan named routes di main.dart. Ini penting agar kita bisa memanggil halaman dengan nama, bukan cuma MaterialPageRoute terus-terusan.
// main.dart
import 'package:flutter/material.dart';
import 'home_page.dart'; // Anggap aja ada file ini
import 'todo_detail_page.dart'; // Anggap aja ada file ini
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Simple To-Do App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/detail_todo': (context) => const TodoDetailPage(), // Kita akan passing data via arguments
},
);
}
}
Navigasi dari Home ke Detail (Meneruskan Data To-Do)
Di HomePage, kita akan punya daftar To-Do. Ketika salah satu To-Do diklik, kita akan navigasi ke TodoDetailPage sambil mengirimkan data To-Do yang diklik tersebut.
// todo_item.dart (Model data sederhana)
class TodoItem {
String id;
String title;
String description;
bool isCompleted;
TodoItem({
required this.id,
required this.title,
this.description = '',
this.isCompleted = false,
});
TodoItem copyWith({
String? id,
String? title,
String? description,
bool? isCompleted,
}) {
return TodoItem(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
// home_page.dart
import 'package:flutter/material.dart';
import 'todo_item.dart'; // Import model To-Do
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<TodoItem> todos = [
TodoItem(id: '1', title: 'Belajar Flutter Navigator', description: 'Jangan sampai nyangkut!', isCompleted: false),
TodoItem(id: '2', title: 'Ngopi biar Melek', description: 'Kopi hitam tanpa gula', isCompleted: true),
TodoItem(id: '3', title: 'Beresin Bug', description: 'Bug kemaren yang bikin lembur', isCompleted: false),
];
void _handleTodoUpdate(TodoItem updatedTodo) {
setState(() {
int index = todos.indexWhere((todo) => todo.id == updatedTodo.id);
if (index != -1) {
todos[index] = updatedTodo;
}
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('To-Do "${updatedTodo.title}" berhasil diupdate!')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My To-Do List')),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
elevation: 2,
child: ListTile(
title: Text(todo.title, style: TextStyle(
decoration: todo.isCompleted ? TextDecoration.lineThrough : TextDecoration.none,
color: todo.isCompleted ? Colors.grey : Colors.black,
)),
subtitle: Text(todo.description),
trailing: Checkbox(
value: todo.isCompleted,
onChanged: (bool? newValue) {
setState(() {
todo.isCompleted = newValue!;
});
},
),
onTap: () async {
// Navigasi ke halaman detail sambil mengirim objek To-Do
// Kita menggunakan Navigator.push() karena ingin menerima hasil kembali
final updatedTodo = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TodoDetailPage(initialTodo: todo),
),
);
if (updatedTodo != null && updatedTodo is TodoItem) {
_handleTodoUpdate(updatedTodo);
}
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// Contoh menambah To-Do baru (akan dikembalikan dari halaman detail)
final newTodo = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TodoDetailPage(initialTodo: TodoItem(id: UniqueKey().toString(), title: '', description: '')),
),
);
if (newTodo != null && newTodo is TodoItem && newTodo.title.isNotEmpty) {
setState(() {
todos.add(newTodo);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('To-Do "${newTodo.title}" berhasil ditambahkan!')),
);
}
},
child: const Icon(Icons.add),
),
);
}
}
Navigasi dari Detail Balik ke Home (Meneruskan Hasil Editan/Status)
Di TodoDetailPage, kita akan menampilkan detail To-Do dan menyediakan form untuk mengeditnya. Setelah selesai edit, kita akan pop halaman ini sambil mengembalikan objek To-Do yang sudah di-update.
// todo_detail_page.dart
import 'package:flutter/material.dart';
import 'todo_item.dart'; // Import model To-Do
class TodoDetailPage extends StatefulWidget {
final TodoItem initialTodo;
const TodoDetailPage({Key? key, required this.initialTodo}) : super(key: key);
@override
State<TodoDetailPage> createState() => _TodoDetailPageState();
}
class _TodoDetailPageState extends State<TodoDetailPage> {
late TextEditingController _titleController;
late TextEditingController _descriptionController;
late bool _isCompleted;
@override
void initState() {
super.initState();
_titleController = TextEditingController(text: widget.initialTodo.title);
_descriptionController = TextEditingController(text: widget.initialTodo.description);
_isCompleted = widget.initialTodo.isCompleted;
}
@override
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}
void _saveTodo() {
final updatedTodo = widget.initialTodo.copyWith(
title: _titleController.text,
description: _descriptionController.text,
isCompleted: _isCompleted,
);
// Mengembalikan objek updatedTodo saat halaman di-pop
Navigator.pop(context, updatedTodo);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.initialTodo.title.isEmpty ? 'Tambah To-Do' : 'Edit To-Do')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _titleController,
decoration: const InputDecoration(
labelText: 'Judul To-Do',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
TextField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Deskripsi',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 20),
Row(
children: [
Checkbox(
value: _isCompleted,
onChanged: (bool? newValue) {
setState(() {
_isCompleted = newValue!;
});
},
),
const Text('Sudah Selesai?'),
],
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _saveTodo,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(50), // Lebar penuh
),
child: const Text('Simpan Perubahan'),
),
const SizedBox(height: 10),
TextButton(
onPressed: () {
Navigator.pop(context); // Kembali tanpa menyimpan
},
child: const Text('Batal'),
),
],
),
),
);
}
}
Dengan begini, kita berhasil membuat sistem navigasi yang tidak hanya pindah halaman, tapi juga bisa kirim dan terima data bolak-balik. Ini adalah fondasi penting banget buat bikin aplikasi Flutter yang interaktif dan dinamis!
Latihan: Waktunya Nge-bug Bareng!
Oke, biar makin mantap pemahaman kalian tentang Navigator, ini ada tantangan lucu-lucuan buat kalian! Anggap saja kalian adalah seorang programmer yang lagi bikin aplikasi kencan (jangan mesum ya!).
Skenario:
- Kalian punya halaman
ProfilePageyang menampilkan nama dan bio pengguna. - Ada tombol "Edit Profile" di
ProfilePageyang akan membawa kalian keEditProfilePage. - Di
EditProfilePage, ada duaTextFielduntuk mengedit nama dan bio, serta tombol "Simpan" dan "Batal". - Jika tombol "Simpan" ditekan, kalian harus mengirim kembali nama dan bio yang baru ke
ProfilePage. Lalu,ProfilePageharus langsung meng-update tampilannya. - Jika tombol "Batal" ditekan, kalian cukup kembali ke
ProfilePagetanpa perubahan apapun. - Bonus: Setelah selesai edit dan kembali ke
ProfilePage, tampilkanSnackBaryang isinya "Profil berhasil di-update, siap cari jodoh!" jika ada perubahan, atau "Nggak jadi di-update ya? Mungkin udah betah jomblo..." jika batal.
Gunakan semua jurus Navigator.push() dan Navigator.pop(context, result) yang udah kita pelajari! Jangan lupa bikin model data User sederhana kalian sendiri ya. Semangat ngoding, dan semoga nggak ketemu bug cinta!
.png)