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

Flutter: BottomNavigationBar Kullanımı

Ahmet Balaman

Son güncelleme:

6 dk okuma

FlutterBottomNavigationBarIndexedStackNavigationMaterialUI
Flutter: BottomNavigationBar Kullanımı

BottomNavigationBar, uygulamanın ana bölümleri arasında geçişi sağlayan, ekranın altında duran sekme çubuğudur: Ana Sayfa, Arama, Sepet, Profil gibi birbirinden bağımsız ve eşit öneme sahip üç-beş hedef. Üstteki TabBar'dan farkı, kaydırma hareketine tepki vermemesi ve tek bir sayfanın alt bölümlerini değil, uygulamanın en üst düzey alanlarını temsil etmesidir. Bu yazıda widget'ı nasıl doğru kurduğumuzu, sekme değişince sayfa durumunun neden kaybolduğunu ve dört öğede aniden "kaybolan" ikon sorununu ele alacağım.

Canlı Demo

Aşağıdaki interaktif örnekte bu widget'ı deneyebilirsiniz:

💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.

Temel Kullanım

Üç parça gerekir: seçili sekmeyi tutan bir int, currentIndex olarak bu değeri okuyan çubuk ve onTap içinde değeri güncelleyen bir setState. Bu üçlüden biri eksikse çubuk ya hiç tepki vermez ya da vurgu hep ilk sekmede kalır.

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _selectedIndex = 0;

  static const List<Widget> _pages = [
    HomePage(),
    SearchPage(),
    ProfilePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages[_selectedIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Ana Sayfa',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Arama',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profil',
          ),
        ],
      ),
    );
  }
}

BottomNavigationBar'ın kendisi hiçbir sayfa değiştirmez; sadece "kullanıcı 2. öğeye dokundu" der. Hangi sayfanın görüneceğine body karar verir. Bu ayrım önemli, çünkü sayfa geçişini Navigator.push ile yapmaya kalkmak yaygın bir hatadır: sekmeler yığın üzerine yeni sayfa açmaz, aynı Scaffold içinde içerik değiştirir. Yığın tabanlı geçişler için Navigator yazısına bakabilirsiniz.

Önemli Özellikler

Özellik Açıklama
currentIndex Seçili öğenin indeksi; State içinde tutulmalı
onTap Dokunulan öğenin indeksini veren callback
items En az iki BottomNavigationBarItem
type fixed ya da shifting
selectedItemColor Seçili öğe rengi
unselectedItemColor Seçili olmayan öğe rengi
showUnselectedLabels Seçili olmayan etiketleri göster
backgroundColor Arka plan rengi (fixed tipinde geçerli)

fixed ve shifting Farkı

type parametresi çubuğun davranışını belirler ve varsayılan değeri öğe sayısına bağlıdır: üç ve daha az öğede fixed, dörtten itibaren shifting. Bu kural yeni başlayanları en çok şaşırtan ayrıntıdır.

// fixed: bütün öğeler eşit genişlikte, etiketler her zaman görünür
BottomNavigationBar(
  type: BottomNavigationBarType.fixed,
  // ...
)

// shifting: yalnızca seçili öğenin etiketi görünür, arka plan öğeye göre değişir
BottomNavigationBar(
  type: BottomNavigationBarType.shifting,
  items: const [
    BottomNavigationBarItem(
      icon: Icon(Icons.home),
      label: 'Ana Sayfa',
      backgroundColor: Colors.indigo, // shifting'de her öğe kendi arka planını verir
    ),
    // ...
  ],
)

shifting tipinde seçili olmayan etiketler gizlenir ve çubuğun arka planı seçili öğenin backgroundColor'ından gelir. Öğelere renk vermezseniz sonuç, hatalar bölümünde anlatacağım "beyaz üstüne beyaz" görünümüdür. Uygulamaların büyük kısmı için fixed doğru seçimdir; dört ya da beş öğe kullanıyorsanız bunu açıkça yazın.

Aktif ve Pasif İkon

Material'in önerdiği desen, seçili öğede dolu, diğerlerinde çizgili ikon kullanmaktır:

BottomNavigationBarItem(
  icon: Icon(Icons.home_outlined), // Pasif ikon
  activeIcon: Icon(Icons.home),    // Aktif ikon
  label: 'Ana Sayfa',
)

IndexedStack ile Sayfa Durumunu Koruma

Yukarıdaki _pages[_selectedIndex] yaklaşımının bir bedeli var: sekme değiştiğinde önceki sayfa widget ağacından çıkarılır, State nesnesi dispose edilir. Arama sayfasında yazdığınız metin, listede kaydırdığınız konum, açtığınız filtre hepsi sıfırlanır; geri döndüğünüzde sayfa sanki ilk kez açılıyormuş gibi initState çalıştırır. Bunun neden böyle olduğunu yaşam döngüsü yazısında anlatmıştım.

