İçeriğe geç / Skip to content / Zum Inhalt

Flutter: BottomNavigationBar ve NavigationBar Karşılaştırması

Ahmet Balaman

Son güncelleme:

6 dk okuma

FlutterBottomNavigationBarNavigationBarMaterial 3MigrationNavigation
Flutter: BottomNavigationBar ve NavigationBar Karşılaştırması

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.

Material 2 çubuğu seçiliyi yalnızca renkle vurgular, Material 3 çubuğu seçili ikonun arkasına hap biçimli bir gösterge koyar

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 ThemeData Material 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: false ile 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:

  1. items listesini destinations yapın; her BottomNavigationBarItem'ı NavigationDestination'a çevirip activeIcon'ı selectedIcon olarak yeniden adlandırın.
  2. currentIndex → selectedIndex, onTap → onDestinationSelected. State alanınız ve setState mantığınız değişmez.
  3. type satırını silin. showUnselectedLabels: false kullanıyorsanız yerine labelBehavior: onlyShowSelected yazın.
  4. selectedItemColor ve unselectedItemColor değerlerini NavigationBarThemeData içindeki iconTheme ve labelTextStyle'a taşıyın.
  5. Uygulamayı açıp yüksekliğe bakın; gerekiyorsa height verin.

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.

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.

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.

Yorumlar