Flutter Bottom Navigation
2/19/2020 • ProgrammingSalah satu komponen material design untuk navigasi adalah Bottom Navigation Bar, dengan komponen tersebut pengguna dapat berpindah halaman atau layar dengan mudah dan ergonomis terutama pada perangkat mobile. Bottom Navigation pada praktik terbaiknya haruslah memiliki tiga hingga lima destinasi atau tujuan. Setiap destinasi dilambangkan dengan ikon dan teks. Teks pada Bottom Navigation bersifat opsional. Untuk lebih lengkapnya tentang komponen ini bisa kunjungi halaman material design di sini.
Di Flutter, Bottom Navigation sudah tersedia dalam paket flutter/material.dart
dengan nama widget BottomNavigationBar
dan BottomNavigationBarItem
sebagai item navigasi. Widget ini dapat diterapkan pada properti bottomNavigationBar
pada widget Scaffold
. Sebenarnya ada banyak cara penggunaan komponen ini, namun pada tulisan ini hanya akan menunjukkan salah satunya saja.
class Main extends StatefulWidget {
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: /* ... */,
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
/* ... */
},
items: <BottomNavigationBarItem>[
/* ... */
],
),
);
}
}
Karena setiap kali BottomNavigationBarItem
di-tap kita perlu berganti layar maka di sini kita menggunakan StatefulWidget
. AFAIK, di Flutter tidak ada controller atau adapter untuk menangani pergantian layar pada Bottom Navigation. Jadi konsepnya, setiap aksi tap pada item navigasi, method onTap
pada BottomNavigationBar
akan triggered dengan membawa index dari item yang mendapat aksi tap. Nah, method onTap
inilah yang akan kita gunakan untuk berganti layar dengan memperbarui body
pada Scaffold
.
class _MainState extends State<Main> {
final List<Widget> _screens = []; // List untuk menampung layar
int _activeScreenIndex = 0; // Indeks layar yang sedang tampil
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: _screens[_activeScreenIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
// Update indeks layar yang sedang tampil
// setiap kali menerima aksi tap
setState(() {
_activeScreenIndex = index;
});
},
items: <BottomNavigationBarItem>[
/* ... */
],
),
);
}
}
Untuk _screens
kita contohkan tiga buah simple widget dan untuk items
pada BottomNavigationBar
tentu kita definisikan tiga item menyesuaikan widget pada _screens
.
class _MainState extends State<Main> {
// Tiga layar
final List<Widget> _screens = [
HomeScreen(),
FavoriteScreen(),
AccountScreen()
];
int _activeScreenIndex = 0;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: _screens[_activeScreenIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
_activeScreenIndex = index;
});
},
items: <BottomNavigationBarItem>[
// Tiga item navigasi
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite),
title: Text('Favorite'),
),
BottomNavigationBarItem(
icon: Icon(Icons.account_box),
title: Text('Account'),
),
],
),
);
}
}
Widget untuk masing-masing layar
class HomeScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text('Welcome to Home!'),
),
);
}
}
class FavoriteScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text('Favorite things!'),
),
);
}
}
class AccountScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text('Your account!'),
),
);
}
}
Sampai di sini kita telah menerapkan Bottom Navigation, tambahkan main function
void main() => runApp(
MaterialApp(
theme: ThemeData(
primarySwatch: Colors.teal,
),
home: Main(),
),
);
Lalu coba jalankan, seharusnya aplikasi dapat berjalan seperti berikut
Layar berganti sesuai dengan item yang dipilih namun item pada Bottom Navigation tidak mengubah state-nya saat menerima aksi tap, untuk mengatasi ini cukup tambahkan properti currentIndex
pada widget BottomNavigationBar
dan nilainya adalah _activeScreenIndex
seperti ini
BottomNavigationBar(
currentIndex: _activeScreenIndex,
onTap: ...,
items: ...
)
Jika dijalankan lagi seharusnya hasilnya seperti ini
Bekerja dengan baik. Selanjutnya kita akan mencoba menampilkan daftar teks plain sederhana pada layar favorite, cukup dengan menggunakan ListView
seperti ini
class FavoriteScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
child: ListView(
children: List<Widget>.generate(20, (index) {
return ListTile(
title: Text(
'Favorite $index',
style: TextStyle(
fontSize: 18,
),
),
subtitle: Text('This is description of favorite $index'),
);
}),
),
);
}
}
Ketika dijalankan lagi, layar favorite akan menampilkan data seperti berikut
Ada experience yang kurang baik saat melakukan navigasi dari layar favorite yang menampilkan daftar data menuju ke layar account lalu kembali lagi ke layar favorite dengan daftar data yang berada di posisi awal lagi, ini menunjukkan state layar tidak tersimpan, setiap kali layar tampil selalu membuat instansi widget baru. Untuk mengatasi ini, kita dapat memanfaatkan widget PageStorage
yang dapat membuat penampung widget layar supaya tidak selalu membuat instansi baru saat dibutuhkan. Widget PageStorage
digunakan bersama dengan PageStorageKey
sebagai key dan PageStorageBucket
sebagai penampung atau penyimpan state dari masing-masing layar. Penerapan seperti ini
final PageStorageBucket _bucket = PageStorageBucket(); // Dekralasi PageStorageBucket
final List<Widget> _screens = [
// Gunakan PageStorageKey pada masing-masing widget layar
HomeScreen(key: PageStorageKey('key-home')),
FavoriteScreen(key: PageStorageKey('key-favorite')),
AccountScreen(key: PageStorageKey('key-account'))
];
Widget build(BuildContext context) {
return Scaffold(
appBar: /* ... */,
body: PageStorage( // Terapkan PageStorage di body
child: _screens[_activeScreenIndex],
bucket: _bucket,
),
bottomNavigationBar: /* ... */
);
}
Pada widget layar kita menggunakan PageStorageKey
maka kita perlu membuat constructor dengan parameter Key
supaya dapat di-pass ke super constructor widget layar. Super class widget layar adalah widget, setiap widget memiliki properti key, key ini berfungsi untuk mengontrol bagaimana satu widget menggantikan widget lain di widget tree.
class HomeScreen extends StatelessWidget {
const HomeScreen({Key key}) : super(key: key);
Widget build(BuildContext context) { /* ... */ }
}
class FavoriteScreen extends StatelessWidget {
const FavoriteScreen({Key key}) : super(key: key);
Widget build(BuildContext context) { /* ... */ }
}
class AccountScreen extends StatelessWidget {
const AccountScreen({Key key}) : super(key: key);
Widget build(BuildContext context) { /* ... */ }
}
Lalu coba jalankan lagi, maka seharusnya sesuai ekspektasi
Supaya kode lebih rapi, kita dapat membuat class model untuk menampung properti layar dan item navigasi, misal di sini saya beri nama class-nya PageItem
seperti ini
class PageItem {
final String title;
final IconData icon;
final Widget screen;
PageItem(this.title, this.icon, this.screen);
}
Lalu kita sesuaikan kode pada class _MainState
sehingga menjadi seperti ini
class _MainState extends State<Main> {
final PageStorageBucket _bucket = PageStorageBucket();
final List<PageItem> _items = [
PageItem('Home', Icons.home, HomeScreen(key: PageStorageKey('key--home'))),
PageItem('Favorite', Icons.favorite, FavoriteScreen(key: PageStorageKey('key--favorite'))),
PageItem('Account', Icons.account_box, AccountScreen(key: PageStorageKey('key--account'))),
];
int _activeScreenIndex = 0;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_items[_activeScreenIndex].title),
),
body: PageStorage(
child: _items[_activeScreenIndex].screen,
bucket: _bucket,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _activeScreenIndex,
onTap: (index) {
setState(() {
_activeScreenIndex = index;
});
},
items: _items.map((item) {
return BottomNavigationBarItem(
title: Text(item.title),
icon: Icon(item.icon),
);
}).toList(),
),
);
}
}
Selain lebih rapi, kita juga akan lebih mudah saat menambah widget layar baru. Cukup dengan menambahkan PageItem
pada list _items
, layar dan item navigasi akan menyesuaikan. Selain itu kita membuat judul pada AppBar
juga menjadi dinamis sesuai layar yang tampil.
Dan kode selengkapnya dapat dilihat di bawah ini.
Keseluruhan kode
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
theme: ThemeData(
primarySwatch: Colors.teal,
),
home: Main(),
),
);
class Main extends StatefulWidget {
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
final PageStorageBucket _bucket = PageStorageBucket();
final List<PageItem> _items = [
PageItem('Home', Icons.home, HomeScreen(key: PageStorageKey('key--home'))),
PageItem('Favorite', Icons.favorite, FavoriteScreen(key: PageStorageKey('key--favorite'))),
PageItem('Account', Icons.account_box, AccountScreen(key: PageStorageKey('key--account'))),
];
int _activeScreenIndex = 0;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_items[_activeScreenIndex].title),
),
body: PageStorage(
child: _items[_activeScreenIndex].screen,
bucket: _bucket,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _activeScreenIndex,
onTap: (index) {
setState(() {
_activeScreenIndex = index;
});
},
items: _items.map((item) {
return BottomNavigationBarItem(
title: Text(item.title),
icon: Icon(item.icon),
);
}).toList(),
),
);
}
}
class PageItem {
final String title;
final IconData icon;
final Widget screen;
PageItem(this.title, this.icon, this.screen);
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key key}) : super(key: key);
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text('Welcome to Home!'),
),
);
}
}
class FavoriteScreen extends StatelessWidget {
const FavoriteScreen({Key key}) : super(key: key);
Widget build(BuildContext context) {
return Container(
child: ListView(
children: List<Widget>.generate(20, (index) {
return ListTile(
title: Text(
'Favorite $index',
style: TextStyle(
fontSize: 18,
),
),
subtitle: Text('This is description of favorite $index'),
);
}),
),
);
}
}
class AccountScreen extends StatelessWidget {
const AccountScreen({Key key}) : super(key: key);
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text('Your account!'),
),
);
}
}
Seperti yang sebelumnya saya singgung, ada banyak cara penerapan Bottom Navigation pada Flutter dan ini hanya salah satunya. Di Flutter, everything is widget, jadi sangat fleksibel.
Demikian, semoga bermanfaat.