Flutter'da Navigator ile Sayfalar Arası Geçiş ve Veri Transferi
Son güncelleme:
6 dk okuma

Web geliştirmeden gelenlerin Flutter'da ilk sorduğu şeylerden biri şu: "Link yoksa sayfalar arasında nasıl geçiliyor?" Cevap Navigator.
Flutter'da sayfalar arası geçiş yapmak için Navigator sınıfı kullanılır. Bir çeşit yığın (stack) yapısı gibi çalışır: Sayfa açarsınız üste eklenir, geri tuşuna basarsınız üstteki sayfa çıkar. Widget ağacı, Scaffold ve BuildContext gibi kavramlar henüz oturmadıysa önce widget ağacı ve layout yazısına göz atmanızı öneririm; buradaki her şey onların üstüne kuruluyor.
Canlı Demo: Navigator Kullanımı
Sayfalar arası geçiş ve veri transferini interaktif olarak deneyin:
💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.
Temel Navigator Kullanımı
En basit haliyle bir sayfadan diğerine geçiş:
// Ana sayfa
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Ana Sayfa')),
body: Center(
child: ElevatedButton(
child: Text('Detay Sayfasına Git'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
);
},
),
),
);
}
}
// Detay sayfası
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detay Sayfa')),
body: Center(
child: Text('Bu detay sayfası'),
),
);
}
}Navigator.push() yeni bir sayfa açar. MaterialPageRoute sayfa geçiş animasyonunu sağlar.
Geri Dönüş - pop()
Geri dönmek için iki yol var:
1. Otomatik Geri Tuşu
AppBar kullanırsanız Flutter otomatik olarak geri tuşu ekler. Kullanıcı buna tıklayınca geri döner.
2. Kod ile Geri Dönüş
ElevatedButton(
child: Text('Geri Dön'),
onPressed: () {
Navigator.pop(context);
},
)Navigator.pop() üstteki sayfayı stack'ten çıkarır.
Sayfalar Arası Veri Gönderme
Asıl güzellik buradan başlıyor. Sayfaya veri göndermek için constructor kullanıyoruz.
Veri Gönderen Sayfa
class ProductListPage extends StatelessWidget {
const ProductListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Ürünler')),
body: ListView(
children: [
ListTile(
title: Text('iPhone 15'),
subtitle: Text('50.000 TL'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
productName: 'iPhone 15',
price: 50000,
description: 'Yeni nesil akıllı telefon',
),
),
);
},
),
ListTile(
title: Text('Samsung S24'),
subtitle: Text('45.000 TL'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
productName: 'Samsung S24',
price: 45000,
description: 'Android\'in en iyisi',
),
),
);
},
),
],
),
);
}
}Veri Alan Sayfa
class ProductDetailPage extends StatelessWidget {
final String productName;
final int price;
final String description;
const ProductDetailPage({
super.key,
required this.productName,
required this.price,
required this.description,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(productName)),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
productName,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'${price.toString()} TL',
style: TextStyle(fontSize: 20, color: Colors.green),
),
SizedBox(height: 16),
Text(
description,
style: TextStyle(fontSize: 16),
),
],
),
),
);
}
}Constructor'da required ile parametreleri zorunlu kılıyoruz. Böylece sayfa açılırken mutlaka bu bilgiler verilmeli.
Geri Dönerken Veri Döndürme
Bazen açtığınız sayfadan veri almak istersiniz. Mesela kullanıcı bir şey seçti, bu seçimi önceki sayfaya göndermek istiyorsunuz.
Veri İsteyen Sayfa
class SelectCityPage extends StatelessWidget {
const SelectCityPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Şehir Seç')),
body: Center(
child: ElevatedButton(
child: Text('Şehir Seç'),
onPressed: () async {
// Sayfa aç ve sonuç bekle
final selectedCity = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const CityListPage()),
);
// Beklerken sayfa kapanmış olabilir; context'i kullanmadan önce kontrol et
if (!context.mounted) return;
// Sonuç geldi
if (selectedCity != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Seçilen: $selectedCity')),
);
}
},
),
),
);
}
}Veri Döndüren Sayfa
class CityListPage extends StatelessWidget {
const CityListPage({super.key});
static const List<String> cities = [
'İstanbul',
'Ankara',
'İzmir',
'Antalya',
'Bursa',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Şehir Listesi')),
body: ListView.builder(
itemCount: cities.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(cities[index]),
onTap: () {
// Seçilen şehri geri gönder
Navigator.pop(context, cities[index]);
},
);
},
),
);
}
}Navigator.pop(context, deger) ile ikinci parametre olarak veri gönderiyoruz. Navigator.push<String> yazarak dönecek verinin tipini de belirtiyoruz; kullanıcı seçim yapmadan geri tuşuyla çıkarsa sonuç null olur.
await satırına dikkat: sonuç beklenirken kullanıcı sayfadan çıkmış olabilir. Bu yüzden bekleme sonrasında context'i kullanmadan önce context.mounted kontrolü yapıyoruz. Flutter'ın use_build_context_synchronously lint kuralı da bu kontrol eksikse uyarı verir.
pushReplacement - Geri Dönüşü Engelleme
Bazen kullanıcının geri tuşu ile önceki sayfaya dönmesini istemezsiniz. Tipik örnek: Giriş yaptıktan sonra login sayfasına geri dönülmesin.
push() ile Normal Geçiş
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
// Kullanıcı geri tuşuna basarsa login sayfasına dönerpushReplacement() ile Geçiş
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
// Geri tuşu stack'teki bir önceki sayfaya gider, login atlanırPratik Örnek: Login Sonrası
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
void _login(BuildContext context) {
// Login işlemi başarılı
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
child: Text('Giriş Yap'),
onPressed: () => _login(context),
),
),
);
}
}Artık HomePage'deyken geri tuşuna basarsa LoginPage'e dönmez, uygulamadan çıkar.
Kayıt Sonrası Örnek
class RegisterPage extends StatelessWidget {
const RegisterPage({super.key});
void _register(BuildContext context) {
// Kayıt işlemi başarılı
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Kayıt başarılı! Giriş yapabilirsiniz.')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Kayıt Ol')),
body: Center(
child: ElevatedButton(
child: Text('Kaydı Tamamla'),
onPressed: () => _register(context),
),
),
);
}
}pushAndRemoveUntil - Tüm Stack'i Temizleme
Bazen sadece bir sayfayı değil, tüm stack'i temizlemek istersiniz.
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
(route) => false, // Tüm sayfaları kaldır
);Bu, kullanıcıyı tamamen yeni bir akışa yönlendirmek için kullanılır. Örneğin çıkış yapınca tüm sayfalar temizlenir, sadece login ekranı kalır.
Çıkış Yapma Örneği
void logout(BuildContext context) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
(route) => false,
);
}Named Routes (İsimlendirilmiş Rotalar)
Daha büyük projelerde her seferinde MaterialPageRoute yazmak zahmetli. İsimlendirilmiş rotalar kullanabilirsiniz.
Not: Flutter'ın resmi dokümantasyonu, isimlendirilmiş rotaları çoğu uygulama için önermiyor. Derin bağlantı (deep link) geldiğinde davranış özelleştirilemiyor ve web'de tarayıcının ileri tuşu desteklenmiyor. Bu ihtiyaçlar varsa go_router gibi bir yönlendirme paketi daha doğru seçim. Yine de mevcut projelerde sık karşılaşacağınız için nasıl çalıştığını bilmekte fayda var.
main.dart'ta Rota Tanımlama
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/detail': (context) => const DetailPage(),
'/profile': (context) => const ProfilePage(),
'/settings': (context) => const SettingsPage(),
'/login': (context) => const LoginPage(),
},
);
}
}İsimlendirilmiş Rota ile Geçiş
// Normal geçiş
Navigator.pushNamed(context, '/detail');
// Geri dönüşü engelleme
Navigator.pushReplacementNamed(context, '/');
// Stack temizleme
Navigator.pushNamedAndRemoveUntil(
context,
'/login',
(route) => false,
);İsimlendirilmiş Rotada Veri Gönderme
// Veri gönderme
Navigator.pushNamed(
context,
'/detail',
arguments: {
'id': 123,
'name': 'Ahmet',
},
);
// Veri alma
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map;
return Scaffold(
appBar: AppBar(title: Text(args['name'])),
body: Center(
child: Text('ID: ${args['id']}'),
),
);
}
}Navigation ile Animasyon
Farklı geçiş animasyonları ekleyebilirsiniz. İki sayfada ortak bir görselin sayfadan sayfaya akarak taşınmasını istiyorsanız bunun ayrı bir aracı var: Hero animasyonu.
Sağdan Sola Kayma (Varsayılan)
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
);Aşağıdan Yukarı
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailPage(),
fullscreenDialog: true,
),
);Özel Animasyon
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => const DetailPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(
CurveTween(curve: curve),
);
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
),
);Geri Tuşunu Yakalamak: PopScope
Kullanıcı sayfadan çıkmak istediğinde araya girmek (örneğin kaydedilmemiş değişiklik varsa onay sormak) için PopScope kullanılır. Android'in geri tuşu ve geri hareketi, iOS'taki kaydırarak geri dönme ve AppBar'daki geri oku bu kontrolden geçer:
class MyPage extends StatelessWidget {
const MyPage({super.key});
Future<bool?> _showExitDialog(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Çıkmak istediğinize emin misiniz?'),
actions: [
TextButton(
child: const Text('Hayır'),
onPressed: () => Navigator.pop(context, false),
),
TextButton(
child: const Text('Evet'),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return PopScope<Object?>(
// false: geri hareketi sayfayı doğrudan kapatmaz, önce bize sorulur
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) async {
// Sayfa zaten kapandıysa yapacak bir şey yok
if (didPop) return;
final shouldPop = await _showExitDialog(context) ?? false;
if (shouldPop && context.mounted) {
Navigator.pop(context);
}
},
child: Scaffold(
appBar: AppBar(title: const Text('Sayfa')),
body: const Center(child: Text('İçerik')),
),
);
}
}Nasıl çalışıyor:
canPop: falseolduğu sürece geri hareketi sayfayı kapatmaz; bunun yerineonPopInvokedWithResultdidPop: falseile çağrılır.- Kullanıcı "Evet" derse sayfayı kendimiz
Navigator.pop(context)ile kapatıyoruz. Bu çağrı daonPopInvokedWithResult'ı bu kezdidPop: trueile tetikler; en baştakiif (didPop) return;satırı bu yüzden var. awaitsonrasındacontext.mountedkontrolü, diyalog açıkken sayfanın başka bir sebeple kapanmış olma ihtimaline karşı.
Eski kaynaklarda bu iş için WillPopScope ve onWillPop görürsünüz. WillPopScope Flutter 3.16'dan beri kullanımdan kaldırılmış (deprecated) durumda, çünkü Android'in "predictive back" (öngörülü geri) hareketiyle çalışmıyor: sistem, kullanıcı hareketi bitirmeden sayfanın kapanıp kapanamayacağını bilmek zorunda. PopScope bu kararı canPop ile önceden bildirdiği için bu sorunu çözüyor. Aynı sebeple onPopInvoked parametresi de kullanımdan kaldırıldı; yerine onPopInvokedWithResult kullanılıyor.
Pratik İpuçları
1. Navigator'ı, Navigator'ın altında kalan bir context ile çağırın
Navigator.push(context, ...) verdiğiniz context'ten yukarı doğru çıkarak en yakın Navigator'ı arar. Bir sayfanın build metodundaki context bu iş için uygundur; bu yazıdaki örneklerin hepsi böyle çalışıyor. Sorun, context Navigator'ın üstünde kaldığında çıkar. En sık görülen hali, MaterialApp'i oluşturan widget'ın kendi context'iyle navigasyon yapmaya çalışmak:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
// ❌ Bu context MyApp'e ait. Navigator'ı MaterialApp oluşturuyor,
// yani Navigator bu context'in altında kalıyor.
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
),
child: const Text('Git'),
),
),
),
);
}
}Düğmeye basıldığında "Navigator operation requested with a context that does not include a Navigator" hatası alırsınız. Çözüm, düğmeyi MaterialApp'in altında kurulan ayrı bir widget'a taşımak:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: HomePage());
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
// ✅ Bu context MaterialApp'in, dolayısıyla Navigator'ın altında
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
),
child: const Text('Git'),
),
),
);
}
}Aynı build metodunda kalmak isterseniz home: içindeki kısmı bir Builder ile sarmak da işe yarar; Builder size MaterialApp'in altında yeni bir context verir. Ama ayrı bir sayfa widget'ı çoğu zaman daha okunaklıdır. Yani Builder her Navigator.push çağrısı için gerekli değil, sadece context gerçekten Navigator'ın üstündeyse gerekiyor.
2. Async/Await Kullanımı
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const SelectPage()),
);
// Beklerken sayfa kapanmış olabilir
if (!context.mounted) return;
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Sonuç: $result')),
);
}Navigator.push bir Future döndürür ve açılan sayfa pop edildiğinde tamamlanır. await sonrasında context'i kullanmadan önce context.mounted kontrolünü alışkanlık haline getirin.
3. Gereksiz Navigator Kullanımından Kaçının
// ❌ Gereksiz - aynı sayfa içinde
Navigator.push(context, MaterialPageRoute(...));
// ✅ State güncellemesi kullanın
setState(() {
selectedIndex = 1;
});Her push açtığı sayfa için yeni bir State nesnesi kurar, pop ise onu dispose eder. Hangi metodun tam olarak ne zaman çalıştığını merak ediyorsanız yaşam döngüsü yazısında adım adım anlattım.
Özet
Flutter'da Navigation:
- push(): Yeni sayfa aç
- pop(): Geri dön
- pushReplacement(): Geri dönüşü engelle
- pushAndRemoveUntil(): Stack'i temizle
- pushNamed(): İsimli rota kullan
- PopScope: Geri hareketini kontrol et (eski
WillPopScopeyerine)
Veri Transferi:
- Constructor ile ileri veri gönder
pop(context, veri)ile geri veri döndür
Back Stack Yönetimi:
- Login/Register sonrası
pushReplacementkullan - Çıkış yaparken
pushAndRemoveUntilile stack'i temizle
Navigator, Flutter'ın en temel özelliklerinden. Bir kere alıştıktan sonra çok rahat kullanıyorsunuz.
Navigation konusunda takıldınız mı?
Bir sonraki yazıda görüşmek üzere! 🚀
Sık Sorulan Sorular
push ile pushReplacement arasındaki fark nedir?
push yeni sayfayı yığının üstüne ekler, eski sayfa altta durmaya devam eder ve geri tuşu çalışır. pushReplacement üstteki sayfayı yenisiyle değiştirir; geri dönülecek bir sayfa kalmaz. Login ve splash ekranlarından çıkarken ikincisini tercih edin.
Geri dönerken önceki sayfaya veri nasıl gönderilir?
Kapanan sayfada Navigator.pop(context, veri) çağırın; açan taraftaki Navigator.push(...) bir Future döndürdüğü için await ile o veriyi yakalayabilirsiniz. Kullanıcı geri tuşuyla çıkarsa dönen değer null olur, bu yüzden her zaman null kontrolü yapın.
Named route mu MaterialPageRoute mu kullanmalıyım?
Küçük ve orta ölçekli uygulamalarda MaterialPageRoute yeterli ve daha tip güvenlidir. Flutter'ın resmi dokümantasyonu named route'ları çoğu uygulama için önermiyor: derin bağlantı (deep link) davranışı özelleştirilemiyor ve web'de tarayıcının ileri tuşu desteklenmiyor. Rotaları tek yerden yönetmek ya da derin bağlantı desteği istiyorsanız go_router gibi bir paket daha uygun.
WillPopScope yerine ne kullanmalıyım?
PopScope kullanın. canPop: false ile geri hareketini durdurur, onPopInvokedWithResult içinde karar verip gerekirse Navigator.pop(context) ile sayfayı kendiniz kapatırsınız. WillPopScope Flutter 3.16'dan beri kullanımdan kaldırılmış durumda ve Android'in öngörülü geri hareketiyle çalışmıyor.
İlgili Yazılar
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.
Flutter: Riverpod 3 ile Modern State Management
Riverpod 3 ile Flutter'da state yönetimi: Notifier, AsyncNotifier, ref.watch ve ref.read, family, autoDispose ve eski StateNotifier kodu için geçiş notları.
Flutter: Provider ile State Management
Flutter'da Provider paketi ile state yönetimi, ChangeNotifier, Consumer, MultiProvider kullanımı ve best practices.