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

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

# go_router ile Flutter Navigation

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: ^class="code-number">14.0.class="code-number">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 class="code-string">'package:go_router/go_router.dart';
    
    final GoRouter router = GoRouter(
      initialLocation: class="code-string">'/',
      routes: [
        GoRoute(
          path: class="code-string">'/',
          name: class="code-string">'home',
          builder: (context, state) => const HomeScreen(),
        ),
        GoRoute(
          path: class="code-string">'/settings',
          name: class="code-string">'settings',
          builder: (context, state) => const SettingsScreen(),
        ),
        GoRoute(
          path: class="code-string">'/profile/:userId',
          name: class="code-string">'profile',
          builder: (context, state) {
            final userId = state.pathParameters[class="code-string">'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
    class=class="code-string">"code-comment">// Path ile
    context.go(class="code-string">'/profile/class="code-number">42');
    
    class=class="code-string">"code-comment">// İsim ile (benim tercih ettiğim yaklaşım)
    context.goNamed(class="code-string">'profile', pathParameters: {class="code-string">'userId': class="code-string">'class="code-number">42'});
    
    class=class="code-string">"code-comment">// Stack üzerine eklemek için push
    context.push(class="code-string">'/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
    class=class="code-string">"code-comment">// lib/routing/app_router.dart
    
    import class="code-string">'package:go_router/go_router.dart';
    import class="code-string">'package:flutter_riverpod/flutter_riverpod.dart';
    import class="code-string">'route_names.dart';
    import class="code-string">'auth_routes.dart';
    import class="code-string">'home_routes.dart';
    import class="code-string">'profile_routes.dart';
    import class="code-string">'admin_routes.dart';
    
    final routerProvider = Provider<GoRouter>((ref) {
      final authState = ref.watch(authStateProvider);
    
      return GoRouter(
        initialLocation: class="code-string">'/',
        debugLogDiagnostics: true,
        refreshListenable: authState,
        redirect: (context, state) {
          final isLoggedIn = authState.isAuthenticated;
          final isOnLoginPage = state.matchedLocation == class="code-string">'/login';
          final isOnOnboarding = state.matchedLocation == class="code-string">'/onboarding';
    
          class=class="code-string">"code-comment">// Giriş yapılmamış — login sayfasına yönlendir
          if (!isLoggedIn && !isOnLoginPage) return class="code-string">'/login';
    
          class=class="code-string">"code-comment">// Giriş yapılmış ama login sayfasında — ana sayfaya yönlendir
          if (isLoggedIn && isOnLoginPage) return class="code-string">'/';
    
          class=class="code-string">"code-comment">// Giriş yapılmış ama onboarding tamamlanmamış
          if (isLoggedIn && !authState.onboardingComplete && !isOnOnboarding) {
            return class="code-string">'/onboarding';
          }
    
          return null; class=class="code-string">"code-comment">// 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
    class=class="code-string">"code-comment">// lib/routing/route_names.dart
    
    abstract class RouteNames {
      static const home = class="code-string">'home';
      static const login = class="code-string">'login';
      static const onboarding = class="code-string">'onboarding';
      static const profile = class="code-string">'profile';
      static const profileEdit = class="code-string">'profile-edit';
      static const settings = class="code-string">'settings';
      static const adminDashboard = class="code-string">'admin-dashboard';
      static const adminUsers = class="code-string">'admin-users';
      static const articleDetail = class="code-string">'article-detail';
    }

    Feature Bazlı Route Dosyaları

    dart
    class=class="code-string">"code-comment">// lib/routing/home_routes.dart
    
    import class="code-string">'package:go_router/go_router.dart';
    import class="code-string">'route_names.dart';
    
    final homeRoutes = <RouteBase>[
      GoRoute(
        path: class="code-string">'/',
        name: RouteNames.home,
        builder: (context, state) => const HomeScreen(),
        routes: [
          GoRoute(
            path: class="code-string">'article/:articleId',
            name: RouteNames.articleDetail,
            builder: (context, state) {
              final id = state.pathParameters[class="code-string">'articleId']!;
              return ArticleDetailScreen(articleId: id);
            },
          ),
        ],
      ),
      GoRoute(
        path: class="code-string">'/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: class="code-string">'/feed',
          name: class="code-string">'feed',
          builder: (context, state) => const FeedScreen(),
        ),
        GoRoute(
          path: class="code-string">'/search',
          name: class="code-string">'search',
          builder: (context, state) => const SearchScreen(),
        ),
        GoRoute(
          path: class="code-string">'/profile',
          name: class="code-string">'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: class="code-string">'/feed',
              builder: (context, state) => const FeedScreen(),
              routes: [
                GoRoute(
                  path: class="code-string">'detail/:postId',
                  builder: (context, state) => PostDetailScreen(
                    postId: state.pathParameters[class="code-string">'postId']!,
                  ),
                ),
              ],
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: class="code-string">'/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;
    
      class=class="code-string">"code-comment">// Redirect gerektirmeyen public routeclass="code-string">'lar
      const publicRoutes = ['/loginclass="code-string">', '/registerclass="code-string">', '/forgot-passwordclass="code-string">'];
      if (publicRoutes.contains(location)) {
        return auth.isLoggedIn ? '/class="code-string">' : null;
      }
    
      class=class="code-string">"code-comment">// Diğer her şey auth gerektirir
      if (!auth.isLoggedIn) {
        return '/login?redirect=${Uri.encodeComponent(location)}class="code-string">';
      }
    
      class=class="code-string">"code-comment">// Admin route'ları admin rolü gerektirir
      if (location.startsWith(class="code-string">'/admin') && !auth.isAdmin) {
        return class="code-string">'/';
      }
    
      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: class="code-string">'/login',
      builder: (context, state) {
        final redirectTo = state.uri.queryParameters[class="code-string">'redirect'] ?? class="code-string">'/';
        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=class="code-string">"true">
        <action android:name=class="code-string">"android.intent.action.VIEW" />
        <category android:name=class="code-string">"android.intent.category.DEFAULT" />
        <category android:name=class="code-string">"android.intent.category.BROWSABLE" />
        <data
          android:scheme=class="code-string">"https"
          android:host=class="code-string">"uygulamaniz.com"
          android:pathPrefix=class="code-string">"/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
    {
      class="code-string">"applinks": {
        class="code-string">"apps": [],
        class="code-string">"details": [
          {
            class="code-string">"appID": class="code-string">"TEAMID.com.sirketiniz.uygulamaniz",
            class="code-string">"paths": [class="code-string">"/article/*", class="code-string">"/profile/*"]
          }
        ]
      }
    }

    Deep Link'leri Lokal Ortamda Test Etme

    dart
    class=class="code-string">"code-comment">// Android
    adb shell am start -a android.intent.action.VIEW \
      -d class="code-string">"https:class="code-commentclass="code-string">">//uygulamaniz.com/article/class="code-number">42" com.sirketiniz.uygulamaniz
    
    class=class="code-string">"code-comment">// iOS
    xcrun simctl openurl booted class="code-string">"https:class="code-commentclass="code-string">">//uygulamaniz.com/article/class="code-number">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:

  • **go_router'ı kurun** — `MaterialApp.router` ile başlayıp tek bir feature için route'ları tanımlayın
  • **`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
  • **Feature'ları tek tek taşıyın** — `Navigator.pushNamed` çağrılarını `context.goNamed` ile değiştirin
  • **Auth guard'ları** bireysel ekranlardan üst düzey `redirect`'e taşıyın
  • **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, class=class="code-string">"code-comment">// 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(class="code-string">'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 = class="code-string">'$prefix/${route.path}'.replaceAll(class="code-string">'class=class="code-string">"code-comment">//', class="code-string">'/');
            expect(paths.contains(fullPath), isFalse,
              reason: class="code-string">'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, class="code-string">'');
    });

    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(class="code-string">'kimliği doğrulanmamış kullanıcıyı login sayfasına yönlendirir', () {
      final router = GoRouter(
        initialLocation: class="code-string">'/profile',
        redirect: (context, state) {
          if (!isLoggedIn) return class="code-string">'/login';
          return null;
        },
        routes: [
          GoRoute(path: class="code-string">'/login', builder: (_, __) => const LoginScreen()),
          GoRoute(path: class="code-string">'/profile', builder: (_, __) => const ProfileScreen()),
        ],
      );
    
      class=class="code-string">"code-comment">// Başlatma sonrası router yönlendirmiş olmalı
      expect(router.location, class="code-string">'/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ç