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

Flutter'da Navigator ile Sayfalar Arası Geçiş ve Veri Transferi

Ahmet Balaman

Son güncelleme:

6 dk okuma

FlutterNavigatorRoutingNavigationState Management
Flutter'da Navigator ile Sayfalar Arası Geçiş ve Veri Transferi

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ş:

Navigator yığını: başlangıçta yalnızca ana sayfa, push sonrası üstte detay sayfası, pop sonrası yeniden ana sayfa

// 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öner

pushReplacement() ile Geçiş

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => const HomePage()),
);
// Geri tuşu stack'teki bir önceki sayfaya gider, login atlanır

Pratik Ö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']}'),
      ),
    );
  }
}

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: false olduğu sürece geri hareketi sayfayı kapatmaz; bunun yerine onPopInvokedWithResult didPop: false ile çağrılır.
  • Kullanıcı "Evet" derse sayfayı kendimiz Navigator.pop(context) ile kapatıyoruz. Bu çağrı da onPopInvokedWithResult'ı bu kez didPop: true ile tetikler; en baştaki if (didPop) return; satırı bu yüzden var.
  • await sonrasında context.mounted kontrolü, 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 WillPopScope yerine)

Veri Transferi:

  • Constructor ile ileri veri gönder
  • pop(context, veri) ile geri veri döndür

Back Stack Yönetimi:

  • Login/Register sonrası pushReplacement kullan
  • Çıkış yaparken pushAndRemoveUntil ile 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.

Yorumlar