Flutter

Flutter Navigation: go_router ile Ölçeklenebilir Yönlendirme

13 dk okuma9 Şubat 2026Güncellendi 9 Mar 2026
Flutter go_routerFlutter navigationFlutter deep linkFlutter route guardFlutter nested routesgo_router redirectFlutter shell routeFlutter routing best practices

Uygulama büyüdükçe navigasyon yönetimi en karmaşık konulardan biri haline gelir. Deep link'ler bozulur, auth kontrolleri widget'lara dağılır, route tanımları tekrarlanır. Bu döngüyü birden fazla projede yaşadım ve go_router, routing katmanıma düzen getiren araç oldu.

Bu yazıda orta ölçekli bir uygulama için eksiksiz bir go_router kurulumunu, eski Navigator API'lerinden geçişi ve kaçınılması gereken yaygın hataları paylaşacağım.

Neden go_router?

Flutter, Navigator 1.0 (imperative push/pop) ve Navigator 2.0 (declarative ama fazlasıyla verbose) ile birlikte gelir. go_router, Navigator 2.0 üzerine oturur ve şunları sağlar:

  • Declarative route tanımları — tüm route'lar tek bir yerde görünür
  • Yerleşik redirect mantığı — auth guard'ları, onboarding kontrolleri, rol bazlı erişim
  • Kutudan çıkan deep link desteği — Android App Links, iOS Universal Links ve web URL'leri ekstra konfigürasyon gerektirmeden çalışır
  • Shell route'lar — her navigasyonda shell'i yeniden oluşturmadan kalıcı bottom nav bar, side drawer veya tab yapıları
  • Type-safe path parametreleri ve query parametreleri
  • Kolay test edilebilirlik — redirect mantığınızı widget tree başlatmadan unit test edebilirsiniz

Kurulum

pubspec.yaml dosyanıza go_router'ı ekleyin:

dart
dependencies:
  go_router: ^14.0.0

Ardından flutter pub get komutunu çalıştırın.

Temel Route Yapılandırması

En basit kurulum şu şekilde. Bir GoRouter instance'ı route'larınızı tanımlar ve MaterialApp.router'a iletilir:

dart
import 'package:go_router/go_router.dart';

final GoRouter router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      name: 'home',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/settings',
      name: 'settings',
      builder: (context, state) => const SettingsScreen(),
    ),
    GoRoute(
      path: '/profile/:userId',
      name: 'profile',
      builder: (context, state) {
        final userId = state.pathParameters['userId']!;
        return ProfileScreen(userId: userId);
      },
    ),
  ],
);

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
    );
  }
}

Navigasyon oldukça kolay:

dart
// Path ile
context.go('/profile/42');

// İsim ile (benim tercih ettiğim yaklaşım)
context.goNamed('profile', pathParameters: {'userId': '42'});

// Stack üzerine eklemek için push
context.push('/settings');

Orta Ölçekli Uygulama İçin Tam Kurulum

Kendi uygulamalarımda route'ları feature bazlı yapılandırırım. Her feature modülü kendi RouteBase listesini dışa aktarır ve üst düzey router bunları birleştirir. Bu yaklaşım dosyaları küçük tutar ve hangi route'un kime ait olduğunu açıkça gösterir.

dart
// lib/routing/app_router.dart

import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'route_names.dart';
import 'auth_routes.dart';
import 'home_routes.dart';
import 'profile_routes.dart';
import 'admin_routes.dart';

final routerProvider = Provider<GoRouter>((ref) {
  final authState = ref.watch(authStateProvider);

  return GoRouter(
    initialLocation: '/',
    debugLogDiagnostics: true,
    refreshListenable: authState,
    redirect: (context, state) {
      final isLoggedIn = authState.isAuthenticated;
      final isOnLoginPage = state.matchedLocation == '/login';
      final isOnOnboarding = state.matchedLocation == '/onboarding';

      // Giriş yapılmamış — login sayfasına yönlendir
      if (!isLoggedIn && !isOnLoginPage) return '/login';

      // Giriş yapılmış ama login sayfasında — ana sayfaya yönlendir
      if (isLoggedIn && isOnLoginPage) return '/';

      // Giriş yapılmış ama onboarding tamamlanmamış
      if (isLoggedIn && !authState.onboardingComplete && !isOnOnboarding) {
        return '/onboarding';
      }

      return null; // yönlendirme yok
    },
    routes: [
      ...authRoutes,
      ShellRoute(
        builder: (context, state, child) {
          return AppShell(child: child);
        },
        routes: [
          ...homeRoutes,
          ...profileRoutes,
          ...adminRoutes,
        ],
      ),
    ],
    errorBuilder: (context, state) => NotFoundScreen(
      error: state.error,
    ),
  );
});

