feat: add flutter-dev skill
Add comprehensive Flutter cross-platform development guide covering: - Widget patterns and const optimization - Riverpod/Bloc state management - GoRouter navigation - Performance optimization - Testing strategies - Platform-specific implementations Made-with: Cursor
This commit is contained in:
257
skills/flutter-dev/references/gorouter-navigation.md
Normal file
257
skills/flutter-dev/references/gorouter-navigation.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# GoRouter Navigation
|
||||
|
||||
GoRouter navigation guide covering route setup, guards, deep linking, and shell routes.
|
||||
|
||||
## Basic Setup
|
||||
|
||||
```dart
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
final goRouter = GoRouter(
|
||||
initialLocation: '/',
|
||||
debugLogDiagnostics: true,
|
||||
redirect: (context, state) {
|
||||
final isLoggedIn = /* check auth state */;
|
||||
final isAuthRoute = state.matchedLocation.startsWith('/auth');
|
||||
|
||||
if (!isLoggedIn && !isAuthRoute) {
|
||||
return '/auth/login';
|
||||
}
|
||||
if (isLoggedIn && isAuthRoute) {
|
||||
return '/';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
name: 'home',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'details/:id',
|
||||
name: 'details',
|
||||
builder: (context, state) {
|
||||
final id = state.pathParameters['id']!;
|
||||
final extra = state.extra as Map<String, dynamic>?;
|
||||
return DetailsScreen(id: id, title: extra?['title']);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/login',
|
||||
name: 'login',
|
||||
builder: (context, state) => const LoginScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
### App Integration
|
||||
|
||||
```dart
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
routerConfig: goRouter,
|
||||
theme: AppTheme.light,
|
||||
darkTheme: AppTheme.dark,
|
||||
themeMode: ThemeMode.system,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Navigation Methods
|
||||
|
||||
```dart
|
||||
// Navigate and replace entire stack
|
||||
context.go('/details/123');
|
||||
|
||||
// Navigate and add to stack (can go back)
|
||||
context.push('/details/123');
|
||||
|
||||
// Go back
|
||||
context.pop();
|
||||
|
||||
// Go back with result
|
||||
context.pop(result);
|
||||
|
||||
// Replace current route
|
||||
context.pushReplacement('/home');
|
||||
|
||||
// Navigate with extra data
|
||||
context.push('/details/123', extra: {'title': 'Item Title'});
|
||||
|
||||
// Navigate by name
|
||||
context.goNamed('details', pathParameters: {'id': '123'});
|
||||
context.pushNamed('details', pathParameters: {'id': '123'}, extra: data);
|
||||
```
|
||||
|
||||
### Navigation Reference
|
||||
|
||||
| Method | Behavior |
|
||||
|--------|----------|
|
||||
| `context.go()` | Navigate, replace entire stack |
|
||||
| `context.push()` | Navigate, add to stack |
|
||||
| `context.pop()` | Go back one level |
|
||||
| `context.pushReplacement()` | Replace current route |
|
||||
| `context.goNamed()` | Navigate by route name |
|
||||
| `context.canPop()` | Check if can go back |
|
||||
|
||||
## Shell Routes (Persistent UI)
|
||||
|
||||
```dart
|
||||
final goRouter = GoRouter(
|
||||
routes: [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) {
|
||||
return ScaffoldWithNavBar(child: child);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
builder: (_, __) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/search',
|
||||
builder: (_, __) => const SearchScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
builder: (_, __) => const ProfileScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
class ScaffoldWithNavBar extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const ScaffoldWithNavBar({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _calculateSelectedIndex(context),
|
||||
onDestinationSelected: (index) => _onItemTapped(index, context),
|
||||
destinations: const [
|
||||
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
|
||||
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
|
||||
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _calculateSelectedIndex(BuildContext context) {
|
||||
final location = GoRouterState.of(context).matchedLocation;
|
||||
if (location.startsWith('/home')) return 0;
|
||||
if (location.startsWith('/search')) return 1;
|
||||
if (location.startsWith('/profile')) return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void _onItemTapped(int index, BuildContext context) {
|
||||
switch (index) {
|
||||
case 0: context.go('/home');
|
||||
case 1: context.go('/search');
|
||||
case 2: context.go('/profile');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Query Parameters
|
||||
|
||||
```dart
|
||||
GoRoute(
|
||||
path: '/search',
|
||||
builder: (context, state) {
|
||||
final query = state.uri.queryParameters['q'] ?? '';
|
||||
final page = int.tryParse(state.uri.queryParameters['page'] ?? '1') ?? 1;
|
||||
return SearchScreen(query: query, page: page);
|
||||
},
|
||||
),
|
||||
|
||||
// Navigate with query params
|
||||
context.go('/search?q=flutter&page=2');
|
||||
context.goNamed('search', queryParameters: {'q': 'flutter', 'page': '2'});
|
||||
```
|
||||
|
||||
## Riverpod Integration
|
||||
|
||||
```dart
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
return GoRouter(
|
||||
refreshListenable: authState,
|
||||
redirect: (context, state) {
|
||||
final isLoggedIn = authState.isAuthenticated;
|
||||
final isAuthRoute = state.matchedLocation.startsWith('/auth');
|
||||
|
||||
if (!isLoggedIn && !isAuthRoute) return '/auth/login';
|
||||
if (isLoggedIn && isAuthRoute) return '/';
|
||||
return null;
|
||||
},
|
||||
routes: [...],
|
||||
);
|
||||
});
|
||||
|
||||
// In app.dart
|
||||
class MyApp extends ConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final router = ref.watch(routerProvider);
|
||||
return MaterialApp.router(routerConfig: router);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```dart
|
||||
final goRouter = GoRouter(
|
||||
errorBuilder: (context, state) {
|
||||
return ErrorScreen(error: state.error);
|
||||
},
|
||||
routes: [...],
|
||||
);
|
||||
```
|
||||
|
||||
## Deep Linking
|
||||
|
||||
Deep links work automatically when routes are configured with path parameters:
|
||||
|
||||
```dart
|
||||
// URL: myapp://details/123
|
||||
// or: https://myapp.com/details/123
|
||||
GoRoute(
|
||||
path: '/details/:id',
|
||||
builder: (context, state) => DetailsScreen(id: state.pathParameters['id']!),
|
||||
),
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
| Do | Don't |
|
||||
|----|-------|
|
||||
| Use named routes for maintainability | Hardcode paths everywhere |
|
||||
| Use `push()` for detail screens | Use `go()` for all navigation |
|
||||
| Pass simple data via `extra` | Pass complex objects via URL |
|
||||
| Use redirect for auth guards | Check auth in every screen |
|
||||
| Use ShellRoute for persistent UI | Rebuild nav bar in every screen |
|
||||
|
||||
---
|
||||
|
||||
*GoRouter is an open-source navigation package for Flutter.*
|
||||
Reference in New Issue
Block a user