Flutter
Flutter Navigation: go_router ile Ölçeklenebilir Yönlendirme
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:
dependencies:
go_router: ^14.0.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 '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:
// 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.
// 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:
// 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ı
// 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.
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:
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.
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:
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:
<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.comSunucunuzda .well-known/apple-app-site-association dosyası yayınlayın:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.sirketiniz.uygulamaniz",
"paths": ["/article/*", "/profile/*"]
}
]
}
}Deep Link'leri Lokal Ortamda Test Etme
// 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:
- go_router'ı kurun —
MaterialApp.routerile 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.goNamedile 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:
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:
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:
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('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'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ç