Route İsim Sabitleri

Path string'lerini her yere hardcode yazmak, yazım hatalarına davetiye çıkarmaktır. Tüm route isimlerini tek bir dosyada tutarım:

dart
// lib/routing/route_names.dart

abstract class RouteNames {
  static const home = 'home';
  static const login = 'login';
  static const onboarding = 'onboarding';
  static const profile = 'profile';
  static const profileEdit = 'profile-edit';
  static const settings = 'settings';
  static const adminDashboard = 'admin-dashboard';
  static const adminUsers = 'admin-users';
  static const articleDetail = 'article-detail';
}

Feature Bazlı Route Dosyaları

dart
// lib/routing/home_routes.dart

import 'package:go_router/go_router.dart';
import 'route_names.dart';

final homeRoutes = <RouteBase>[
  GoRoute(
    path: '/',
    name: RouteNames.home,
    builder: (context, state) => const HomeScreen(),
    routes: [
      GoRoute(
        path: 'article/:articleId',
        name: RouteNames.articleDetail,
        builder: (context, state) {
          final id = state.pathParameters['articleId']!;
          return ArticleDetailScreen(articleId: id);
        },
      ),
    ],
  ),
  GoRoute(
    path: '/settings',
    name: RouteNames.settings,
    builder: (context, state) => const SettingsScreen(),
  ),
];

İç İçe Route'lar ve Shell Route'lar

Shell route'lar, go_router'ın en güçlü özelliklerinden biridir. Bottom navigation bar gibi kalıcı bir UI bileşenini korurken yalnızca içerik alanını değiştirmenize olanak tanır.

dart
ShellRoute(
  builder: (context, state, child) {
    return ScaffoldWithBottomNav(child: child);
  },
  routes: [
    GoRoute(
      path: '/feed',
      name: 'feed',
      builder: (context, state) => const FeedScreen(),
    ),
    GoRoute(
      path: '/search',
      name: 'search',
      builder: (context, state) => const SearchScreen(),
    ),
    GoRoute(
      path: '/profile',
      name: 'profile',
      builder: (context, state) => const ProfileScreen(),
    ),
  ],
)

ScaffoldWithBottomNav widget'ı child'ı alıp body içine yerleştirir. Bottom nav bar mount edilmiş kalır, durumunu korur; yalnızca içerik geçiş yapar.

Bağımsız Navigation Stack'leri için StatefulShellRoute

Her tab'ın kendi back stack'ine ihtiyacı varsa (Instagram tarzı navigasyon düşünün), StatefulShellRoute kullanın:

dart
StatefulShellRoute.indexedStack(
  builder: (context, state, navigationShell) {
    return MainShell(navigationShell: navigationShell);
  },
  branches: [
    StatefulShellBranch(
      routes: [
        GoRoute(
          path: '/feed',
          builder: (context, state) => const FeedScreen(),
          routes: [
            GoRoute(
              path: 'detail/:postId',
              builder: (context, state) => PostDetailScreen(
                postId: state.pathParameters['postId']!,
              ),
            ),
          ],
        ),
      ],
    ),
    StatefulShellBranch(
      routes: [
        GoRoute(
          path: '/profile',
          builder: (context, state) => const ProfileScreen(),
        ),
      ],
    ),
  ],
)

Her branch kendi navigation stack'ini korur. Kullanıcı tab değiştirip geri döndüğünde, önceki konum aynen kalır.

Redirect Guard'ları

Redirect'ler, auth ve yetki kontrollerini yönetmenin en temiz yoludur. Deneyimlerime göre tüm redirect mantığını tek bir üst düzey fonksiyonda toplamak, guard'ları ayrı ayrı route'lara dağıtmaktan çok daha iyi çalışır.

