Flutter: BottomNavigationBar ve NavigationBar Karşılaştırması
Son güncelleme:
6 dk okuma

Flutter'da alt navigasyon için iki widget var: Material 2 döneminden gelen BottomNavigationBar ve Material 3 ile eklenen NavigationBar. İkisi de aynı işi yapar, üç-beş üst düzey hedef arasında geçiş sağlar; fark görünüşte, API adlarında ve özelleştirme yaklaşımında. Bu yazı bir "nasıl kullanılır" yazısı değil, bir karar ve geçiş rehberi: hangi projede hangisini seçmelisiniz, mevcut bir çubuğu yenisine taşırken hangi özellik neye dönüşür ve geçişten sonra insanları en çok şaşırtan farklar nelerdir. currentIndex'i state'te tutmak, IndexedStack ile sayfa durumunu korumak gibi her iki widget için geçerli temel konuları BottomNavigationBar kullanımı yazısında ayrıntılı anlattım; burada tekrar etmeyeceğim.
Canlı Demo
Aşağıdaki interaktif örnekte alt navigasyonu deneyebilirsiniz:
💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.
İki Widget Yan Yana
Aynı üç hedef, iki farklı widget:
// Material 2 nesli
BottomNavigationBar(
currentIndex: index,
onTap: (value) => setState(() => index = value),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Ana Sayfa'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Arama'),
BottomNavigationBarItem(icon: Icon(Icons.person_outline), activeIcon: Icon(Icons.person), label: 'Profil'),
],
)
// Material 3 nesli
NavigationBar(
selectedIndex: index,
onDestinationSelected: (value) => setState(() => index = value),
destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Ana Sayfa'),
NavigationDestination(icon: Icon(Icons.search), label: 'Arama'),
NavigationDestination(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profil'),
],
)Yapı bire bir aynı; yalnızca kelimeler değişmiş. NavigationBar'ın "hedef" (destination) kelimesini seçmesi tesadüf değil: aynı sözlük NavigationRail ve NavigationDrawer'da da kullanılır, böylece üç widget arasında geçiş yapmak kolaylaşır. Bunun ne işe yaradığını senaryoda göreceğiz.
Özellik Eşleme Tablosu
Geçiş yaparken elinizin altında olması gereken tablo:
| BottomNavigationBar | NavigationBar | Not |
|---|---|---|
items |
destinations |
Liste türü değişir |
BottomNavigationBarItem |
NavigationDestination |
label her ikisinde de String |
currentIndex |
selectedIndex |
Yine State içinde tutulur |
onTap |
onDestinationSelected |
Aynı imza: void Function(int) |
activeIcon |
selectedIcon |
Verilmezse icon kullanılır |
type: fixed / shifting |
yok | NavigationBar her zaman eşit genişlikli çalışır |
showUnselectedLabels, showSelectedLabels |
labelBehavior |
Tek bir enum |
selectedItemColor, unselectedItemColor |
NavigationBarThemeData.iconTheme / labelTextStyle |
Duruma göre renk |
backgroundColor, elevation |
backgroundColor, elevation |
Aynı |
| yok | indicatorColor, indicatorShape |
Seçili öğenin arkasındaki hap şekli |
BottomNavigationBarItem.backgroundColor |
yok | Öğeye göre değişen arka plan M3'te yoktur |
Görünür Farklar
NavigationBar'ın seçili öğesinin arkasında "hap" biçiminde bir gösterge (indicator) vardır; rengi temanın secondaryContainer renginden gelir ve seçim değişince ikonun altında kayarak animasyonla hareket eder. BottomNavigationBar'da vurgu yalnızca ikon ve etiket rengiyle yapılır.
Yükseklik farklıdır: NavigationBar varsayılan olarak 80 birim yüksekliğindedir ve etiketler her zaman görünür; klasik çubuk daha alçaktır. Geçişten sonra "çubuk kalınlaştı" hissi buradan gelir. height parametresiyle düşürülebilir, ama Material 3 dokunma alanı önerileri için 80'in tercih edildiğini bilerek karar verin.
Etiket davranışı tek bir enum ile yönetilir:
NavigationBar(
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, // alwaysShow (varsayılan), alwaysHide
// ...
)shifting tipinin "sadece seçili etiket görünür" davranışı onlyShowSelected ile elde edilir; öğeye göre değişen arka plan rengi ise Material 3'te yoktur ve karşılığı da aranmamalıdır.
Tema ile Özelleştirme: NavigationBarThemeData
BottomNavigationBar'da renkleri doğrudan widget'a yazardık. NavigationBar ise WidgetStateProperty tabanlı bir tema bekler; seçili ve seçili olmayan durumlar tek bir fonksiyonda ayrılır:
final scheme = ColorScheme.fromSeed(seedColor: Colors.teal);
ThemeData(
colorScheme: scheme,
navigationBarTheme: NavigationBarThemeData(
height: 72,
indicatorColor: scheme.primaryContainer,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final selected = states.contains(WidgetState.selected);
return TextStyle(
fontSize: 12,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
);
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
final selected = states.contains(WidgetState.selected);
return IconThemeData(
color: selected ? scheme.onPrimaryContainer : scheme.onSurfaceVariant,
);
}),
),
)Bu ilk bakışta daha uzun görünür ama avantajı şu: uygulamadaki bütün NavigationBar'lar tek yerden yönetilir ve karanlık temada colorScheme değişince renkler kendiliğinden uyar. Widget üzerinde indicatorColor, backgroundColor gibi parametreler de vardır; temayı tek bir ekranda ezmek gerektiğinde onlar kullanılır.
Ne Zaman Hangisi?
- NavigationBar seç: yeni bir projeye başlıyorsanız ve
ThemeDataMaterial 3 ile çalışıyorsa (güncel Flutter sürümlerinde varsayılan budur). Tema renkleri, gösterge ve tipografi hazır gelir; tasarımcıyla Material 3 kılavuzu üzerinden konuşmak kolaylaşır. - NavigationBar seç: tablet ya da web desteği planlıyorsanız. Aynı hedef listesi
NavigationRail'e de verilebildiği için geniş ekranda yan çubuğa geçmek birkaç satır iş olur. - BottomNavigationBar'da kal: proje
useMaterial3: falseile Material 2 görünümünde çalışıyorsa ve yakın zamanda tema geçişi planlanmıyorsa. İki widget'ı karıştırmak uygulamada tutarsız bir görünüm yaratır. - BottomNavigationBar'da kal: öğeye göre değişen arka plan rengi (
shifting) tasarımın parçasıysa. M3'te bu davranış yoktur. - İkisini de kullanma: hedef sayısı beşi geçiyorsa. Fazlasını bir Drawer'a taşımak gerekir; Material 3 tarafında bunun karşılığı
NavigationDrawer'dır.
Kararı verdikten sonra iki widget'ta da aynı olan kısım unutulmamalı: seçili indeks State'te durur ve sayfa durumunu korumak için body bir IndexedStack olur.
Geçiş Adımları
Mevcut bir BottomNavigationBar'ı taşımak genellikle on dakikalık iştir:
itemslistesinidestinationsyapın; herBottomNavigationBarItem'ıNavigationDestination'a çeviripactiveIcon'ıselectedIconolarak yeniden adlandırın.currentIndex→selectedIndex,onTap→onDestinationSelected. State alanınız vesetStatemantığınız değişmez.typesatırını silin.showUnselectedLabels: falsekullanıyorsanız yerinelabelBehavior: onlyShowSelectedyazın.selectedItemColorveunselectedItemColordeğerleriniNavigationBarThemeDataiçindekiiconThemevelabelTextStyle'a taşıyın.- Uygulamayı açıp yüksekliğe bakın; gerekiyorsa
heightverin.
Sık Yapılan Hatalar
1. selectedItemColor arıyorsunuz, yok
Belirti: NavigationBar'da seçili ikonun rengini değiştirmek için parametre bulamıyorsunuz; indicatorColor yalnızca arkadaki hapı boyuyor.
Sebep: Material 3 renkleri duruma göre çözümlenen tema özellikleri üzerinden verir.
Çözüm: Yukarıdaki gibi NavigationBarThemeData.iconTheme'e WidgetStateProperty.resolveWith ile seçili ve seçili olmayan IconThemeData vermek. Aynı yaklaşım labelTextStyle için geçerlidir.
2. Geçişten sonra etiketler hep görünüyor
Belirti: Eski çubukta yalnızca seçili öğenin etiketi görünüyordu; yeni çubukta hepsinin etiketi var.
Sebep: NavigationBar'ın varsayılan labelBehavior değeri alwaysShow'dur; shifting'in gizleme davranışı otomatik taşınmaz.
Çözüm: labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected vermek. Erişilebilirlik açısından etiketleri göstermek genellikle daha iyidir; bu değişikliği bilinçli yapın.
3. selectedIndex aralık dışı assertion'ı
Belirti: Hedef listesi kullanıcı rolüne göre kısalınca uygulama selectedIndex ile ilgili bir assertion hatasıyla çöküyor.
Sebep: Kayıtlı indeks (örneğin 3) yeni listede yok; NavigationBar indeksin 0 ile destinations.length - 1 arasında olmasını ister.
Çözüm: Listeyi değiştirirken indeksi de sınırlamak: _selectedIndex = _selectedIndex.clamp(0, destinations.length - 1).
4. Aynı uygulamada iki nesil karışık
Belirti: Bir ekranda NavigationBar, başka bir kabukta BottomNavigationBar; kullanıcı geçişte iki farklı yükseklik ve vurgu stili görüyor.
Çözüm: Alt navigasyon uygulamada tek bir kabuk widget'ında yaşamalı. Geçişi ekran ekran değil, o tek kabukta bir kerede yapın.
Mini Senaryo: Telefonda NavigationBar, Tablette NavigationRail
NavigationBar'ın gerçek getirisi geniş ekranda ortaya çıkar. Aynı hedef listesini bir kez tanımlayıp dar ekranda alt çubuğa, geniş ekranda yan raya veriyoruz; seçili indeks ve sayfalar ortak. Eşiği nasıl seçeceğinizi LayoutBuilder yazısının breakpoint bölümünde anlatmıştım.
class AdaptiveShell extends StatefulWidget {
const AdaptiveShell({super.key});
@override
State<AdaptiveShell> createState() => _AdaptiveShellState();
}
class _AdaptiveShellState extends State<AdaptiveShell> {
int _selectedIndex = 0;
static const _destinations = [
(icon: Icons.home_outlined, selectedIcon: Icons.home, label: 'Ana Sayfa'),
(icon: Icons.search, selectedIcon: Icons.search, label: 'Arama'),
(icon: Icons.person_outline, selectedIcon: Icons.person, label: 'Profil'),
];
static const List<Widget> _pages = [HomePage(), SearchPage(), ProfilePage()];
void _select(int i) => setState(() => _selectedIndex = i);
@override
Widget build(BuildContext context) {
final wide = MediaQuery.sizeOf(context).width >= 600;
final body = IndexedStack(index: _selectedIndex, children: _pages);
if (wide) {
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: _selectedIndex,
onDestinationSelected: _select,
labelType: NavigationRailLabelType.all,
destinations: [
for (final d in _destinations)
NavigationRailDestination(
icon: Icon(d.icon),
selectedIcon: Icon(d.selectedIcon),
label: Text(d.label),
),
],
),
const VerticalDivider(width: 1),
Expanded(child: body),
],
),
);
}
return Scaffold(
body: body,
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: _select,
destinations: [
for (final d in _destinations)
NavigationDestination(
icon: Icon(d.icon),
selectedIcon: Icon(d.selectedIcon),
label: d.label,
),
],
),
);
}
}Hedefler Dart 3 record'ları olarak tek bir sabit listede duruyor; iki widget da bu listeden üretiliyor, böylece yeni bir sekme eklemek tek satırlık iş. Dikkat edilecek nokta IndexedStack'in her iki dalda da aynı nesne olması: sayfa State'leri telefon-tablet geçişinde (örneğin pencere yeniden boyutlandığında) korunur, çünkü widget ağacındaki konumu ve türü değişmez. NavigationRail'in label'ı bir Widget, NavigationDestination'ınki bir String ister; küçük ama derleme hatası veren bir farktır.
Sık Sorulan Sorular
BottomNavigationBar deprecated mı, mutlaka geçmeli miyim?
Hayır, klasik widget çalışmaya devam ediyor ve kaldırılmadı. Material 3 kullanan yeni projelerde NavigationBar doğal seçimdir; çalışan bir Material 2 uygulamasını sırf bu yüzden taşımak zorunda değilsiniz.
NavigationBar'da seçili ikonun rengi nasıl değiştirilir?
selectedItemColor karşılığı yoktur. ThemeData.navigationBarTheme içinde iconTheme ve labelTextStyle alanlarına WidgetStateProperty.resolveWith ile WidgetState.selected durumuna göre farklı değerler verilir; hap rengi için indicatorColor kullanılır.
NavigationBar'da shifting tipi var mı?
Yok. Etiket gizleme davranışı labelBehavior: onlyShowSelected ile sağlanır; öğeye göre değişen arka plan rengi Material 3'te bulunmaz.
Geçişte IndexedStack ve state mantığı değişir mi?
Hayır. Seçili indeks yine State'te tutulur, sayfalar yine IndexedStack ile korunur; yalnızca çubuğun parametre adları değişir.
İlgili Yazılar
Flutter: BottomNavigationBar Kullanımı
Flutter BottomNavigationBar: currentIndex ve state, IndexedStack ile sayfa durumu koruma, fixed/shifting farkı, kaybolan öğeler hatası ve sepet rozeti örneği.
Flutter: AppBar Widget ve Özelleştirme
Flutter AppBar rehberi: title, leading, actions ve bottom kullanımı, Material 3'te kaydırınca renk değişimi ve sık yapılan hataların çözümü.
Flutter: Theme ve ThemeData ile Açık ve Koyu Tema
Flutter'da ThemeData ile tema kurmak: ColorScheme.fromSeed, açık ve koyu tema, ThemeMode, Theme.of ile okuma, TextTheme, bileşen temaları ve ThemeExtension.