body’ye tek sayfa verildiğinde diğerlerinin State nesnesi silinir, IndexedStack ise hepsini ağaçta tutup yalnızca seçiliyi çizer

IndexedStack bütün çocukları ağaçta tutar, yalnızca index'teki çocuğu çizer:

body: IndexedStack(
  index: _selectedIndex,
  children: const [
    HomePage(),
    SearchPage(),
    ProfilePage(),
  ],
)

Bedeli şu: bütün sayfalar uygulama açılır açılmaz oluşturulur. Her sayfa initState içinde ağ isteği atıyorsa, dördü de aynı anda çalışır. Sayfalar hafifse bu sorun değildir; ağırsa veri yüklemeyi sayfa ilk kez görünür olduğunda tetiklemek ya da yalnızca ziyaret edilmiş sekmeleri oluşturan küçük bir yapı kurmak gerekir.

Badge Ekleme

Bildirim ya da sepet sayısını ikonun köşesinde göstermek için Material 3'ün Badge widget'ı yeterlidir:

BottomNavigationBarItem(
  icon: Badge(
    label: Text('3'),
    child: Icon(Icons.notifications),
  ),
  label: 'Bildirimler',
)

Sayı sıfırken rozeti gizlemek için Badge.count(count: n, isLabelVisible: n > 0, child: ...) kullanılır; senaryoda göreceğiz.

Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?

  • Kullan: uygulamanın üç ile beş arasında, birbirinden bağımsız ve her an erişilmesi gereken üst düzey alanı varsa.
  • Kullanma: beşten fazla hedef varsa. Öğeler daralır, etiketler okunmaz olur. Fazlasını bir Drawer'a ya da "Daha Fazla" sekmesinin altına taşımak gerekir.
  • Kullanma: hedefler tek bir ekranın alt bölümleriyse (bir ürünün "Açıklama / Yorumlar / Sorular" gibi). Bu iş üstteki TabBar'ındır.
  • Kullanma: akış sıralıysa (adım 1, adım 2, ödeme). Sıralı akış Navigator ile yığın üzerinde ilerler; sekmeler arasında serbest dolaşım bu akışı bozar.
  • Düşün: projeniz Material 3 kullanıyorsa aynı işi gören NavigationBar var. Hangisini seçeceğinizi ve geçişin nasıl yapıldığını BottomNavigationBar ve NavigationBar karşılaştırması yazısında ayrı ele aldım. Tablet ve masaüstünde ise alt çubuk yerine yan tarafta NavigationRail tercih edilir.

Sık Yapılan Hatalar

1. Dördüncü öğeyi ekleyince ikonlar kayboldu

Belirti: Üç öğede her şey yolundayken dördüncü öğeyi ekleyince etiketler gizleniyor, ikonlar açık temada beyaz arka plan üstünde beyaza dönüp görünmez oluyor.

Sebep: Dörtten itibaren varsayılan type shifting'e geçer. Bu tipte öğe rengi temanın yüzey rengine (açık temada beyaz) döner ve arka plan öğelerin backgroundColor'ından alınır; siz vermediyseniz ikonlar arka planla aynı renkte kalır.

Çözüm: type: BottomNavigationBarType.fixed yazmak. Gerçekten shifting istiyorsanız her BottomNavigationBarItem'a bir backgroundColor verin.

2. Vurgu hep ilk sekmede kalıyor

Belirti: Dokununca sayfa değişiyor ama çubukta seçili görünen öğe hiç kımıldamıyor.

Sebep: currentIndex: 0 sabit yazılmış ya da onTap içinde setState çağrılmamış. Çubuk kendi seçimini hatırlamaz; her build'de currentIndex ne diyorsa onu gösterir.

Çözüm: İndeksi State içindeki bir alanda tutup currentIndex'e o alanı vermek ve onTap içinde setState ile güncellemek.

3. "items.length >= 2" assertion hatası

Belirti: Uygulama açılırken 'items.length >= 2': is not true mesajıyla çöküyor.

Sebep: Öğeler bir listeden dinamik üretiliyor ve liste boş ya da tek elemanlı geldi; widget en az iki öğe ister.

Çözüm: Liste iki öğeden kısaysa çubuğu hiç göstermemek (bottomNavigationBar: items.length >= 2 ? BottomNavigationBar(...) : null).

4. Sekme değişince arama metni ve kaydırma konumu sıfırlanıyor

Belirti: Arama sekmesinde bir şey yazıp Profil'e geçip geri dönünce alan boş.