dart
redirect: (context, state) {
  final auth = ref.read(authProvider);
  final location = state.matchedLocation;

  // Redirect gerektirmeyen public route'lar
  const publicRoutes = ['/login', '/register', '/forgot-password'];
  if (publicRoutes.contains(location)) {
    return auth.isLoggedIn ? '/' : null;
  }

  // Diğer her şey auth gerektirir
  if (!auth.isLoggedIn) {
    return '/login?redirect=${Uri.encodeComponent(location)}';
  }

  // Admin route'ları admin rolü gerektirir
  if (location.startsWith('/admin') && !auth.isAdmin) {
    return '/';
  }

  return null;
},

redirect query parametresine dikkat edin. Giriş yaptıktan sonra bu parametreyi okuyup kullanıcıyı aslında gitmek istediği sayfaya yönlendirebilirsiniz:

dart
GoRoute(
  path: '/login',
  builder: (context, state) {
    final redirectTo = state.uri.queryParameters['redirect'] ?? '/';
    return LoginScreen(redirectAfterLogin: redirectTo);
  },
),

Deep Linking Kurulumu

go_router, routing tarafında deep link'leri otomatik olarak yönetir ama platform tarafı konfigürasyon gereklidir.

Android — App Links

android/app/src/main/AndroidManifest.xml dosyasına intent filter ekleyin:

dart
<activity ...>
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
      android:scheme="https"
      android:host="uygulamaniz.com"
      android:pathPrefix="/article" />
  </intent-filter>
</activity>

Domain sahipliğini doğrulamak için sunucunuzda .well-known/assetlinks.json dosyası yayınlayın.

iOS — Universal Links

Xcode'da Associated Domains yetkilendirmesi ekleyin:

applinks:uygulamaniz.com

Sunucunuzda .well-known/apple-app-site-association dosyası yayınlayın:

dart
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.sirketiniz.uygulamaniz",
        "paths": ["/article/*", "/profile/*"]
      }
    ]
  }
}

Deep Link'leri Lokal Ortamda Test Etme

dart
// Android
adb shell am start -a android.intent.action.VIEW \
  -d "https://uygulamaniz.com/article/42" com.sirketiniz.uygulamaniz

// iOS
xcrun simctl openurl booted "https://uygulamaniz.com/article/42"

go_router gelen URL'yi ayrıştırır ve tanımlı route'larınızla eşleştirir. Route ağacınızda /article/:articleId varsa, kullanıcı doğrudan ArticleDetailScreen(articleId: '42') ekranına ulaşır.

Navigator 1.0 / 2.0'dan Geçiş

Navigator 1.0'dan (Imperative) Geçiş

Kod tabanınız Navigator.push, Navigator.pushNamed ve Navigator.pop kullanıyorsa geçiş oldukça doğrudandır:

| Navigator 1.0 | go_router |

|---|---|

| Navigator.pushNamed(context, '/profile') | context.go('/profile') veya context.push('/profile') |

| Navigator.pop(context) | context.pop() |

| Navigator.pushReplacementNamed(context, '/home') | context.go('/home') |

