Flutter Navigation: go_router ile Ölçeklenebilir Yönlendirme
# 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:
Kurulum
`pubspec.yaml` dosyanıza go_router'ı ekleyin:
dependencies:
go_router: ^class="code-number">14.0.class="code-number">0Ardı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:
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:
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.
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:
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ı
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.
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:
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.
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:
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:
<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.comSunucunuzda `.well-known/apple-app-site-association` dosyası yayınlayın:
{
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
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:
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:
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:
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:
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:
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'da Clean Architecture: Ölçeklenebilir Uygulama Geliştirme
Flutter projelerinde Clean Architecture'ı uygulanabilir şekilde öğrenin. Katmanlar, bağımlılık yönetimi ve test edilebilir kod için pratik bir rehber.
Flutter Form Validation: Kullanılabilir ve Güvenli Formlar
Flutter formlarında doğrulama, hata mesajı tasarımı ve kullanıcı deneyimi için pratik yöntemleri öğrenin.
Flutter Localization (i18n): Çok Dilli Uygulama Geliştirme
Flutter'da çok dilli yapı kurun. ARB dosyaları, locale yönetimi ve çeviri süreçlerini ölçeklenebilir hale getirin.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç