Halo developer! π
Di artikel #24 ini kita bakal belajar tentang Bottom Navigation Bar dan TabBar β dua komponen UI paling umum yang hampir pasti ada di setiap app. Instagram pakai bottom nav, WhatsApp juga. Gmail pakai tab bar di inbox-nya.
Topik ini penting banget karena jadi fondasi navigasi multi-halaman dalam satu screen. Kita akan bahas:
BottomNavigationBarβ navigasi di bagian bawah layarTabBar+TabBarViewβ tab horizontal di dalam halaman- Kombinasi keduanya dalam satu aplikasi
Yuk mulai! π
1. BottomNavigationBar Dasar
BottomNavigationBar adalah widget bawaan Flutter yang menampilkan ikon-ikon di bagian bawah layar. User tap ikon, halaman berubah.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bottom Nav Demo',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
home: const MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
final List<Widget> _pages = [
const Center(child: Text('π Home', style: TextStyle(fontSize: 24))),
const Center(child: Text('π Search', style: TextStyle(fontSize: 24))),
const Center(child: Text('π€ Profile', style: TextStyle(fontSize: 24))),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My App')),
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
Yang perlu diperhatikan:
currentIndexβ index halaman yang sedang aktifonTapβ callback saat user tap, update index dengansetStateitemsβ daftar item navigasi (ikon + label)- Body berubah sesuai
_currentIndex
Kalau cuma 2-3 item, di atas udah cukup. Tapi kalau lebih dari 3 item, gunakan type: BottomNavigationBarType.fixed supaya semua label tetap terlihat (default: 4+ item jadi shifting).
BottomNavigationBar(
type: BottomNavigationBarType.fixed, // semua label selalu tampil
currentIndex: _currentIndex,
onTap: (index) => setState(() => _currentIndex = index),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
BottomNavigationBarItem(icon: Icon(Icons.favorite), label: 'Favorites'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
)
BottomNavigationBar Tipe 3 dan 4
Ada 3 style utama di Material 3:
BottomNavigationBarType.fixedβ label selalu tampil, warna statisBottomNavigationBarType.shiftingβ label muncul saat aktif saja, item lebih lebar- Material 3 default β gaya βnavigation barβ dengan background dan indicator
Untuk Material 3, kamu juga bisa pakai NavigationBar (widget baru) yang punya animasi halus bawaan:
NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() => _currentIndex = index);
},
destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search_outlined), selectedIcon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.person_outlined), selectedIcon: Icon(Icons.person), label: 'Profile'),
],
)
Tip: Kalau pakai
useMaterial3: true(default di Flutter 3.16+), gunakanNavigationBaruntuk tampilan terkini.BottomNavigationBarmasih bekerja tapi tampilan agak klasik.
2. TabBar & TabBarView
TabBar cocok untuk navigasi horizontal β seperti tab di WhatsApp (Chat, Status, Calls). Biasanya posisi di bawah AppBar.
class TabDemoScreen extends StatelessWidget {
const TabDemoScreen({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3, // jumlah tab
child: Scaffold(
appBar: AppBar(
title: const Text('TabBar Demo'),
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.chat), text: 'Chat'),
Tab(icon: Icon(Icons.info), text: 'Status'),
Tab(icon: Icon(Icons.phone), text: 'Calls'),
],
),
),
body: const TabBarView(
children: [
Center(child: Text('π¬ Chat List')),
Center(child: Text('π Status Updates')),
Center(child: Text('π Call History')),
],
),
),
);
}
}
Key points:
DefaultTabControllerβ membungkus seluruh widget tree, memberikan tab controller secara otomatis.lengthharus sesuai jumlah tabTabBarβ taruh diAppBar.bottom, berisiTabwidgetsTabBarViewβ body yang isinya konten per tab, urutan harus sama denganTabBar
TabBar dengan Badge / Counter
Mau tambahkan angka notifikasi di tab? Bisa pakai Badge:
TabBar(
tabs: [
const Tab(text: 'Chat'),
const Tab(text: 'Status'),
Badge(
label: const Text('5'),
child: const Tab(text: 'Calls'),
),
],
)
3. Kombinasi: Bottom Nav + TabBar
Ini pattern paling real-world β misalnya Instagram: bottom nav buat ganti section, tapi di halaman tertentu ada tab bar (like, reels, tags di profil).
class CombinedScreen extends StatefulWidget {
const CombinedScreen({super.key});
@override
State<CombinedScreen> createState() => _CombinedScreenState();
}
class _CombinedScreenState extends State<CombinedScreen> {
int _bottomIndex = 0;
// Tab 0: Home page (punya tab bar sendiri)
Widget _buildHomePage() {
return DefaultTabController(
length: 2,
child: Column(
children: [
const TabBar(
tabs: [
Tab(text: 'For You'),
Tab(text: 'Following'),
],
),
Expanded(
child: TabBarView(
children: [
Center(
child: ListView.builder(
itemCount: 5,
itemBuilder: (ctx, i) => ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text('Post For You #$i'),
),
),
),
const Center(child: Text('Following feed')),
],
),
),
],
),
);
}
Widget _buildProfilePage() {
return DefaultTabController(
length: 3,
child: Column(
children: [
// Profil header
const Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(radius: 40, child: Icon(Icons.person, size: 40)),
SizedBox(height: 8),
Text('Ahmad Saifullah', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
Text('Flutter Developer'),
],
),
),
const TabBar(
tabs: [
Tab(icon: Icon(Icons.grid_view)),
Tab(icon: Icon(Icons.bookmark)),
Tab(icon: Icon(Icons.person)),
],
),
Expanded(
child: TabBarView(
children: [
// Grid posts
GridView.builder(
padding: const EdgeInsets.all(4),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 4, mainAxisSpacing: 4),
itemCount: 9,
itemBuilder: (ctx, i) => Container(
color: Colors.blue.shade100,
child: Center(child: Text('${i + 1}')),
),
),
const Center(child: Text('Bookmarked posts')),
const Center(child: Text('Tagged posts')),
],
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('MyApp')),
body: [
_buildHomePage(),
const Center(child: Text('π Search')),
_buildProfilePage(),
][_bottomIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: _bottomIndex,
onDestinationSelected: (i) => setState(() => _bottomIndex = i),
destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search_outlined), selectedIcon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.person_outlined), selectedIcon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
Pattern di atas:
NavigationBardi bawah mengontrol halaman utama (Home, Search, Profile)- Home page punya
TabBarsendiri (For You, Following) - Profile page juga punya tab (Posts, Bookmarks, Tagged)
- Setiap tab section pakai
DefaultTabControllerterpisah
4. Tips Praktis
TabController Manual (Advanced)
Kalau kamu perlu kontrol programmatic (misalnya auto-switch tab setelah action), pakai TabController manual:
class ManualTabScreen extends StatefulWidget {
const ManualTabScreen({super.key});
@override
State<ManualTabScreen> createState() => _ManualTabScreenState();
}
class _ManualTabScreenState extends State<ManualTabScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Manual TabController'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
],
),
),
body: TabBarView(
controller: _tabController,
children: const [
Center(child: Text('Page 1')),
Center(child: Text('Page 2')),
Center(child: Text('Page 3')),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Pindah ke tab berikutnya
_tabController.animateTo(
(_tabController.index + 1) % 3,
);
},
child: const Icon(Icons.arrow_forward),
),
);
}
}
IndexedStack β Preservasi State
Kalau pakai setState dengan list pages, setiap ganti tab state akan reset. Pakai IndexedStack supaya state tetap terjaga:
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
Kesimpulan
| Komponen | Gunanya |
|---|---|
NavigationBar / BottomNavigationBar |
Navigasi utama (Home, Search, Profile) |
TabBar + TabBarView |
Sub-navigasi dalam satu halaman |
DefaultTabController |
Controller otomatis untuk tab |
IndexedStack |
Preserve state saat switch tab |
Next: Di artikel #25 kita bahas Drawer & Navigation Drawer β cara bikin menu slide-out dari kiri. Stay tuned! π―
Coba sendiri! Share ke sosial media dan tag @ahsai001 π