Flutter: TabBar ve TabBarView Kullanımı
Son güncelleme:
4 dk okuma

TabBar ve TabBarView, aynı ekranın kardeş içeriklerini (Aktif / Tamamlanan / İptal gibi) yan yana sayfalar olarak sunar. Kullanıcı sekmeye dokunarak ya da sağa-sola kaydırarak geçiş yapar; ikisini senkron tutan parça ise TabController'dır. Asıl karar da burada verilir: controller'ı Flutter mı yönetsin, siz mi? Bu yazı o kararı, sekme içeriğinin durumunu korumayı ve en sık karşılaşılan hataları anlatıyor.
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.
DefaultTabController ile Basit Kullanım
Sekmeler yalnızca içerik değiştiriyorsa en kısa yol DefaultTabController'dır. StatefulWidget yazmanız, dispose düşünmeniz gerekmez; TabBar ve TabBarView controller'ı ağaçtan kendileri bulur:
DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Tabs'),
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.home), text: 'Ana Sayfa'),
Tab(icon: Icon(Icons.search), text: 'Arama'),
Tab(icon: Icon(Icons.person), text: 'Profil'),
],
),
),
body: const TabBarView(
children: [
Center(child: Text('Ana Sayfa')),
Center(child: Text('Arama')),
Center(child: Text('Profil')),
],
),
),
)TabBar'ın AppBar'ın bottom alanına yerleştiğine dikkat edin; Tab yalnızca ikon, yalnızca metin ya da ikisini birden alabilir. Alt ağaçtan sekme değiştirmek gerekirse DefaultTabController.of(context).animateTo(2) çağrısı yeterlidir.
Kendi TabController'ınızı Yönetmek
Aktif sekmeye göre başka bir şeyi değiştirecekseniz (FAB'ı gizlemek, başlığı güncellemek, sekmeyi koddan seçmek) controller'ı kendiniz oluşturun. Controller animasyon sürdüğü için bir vsync ister; bunu SingleTickerProviderStateMixin sağlar:
class _MyPageState extends State<MyPage>
with SingleTickerProviderStateMixin {
late final TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
}Aynı _tabController'ı hem TabBar'a hem TabBarView'a controller olarak verirsiniz. initState ve dispose ikilisinin neden birlikte yazıldığını yaşam döngüsü yazısında ayrıntılı anlattım.
// Belirli bir sekmeye git
_tabController.animateTo(2);
// Aktif sekmenin index'i
final current = _tabController.index;TabBar Özellikleri
| Özellik | Açıklama |
|---|---|
tabs |
Tab widget listesi |
controller |
TabController (verilmezse DefaultTabController aranır) |
isScrollable |
Sekmeler sığmadığında yatay kaydırmayı açar |
tabAlignment |
Sekmelerin hizası (start, center, fill, startOffset) |
indicatorColor / indicatorWeight |
Alt çizginin rengi ve kalınlığı |
indicatorSize |
Çizgi sekme kadar mı, etiket kadar mı olsun |
labelColor / unselectedLabelColor |
Seçili ve seçili olmayan sekme rengi |
dividerColor |
Material 3'te çubuğun altındaki ince ayırıcı çizgi |
Kaydırılabilir Sekmeler
Beş-altı sekmeden fazlası ekrana sığmaz; isScrollable: true ile çubuğu yatay kaydırılabilir yapın:
const TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
tabs: [
Tab(text: 'Tümü'),
Tab(text: 'Flutter'),
Tab(text: 'Swift'),
Tab(text: 'Algoritma'),
Tab(text: '.NET'),
],
)tabAlignment satırı süs değil; nedenini hatalar bölümünde göreceksiniz.
Sekme Durumunu Korumak
TabBarView ekranda olmayan sekmeleri bellekten atar. Kullanıcı bir listede aşağı inip başka sekmeye geçer ve geri dönerse liste başa sarmış olur; form alanları da sıfırlanır. Sekmenin state'ine AutomaticKeepAliveClientMixin ekleyerek bunu önlersiniz:
class _OrderListState extends State<OrderList>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // mixin bunu şart koşar
return ListView.builder(
itemCount: 30,
itemBuilder: (context, index) => ListTile(
title: Text('${widget.status} sipariş #${index + 1}'),
),
);
}
}Bunu her sekmeye refleks olarak eklemeyin; canlı tutulan her sekme bellekte yer kaplar. Durumu kaybetmenin kullanıcıyı rahatsız ettiği sekmelerde kullanın.
Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?
Sekmeler, aynı bağlamın iki ila altı kardeş görünümü için uygundur: bir siparişin durumları, bir profilin gönderileri ve beğenileri gibi.
- Uygulamanın ana bölümleri (Ana Sayfa, Arama, Profil) için sekme değil BottomNavigationBar kullanın; alt çubuk başparmağa yakındır ve ekranlar arasında sabit kalır.
- Sayfa değiştirmeyen basit bir filtre (Liste / Izgara) için
SegmentedButtondaha hafiftir. - Adımlar sırayla tamamlanacaksa (adres, ödeme, onay) sekme yanlış mesaj verir;
Stepperya da sayfa sayfa bir akış kurun. - Sekme içeriğinde yatay kaydırılan bir galeri ya da harita varsa
TabBarView'ın kaydırma hareketiyle çakışır. Bu durumdaTabBarView'aphysics: const NeverScrollableScrollPhysics()verip geçişi yalnızca dokunmaya bırakabilirsiniz.
Sık Yapılan Hatalar
1. No TabController for TabBar. TabBar'a controller vermediniz ve üstünde DefaultTabController da yok. Ya Scaffold'u DefaultTabController ile sarın ya da kendi controller'ınızı hem TabBar'a hem TabBarView'a verin. Birine verip diğerini unutmak da aynı hatayı üretir.
2. Controller's length property (3) does not match the number of tabs (2) present in TabBar's tabs property. length değeri, tabs sayısı ve TabBarView'daki children sayısı üçü de aynı olmalı. Yeni sekme eklerken bu üç yeri birlikte güncelleyin; sayıyı bir listeden türetmek (length: _tabs.length) hatayı kökten çözer.
3. dispose unutmak ya da yanlış mixin. Controller'ı dispose etmezseniz ticker'ı ve listener'ları serbest bırakılmaz; sayfa kapansa da bellekte kalır. Aynı state içinde ikinci bir AnimationController da varsa SingleTickerProviderStateMixin "multiple tickers were created" hatası verir; o durumda TickerProviderStateMixin kullanın.
4. Listener iki kez tetikleniyor. Sekmeye dokunulduğunda addListener geri çağrısı hem animasyon başlarken hem biterken çalışır. Tek seferlik iş yapacaksanız indexIsChanging ile süzün:
void _onTabChanged() {
if (_tabController.indexIsChanging) return; // animasyon bitince çalış
setState(() {});
}5. Material 3'te kaydırılabilir sekmelerin solunda boşluk. isScrollable: true olduğunda varsayılan hiza TabAlignment.startOffset'tir ve ilk sekmeyi içeriden başlatır. Sekmeler kenardan başlasın istiyorsanız tabAlignment: TabAlignment.start yazın. Çubuğun altındaki ince çizgi de Material 3 ile gelir; istemiyorsanız dividerColor: Colors.transparent verin.
Mini Senaryo: Sipariş Ekranı
Siparişlerim ekranında üç sekme olsun. "Yeni sipariş" butonu yalnızca Aktif sekmesinde görünsün ve kullanıcı sekme değiştirip döndüğünde liste kaldığı yerde dursun. İlk istek kendi controller'ımızı, ikincisi keep-alive'ı gerektiriyor:
class OrdersPage extends StatefulWidget {
const OrdersPage({super.key});
@override
State<OrdersPage> createState() => _OrdersPageState();
}
class _OrdersPageState extends State<OrdersPage>
with SingleTickerProviderStateMixin {
late final TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(_onTabChanged);
}
void _onTabChanged() {
if (_tabController.indexIsChanging) return; // animasyon bitince çalış
setState(() {}); // FAB görünürlüğünü güncelle
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Siparişlerim'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Aktif'),
Tab(text: 'Tamamlanan'),
Tab(text: 'İptal'),
],
),
),
body: TabBarView(
controller: _tabController,
children: const [
OrderList(status: 'Aktif'),
OrderList(status: 'Tamamlanan'),
OrderList(status: 'İptal'),
],
),
floatingActionButton: _tabController.index == 0
? FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
)
: null,
);
}
}OrderList, yukarıdaki keep-alive bölümünde yazdığımız widget'tır. Listener yalnızca geçiş tamamlandığında setState çağırıyor, böylece FloatingActionButton animasyonun ortasında değil, yeni sekme oturduğunda değişiyor. dispose controller'ı kapatırken ona bağlı listener da onunla birlikte gidiyor.
Sık Sorulan Sorular
DefaultTabController mı, kendi TabController'ım mı?
Sekmeler yalnızca içerik değiştiriyorsa DefaultTabController yeterlidir ve daha az kod ister. Aktif index'e göre arayüzün başka bir yerini değiştirecek ya da sekmeyi koddan seçecekseniz kendi TabController'ınızı oluşturun.
Sekmeler arasında kaydırmayı nasıl kapatırım?
TabBarView'a physics: const NeverScrollableScrollPhysics() verin. Sekmeye dokunarak geçiş çalışmaya devam eder.
Sekme değişince içerik neden sıfırlanıyor?
TabBarView görünmeyen sayfaları ağaçtan çıkarır, state'leri de onlarla birlikte gider. Sekmenin state sınıfına AutomaticKeepAliveClientMixin ekleyip wantKeepAlive için true döndürün.
TabBar'ı AppBar dışında kullanabilir miyim?
Evet. TabBar sıradan bir widget'tır; bir Column içine koyup altına Expanded ile sarılmış bir TabBarView yerleştirebilirsiniz. İkisinin aynı controller'ı görmesi yeterlidir.
İlgili Yazılar
Flutter: Drawer Widget ve Yan Menü Kullanımı
Flutter Drawer ile yan menü: temel kurulum, Material 3 NavigationDrawer, menüyü kapatıp sayfaya geçme ve Scaffold.of context hatasının çözümü.
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: BottomNavigationBar ve NavigationBar Karşılaştırması
BottomNavigationBar'dan Material 3 NavigationBar'a geçiş: özellik eşleme tablosu, labelBehavior, karar rehberi ve uyarlanabilir kabuk örneği.