Sebep: body: _pages[_selectedIndex] her geçişte önceki sayfayı yok eder.

Çözüm: body'yi yukarıdaki gibi IndexedStack ile kurmak.

Mini Senaryo: Sepet Rozetli Alışveriş Kabuğu

Dört sekmeli bir alışveriş uygulaması düşünelim: Ana Sayfa, Arama, Sepet ve Profil. Ana sayfadan sepete ürün eklenince Sepet ikonundaki rozet güncellenmeli, sekmeler arasında dolaşınca sayfalar durumunu korumalı ve Android'de geri tuşu uygulamayı kapatmak yerine önce Ana Sayfa'ya dönmeli.

class ShopShell extends StatefulWidget {
  const ShopShell({super.key});

  @override
  State<ShopShell> createState() => _ShopShellState();
}

class _ShopShellState extends State<ShopShell> {
  int _selectedIndex = 0;
  int _cartCount = 0;

  void _addToCart() => setState(() => _cartCount++);

  @override
  Widget build(BuildContext context) {
    final cartIcon = Badge.count(
      count: _cartCount,
      isLabelVisible: _cartCount > 0,
      child: const Icon(Icons.shopping_cart_outlined),
    );

    return PopScope(
      canPop: _selectedIndex == 0, // Sadece Ana Sayfa'dayken uygulamadan çıkılabilir
      onPopInvokedWithResult: (didPop, _) {
        if (!didPop) setState(() => _selectedIndex = 0);
      },
      child: Scaffold(
        body: IndexedStack(
          index: _selectedIndex,
          children: [
            HomePage(onAddToCart: _addToCart),
            const SearchPage(),
            CartPage(count: _cartCount),
            const ProfilePage(),
          ],
        ),
        bottomNavigationBar: BottomNavigationBar(
          type: BottomNavigationBarType.fixed, // 4 öğe: shifting'e düşmesin
          currentIndex: _selectedIndex,
          onTap: (i) => setState(() => _selectedIndex = i),
          items: [
            const BottomNavigationBarItem(
              icon: Icon(Icons.home_outlined),
              activeIcon: Icon(Icons.home),
              label: 'Ana Sayfa',
            ),
            const BottomNavigationBarItem(
              icon: Icon(Icons.search),
              label: 'Arama',
            ),
            BottomNavigationBarItem(
              icon: cartIcon,
              label: 'Sepet',
            ),
            const BottomNavigationBarItem(
              icon: Icon(Icons.person_outline),
              activeIcon: Icon(Icons.person),
              label: 'Profil',
            ),
          ],
        ),
      ),
    );
  }
}

Sepet sayısı kabuk widget'ın State'inde duruyor; HomePage bir callback ile artırıyor, CartPage ve rozet aynı değeri okuyor. Sayfalar büyüdükçe bu paylaşımı Provider gibi bir durum yönetimi katmanına taşımak doğal bir sonraki adımdır, ama desen aynı kalır: sekme çubuğu yalnızca indeks bilir, veriyi bilmez. PopScope'taki canPop ifadesi Ana Sayfa dışındayken geri tuşunu yakalar ve kullanıcıyı önce ilk sekmeye getirir; Android'de kullanıcıların beklediği davranış budur. Son bir dokunuş olarak, zaten seçili olan sekmeye tekrar dokunulduğunda listeyi en üste kaydırmak için onTap içinde i == _selectedIndex durumunu ayrıca ele alabilirsiniz.

Sık Sorulan Sorular

BottomNavigationBar'da dörtten fazla öğe olunca ikonlar neden kayboluyor?

Öğe sayısı üçü geçince type varsayılan olarak shifting olur; bu tipte etiketler gizlenir ve renkler öğelerin backgroundColor değerine bağlanır. type: BottomNavigationBarType.fixed vererek klasik görünüme dönebilirsiniz.

Sekme değiştirince sayfanın durumu nasıl korunur?

body içinde sayfayı doğrudan seçmek yerine IndexedStack kullanın. Bütün sayfalar ağaçta kalır, yalnızca seçili olan çizilir; böylece metin alanları ve kaydırma konumu korunur.

Sekmeler arasında geçişte Navigator.push kullanmalı mıyım?

Hayır. Sekmeler aynı Scaffold içinde içeriği değiştirir; Navigator.push yığına yeni sayfa ekler ve alt çubuk yeni sayfada görünmez. currentIndex'i güncelleyip body'yi değiştirmek yeterlidir.

En fazla kaç öğe olmalı?

Material rehberi üç ile beş arasını önerir. Daha fazla hedef varsa bir kısmını Drawer'a ya da "Daha Fazla" sekmesine taşımak okunabilirliği korur.

Yorumlar