| Navigator.pushAndRemoveUntil(...) | context.go('/home') (stack'i temizler) |

| onGenerateRoute | GoRouter(routes: [...]) |

Temel fark: context.go() hedef yola ulaşmak için tüm navigation stack'i değiştirir. context.push() ise üstüne ekler, eski Navigator.push gibi.

Navigator 2.0'dan (RouterDelegate + RouteInformationParser) Geçiş

Halihazırda özel bir Router yapısı kurduysanız, go_router hem RouterDelegate'inizi hem de RouteInformationParser'ınızı değiştirir. Bu sınıfları tamamen silebilir ve route eşleme mantığınızı GoRouter(routes: [...]) yapısına taşıyabilirsiniz.

Geçiş Stratejisi

Tüm uygulamayı tek seferde taşımak yerine feature bazlı ilerlemenizi öneririm:

  1. go_router'ı kurunMaterialApp.router ile başlayıp tek bir feature için route'ları tanımlayın
  2. `Navigator.push` çağrılarını geçici olarak koruyun — go_router Navigator üzerine oturduğu için eski çağrılar çalışmaya devam eder
  3. Feature'ları tek tek taşıyınNavigator.pushNamed çağrılarını context.goNamed ile değiştirin
  4. Auth guard'ları bireysel ekranlardan üst düzey redirect'e taşıyın
  5. Eski `onGenerateRoute`'u kaldırın — tüm feature'lar taşındığında

Yaygın Routing Hataları

1. push() Yerine go() Kullanmak

context.go('/detail/42') stack'i değiştirir. Kullanıcı geri tuşuna bastığında önceki ekrana dönmez — üst route'a gider. Standart geri davranışı istiyorsanız context.push('/detail/42') kullanın.

2. refreshListenable'ı Unutmak

Redirect mantığınız auth durumuna bağlıysa, go_router'a bu durum değiştiğinde redirect'leri yeniden değerlendirmesini söylemeniz gerekir:

dart
GoRouter(
  refreshListenable: authStateNotifier, // Listenable implement etmeli
  redirect: (context, state) { ... },
)

Bu olmadan, çıkış yapan kullanıcı manuel olarak başka bir sayfaya gidinceye kadar korumalı sayfada kalır.

3. Route Path'lerini Tekrarlamak

Aynı path'i iki farklı yerde tanımlamak öngörülemeyen eşleşmelere yol açar. Benim route yapımda her path tam olarak bir kez tanımlanır ve bunu basit bir test ile kontrol ederim:

dart
test('tekrarlanan route path olmadığını doğrula', () {
  final paths = <String>[];
  void collectPaths(List<RouteBase> routes, String prefix) {
    for (final route in routes) {
      if (route is GoRoute) {
        final fullPath = '$prefix/${route.path}'.replaceAll('//', '/');
        expect(paths.contains(fullPath), isFalse,
          reason: 'Tekrarlanan path: $fullPath');
        paths.add(fullPath);
      }
      if (route is GoRoute) collectPaths(route.routes, route.path);
      if (route is ShellRoute) collectPaths(route.routes, prefix);
    }
  }
  collectPaths(router.configuration.routes, '');
});

4. Gereksiz Derin İç İçe Yapı

Her iç içe route URL yolunu uzatır. /home/feed/post/42/comments/5/reply gibi bir yapı, hiyerarşinizi düzleştirmeniz gerektiğinin işaretidir. Route derinliğini en fazla iki seviyede tutmaya çalışırım.

5. Hata Route'unu Tanımlamamak

Kullanıcı hiçbir route ile eşleşmeyen bir URL'ye gittiğinde, go_router varsayılan bir hata sayfası gösterir. Her zaman kendi hata sayfanızı sağlayın:

dart
GoRouter(
  errorBuilder: (context, state) => const NotFoundScreen(),
)

6. extra ile Karmaşık Nesneler Geçirmek

state.extra navigasyon sırasında herhangi bir nesne geçirmenize izin verir, ama deep link'leri bozar (nesne URL'ye serileştirilemez). Bunun yerine ID'leri path veya query parametresi olarak geçirin ve tam nesneyi hedef ekranda çekin.

Route'ları Test Etme

go_router, route testlerini ham Navigator 2.0'a göre çok daha basit hale getirir:

dart
test('kimliği doğrulanmamış kullanıcıyı login sayfasına yönlendirir', () {
  final router = GoRouter(
    initialLocation: '/profile',
    redirect: (context, state) {
      if (!isLoggedIn) return '/login';
      return null;
    },
    routes: [
      GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),
      GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
    ],
  );

  // Başlatma sonrası router yönlendirmiş olmalı
  expect(router.location, '/login');
});

Sonuç

go_router, basit bir prototip dışındaki her Flutter projem için varsayılan navigasyon çözümüm haline geldi. Declarative route'lar, merkezi redirect'ler, shell route'lar ve deep link desteğinin birleşimi, gerçek dünya navigasyon ihtiyaçlarının büyük çoğunluğunu karşılıyor. Route'larınızı feature bazlı yapılandırın, redirect mantığınızı tek bir yerde tutun, ham path'ler yerine isimli route'ları tercih edin — saatlerce debug süresinden tasarruf edersiniz.

Navigator 1.0 veya 2.0'dan geçiş planlıyorsanız, uygulamanızın mimarisine özel adım adım bir geçiş planı hazırlamamda yardımcı olabilirim.

İlgili Makaleler

Flutter Projeniz mi Var?

iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

İletişime Geç