
URL Strategy for Web Apps Using go_router in Flutter
When developing web applications in Flutter, the URL strategy significantly impacts user experience. In particular, navigation is crucial in a web environment, where users may directly access pages via URLs or use the back button. In this article, we will introduce go_router
, a powerful routing library in Flutter, and explore effective routing strategies using it.
Introduction to go_router
go_router
is the officially recommended routing solution by the Flutter team. It addresses the complexities of the existing Navigator 2.0
while providing declarative and powerful routing capabilities.
Key Features
- Declarative route definitions
- Support for nested navigation
- Support for URL parameters and query parameters
- Deep link support
- Redirect rules configuration
- Error handling
- Web platform optimization
Installation
Add the following dependency to your pubspec.yaml
file:
dependencies:
go_router: ^14.3.0 # Latest version as of October 2024.
1. Removing Hash Tags
The default URL strategy for Flutter web applications is hash-based (e.g., example.com/#/home
). This is not good for SEO and degrades user experience. You can improve this by using usePathUrlStrategy()
.
import 'package:flutter_web_plugins/url_strategy.dart';
void main() {
usePathUrlStrategy(); // 해시태그 제거
runApp(MyApp());
}
2. Basic Configuration of go_router
2.1 Basic Router Setup
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailsScreen(id: id);
},
),
],
),
],
);
2.2 Integration with Riverpod
Integrating Riverpod with go_router allows for effective dynamic routing based on the app’s state. This makes routing based on authentication status or user permissions much easier.
Basic Setup
First, define the necessary providers:
// Provider for managing authentication status
final authStateProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier();
});
// router provider
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authStateProvider);
return GoRouter(
navigatorKey: rootNavKey,
refreshListenable: authState as Listenable,
initialLocation: '/',
routes: [
// Route configuration
],
redirect: (context, state) {
// Redirect logic
return null;
},
);
});
Implementation of Authentication State Class
class AuthState extends ChangeNotifier {
AuthStatus _status = AuthStatus.initial;
User? _user;
AuthState() {
// Initial authentication status check
checkAuthStatus();
}
AuthStatus get status => _status;
User? get user => _user;
void setAuthenticated(User user) {
_user = user;
_status = AuthStatus.authenticated;
notifyListeners();
}
void setUnauthenticated() {
_user = null;
_status = AuthStatus.unauthenticated;
notifyListeners();
}
}
Route Setup and Redirect logic
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authStateProvider);
return GoRouter(
navigatorKey: rootNavKey,
refreshListenable: authState as Listenable,
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/dashboard',
builder: (context, state) => const DashboardScreen(),
),
],
redirect: (context, state) {
final authStatus = authState.status;
final isLoggingIn = state.matchedLocation == '/login';
// Do not redirect If in the initial state
if (authStatus == AuthStatus.initial) return null;
// If an unauthenticated user tried to access a protected route
if (authStatus == AuthStatus.unauthenticated) {
return isLoggingIn ? null : '/login';
}
// If an authenticated user tries to access the login page
if (authStatus == AuthStatus.authenticated && isLoggingIn) {
return '/dashboard';
}
return null;
},
);
});
Example of Actual Usage
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,
title: 'My App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
);
}
}
// Example of login handling
class LoginScreen extends ConsumerWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () async {
// Perform login logic
final user = await performLogin();
// Update authentication status
ref.read(authStateProvider.notifier).setAuthenticated(user);
// go_router automatically handles redirection
},
child: const Text('Login'),
),
),
);
}
}
Integrating with Riverpod provides the following benefits:
- State-based Routing: Automatically redirect to the appropriate page based on changes in the app’s state.
- Code Structuring: Allows for separation of routing logic into provides for better management.
- Ease of Testing: Dependency injection through providers makes testing easier.
- Maintainability: Authentication-related logic is managed in one place, making maintenance easier.
2.3 Utilizing URL Parameters
GoRoute(
path: '/user/:userId/posts/:postId',
builder: (context, state) {
final userId = state.pathParameters['userId']!;
final postId = state.pathParameters['postId']!;
return PostDetailScreen(userId: userId, postId: postId);
},
),
2.4 Handling Query Parameters
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'];
final filter = state.uri.queryParameters['filter'];
return SearchScreen(query: query, filter: filter);
},
),
3. Advanced Routing Features
3.1 Nested Navigation
GoRoute(
path: '/dashboard',
builder: (context, state) => const DashboardScreen(),
routes: [
GoRoute(
path: 'profile',
builder: (context, state) => const ProfileScreen(),
),
GoRoute(
path: 'settings',
builder: (context, state) => const SettingsScreen(),
),
],
),
3.2 Redirect Configuration
redirect: (context, state) {
final authState = ref.read(authStateProvider);
final isLoginPage = state.matchedLocation == '/login';
// Protected routes that require authentication
final protectedPaths = ['/dashboard', '/profile', '/settings'];
// Check if the current path is a protected route
final isProtectedRoute = protectedPaths.any(
(path) => state.matchedLocation.startsWith(path),
);
if (authState == AuthState.unauthenticated && isProtectedRoute) {
// Redirect to the login page while saving the originally intend
return '/login?redirect=${state.matchedLocation}';
}
if (authState == AuthState.authenticated && isLoginPage) {
// If an authenticated user tries to access the login page, redirect to the dashboard
return '/dashboard';
}
return null;
},
3.3 Error Handling
errorBuilder: (context, state) {
return MaterialPage(
key: state.pageKey,
child: Scaffold(
appBar: AppBar(
title: const Text('An error has occurred'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
const SizedBox(height: 16),
Text(
'The requested page could not be found:\n${state.error}',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.go('/'),
child: const Text('Go back to home'),
),
],
),
),
),
);
},
4. Practical Usage Examples
4.1 Page Transition Animation
CustomTransitionPage<void>(
key: state.pageKey,
child: const DetailsScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
4.2 Managing Page History
// Go back
context.pop();
// Navigate to a specific route (add to history)
context.push('/details/123');
// Navigate to a specific route (replace history)
context.replace('/new-route');
// Go to home and reset history
context.go('/');
5. Performance Optimization
5.1 Route Caching
final router = GoRouter(
routes: routes,
// Page caching configuration
restorationScopeId: 'router',
// Limit the number of pages to cache
observers: [
GoRouterObserver(
navigatorObservers: () => [
HeroController(),
],
),
],
);
6. Conclusion
go_router
provides a powerful and flexible routing solution for Flutter web applications. It effectively handles various scenarios such as URL strategies, authentication processing, and nested navigation, and integrates seamlessly with Riverpod for clean state management.
In particular, in a web environment, SEO optimization, deep linking, and browser history management are crucial, and using go_router
allows for effective implementation of these requirements.
For more detailed information and examples, you can check the official go_router documentation.