This document explains the key design decisions in this Flutter Clean Architecture template, including the rationale behind each choice, alternatives considered, trade-offs, and when to reconsider.
This guide covers the rationale behind major technical decisions:
- Routing solution (go_router)
- State management (Riverpod)
- Error handling (Result pattern)
- Logging strategy
- Storage approach (dual storage)
- HTTP client (Dio)
Each decision includes problem statements, alternatives considered, chosen solution, trade-offs, and migration guides.
- Routing: go_router
- State Management: Riverpod
- Error Handling: Result Pattern
- Logging: Custom LoggingService
- Storage: Dual Storage System
- HTTP Client: Dio
- Comparison Tables
- Migration Guides
Flutter apps need a way to navigate between screens. The basic Navigator API is imperative and doesn't handle:
- Deep linking
- URL-based navigation
- Authentication-based routing
- Type-safe route definitions
- Declarative routing configuration
Pros:
- ✅ No dependencies
- ✅ Simple for basic navigation
- ✅ Built into Flutter
Cons:
- ❌ No deep linking support
- ❌ Imperative API (harder to reason about)
- ❌ No URL-based navigation
- ❌ Manual route management
- ❌ No type safety
When to use:
- Very simple apps with 2-3 screens
- No deep linking requirements
- Prototypes
Pros:
- ✅ Code generation (type-safe routes)
- ✅ Deep linking support
- ✅ Declarative configuration
- ✅ Good documentation
Cons:
- ❌ Code generation overhead
- ❌ Less flexible than go_router
- ❌ Smaller community
- ❌ Requires build_runner
When to use:
- Teams that prefer code generation
- Need strong type safety
- Don't mind build_runner
Pros:
- ✅ Declarative routing
- ✅ Deep linking built-in
- ✅ URL-based navigation
- ✅ Authentication redirects
- ✅ Active maintenance (Flutter team)
- ✅ No code generation needed
- ✅ Good performance
- ✅ Excellent documentation
Cons:
- ❌ Learning curve (different from Navigator)
- ❌ Some boilerplate for complex routes
When to use:
- Production apps
- Need deep linking
- Want declarative routing
- Prefer runtime configuration
go_router is used for routing in this template.
Rationale:
- Active Development: Maintained by the Flutter team, ensuring long-term support
- Deep Linking: Built-in support for web and mobile deep links
- Declarative: Routes defined in one place, easier to understand
- Authentication: Built-in redirect logic for protected routes
- No Code Generation: Faster development, no build_runner needed
- Performance: Efficient route matching and navigation
Implementation:
// lib/core/routing/app_router.dart
final goRouterProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: AppRoutes.home,
routes: [
GoRoute(
path: AppRoutes.login,
builder: (context, state) => const LoginScreen(),
),
// ... more routes
],
redirect: (context, state) {
// Authentication-based redirects
},
);
});Advantages:
- ✅ Declarative routing configuration
- ✅ Deep linking support
- ✅ Authentication redirects
- ✅ Active maintenance
Disadvantages:
⚠️ Different API from Navigator (learning curve)⚠️ Some boilerplate for complex nested routes⚠️ Requires understanding of GoRouter concepts
Consider alternatives if:
- Very simple app (< 3 screens) → Use basic Navigator
- Need code generation → Consider AutoRoute
- Team prefers imperative → Use Navigator with a wrapper
- Specific AutoRoute features → Evaluate AutoRoute
Flutter apps need a way to manage state across the widget tree. State management should:
- Handle complex state logic
- Provide dependency injection
- Support testing
- Be performant
- Have good developer experience
Pros:
- ✅ Simple API
- ✅ Official Flutter recommendation
- ✅ Good documentation
- ✅ Lightweight
Cons:
- ❌ Can have performance issues with complex state
- ❌ Less powerful than Riverpod
- ❌ No compile-time safety
- ❌ Limited dependency injection
When to use:
- Simple apps
- Team familiar with Provider
- Don't need advanced features
Pros:
- ✅ Event-driven architecture
- ✅ Predictable state changes
- ✅ Good for complex state
- ✅ Strong testing support
- ✅ Large community
Cons:
- ❌ More boilerplate (Events, States, Bloc classes)
- ❌ Steeper learning curve
- ❌ Can be overkill for simple state
- ❌ Requires understanding of streams
When to use:
- Complex state management
- Event-driven requirements
- Team familiar with BLoC
- Need strict state management
Pros:
- ✅ Compile-time safety
- ✅ Built-in dependency injection
- ✅ Excellent performance
- ✅ Less boilerplate than BLoC
- ✅ Great testing support
- ✅ Active development
- ✅ Works well with Clean Architecture
Cons:
- ❌ Learning curve (different from Provider)
- ❌ Requires understanding of providers
- ❌ Some concepts can be complex
When to use:
- Production apps
- Need dependency injection
- Want compile-time safety
- Prefer less boilerplate
Riverpod is used for state management in this template.
Rationale:
- Compile-time Safety: Catches errors at compile time, not runtime
- Dependency Injection: Built-in DI system, perfect for Clean Architecture
- Performance: Efficient rebuilds, only updates what changed
- Testing: Easy to override providers in tests
- Less Boilerplate: Simpler than BLoC for most use cases
- Clean Architecture: Works naturally with repository pattern and use cases
Implementation:
// Domain layer - use case
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
final repository = ref.watch(authRepositoryProvider);
return LoginUseCase(repository);
});
// Presentation layer - state management
class AuthNotifier extends Notifier<AuthState> {
@override
AuthState build() => const AuthState.initial();
Future<void> login(String email, String password) async {
final useCase = ref.read(loginUseCaseProvider);
final result = await useCase(email, password);
// Handle result
}
}Advantages:
- ✅ Compile-time safety
- ✅ Built-in dependency injection
- ✅ Excellent performance
- ✅ Less boilerplate than BLoC
- ✅ Great for Clean Architecture
Disadvantages:
⚠️ Learning curve (provider concepts)⚠️ Different from Provider (even though similar name)⚠️ Some advanced features can be complex
Consider alternatives if:
- Simple app → Provider might be sufficient
- Event-driven requirements → Consider BLoC
- Team familiar with BLoC → BLoC might be better
- Need Provider compatibility → Use Provider
Flutter apps need a way to handle errors from async operations. Traditional exception-based error handling:
- Doesn't force error handling (can be forgotten)
- Not type-safe
- Hard to distinguish error types
- Doesn't work well with functional programming
Pros:
- ✅ Familiar to most developers
- ✅ Simple try-catch
- ✅ Works with existing Dart code
Cons:
- ❌ Not type-safe (can forget to catch)
- ❌ Doesn't force error handling
- ❌ Hard to distinguish error types
- ❌ Not functional-friendly
When to use:
- Simple error handling
- Team unfamiliar with Result pattern
- Quick prototypes
Pros:
- ✅ Functional programming approach
- ✅ Type-safe error handling
- ✅ Forces error handling
- ✅ Good for functional codebases
Cons:
- ❌ Requires functional programming knowledge
- ❌ Less familiar to most developers
- ❌ Additional dependency (fpdart)
- ❌ Can be verbose
When to use:
- Functional programming codebase
- Team familiar with functional patterns
- Want strong functional guarantees
Pros:
- ✅ Type-safe error handling
- ✅ Forces error handling (can't ignore errors)
- ✅ Clear success/failure distinction
- ✅ Works with pattern matching (Dart 3.0)
- ✅ No external dependencies
- ✅ Familiar to developers from other languages (Rust, Swift)
Cons:
- ❌ Different from exceptions (learning curve)
- ❌ Requires consistent usage
- ❌ Some boilerplate
When to use:
- Production apps
- Want type-safe error handling
- Prefer explicit error handling
- Using Dart 3.0+ pattern matching
Result Pattern is used for error handling in this template.
Rationale:
- Type Safety: Compiler forces error handling
- Explicit: Success and failure are explicit in the type
- Pattern Matching: Works great with Dart 3.0 sealed classes
- No Dependencies: Pure Dart implementation
- Clean Architecture: Fits well with use cases and repositories
Implementation:
// Result type
sealed class Result<T> {
const Result();
}
final class Success<T> extends Result<T> {
const Success(this.data);
final T data;
}
final class ResultFailure<T> extends Result<T> {
const ResultFailure(this.failure);
final Failure failure;
}
// Usage in use case
class LoginUseCase {
Future<Result<User>> call(String email, String password) async {
try {
final user = await repository.login(email, password);
return Success(user);
} on AuthException catch (e) {
return ResultFailure(AuthFailure(e.message));
}
}
}
// Usage in UI
final result = await loginUseCase(email, password);
result.when(
success: (user) => navigateToHome(),
failureCallback: (failure) => showError(failure.message),
);Advantages:
- ✅ Type-safe error handling
- ✅ Forces explicit error handling
- ✅ Works with pattern matching
- ✅ No external dependencies
- ✅ Clear success/failure distinction
Disadvantages:
⚠️ Different from exceptions (learning curve)⚠️ Requires consistent usage across codebase⚠️ Some boilerplate for error mapping
Consider alternatives if:
- Simple error handling → Exceptions might be sufficient
- Functional programming → Consider Either pattern
- Team unfamiliar with Result → Start with exceptions, migrate later
- Legacy codebase → Exceptions might be easier to integrate
Apps need logging for:
- Debugging during development
- Error tracking in production
- Performance monitoring
- User behavior analytics
Logging should:
- Support multiple outputs (console, file, remote)
- Be configurable per environment
- Have different log levels
- Be performant
- Support structured logging
Pros:
- ✅ No dependencies
- ✅ Simple
- ✅ Built into Dart
Cons:
- ❌ No log levels
- ❌ No file logging
- ❌ No structured logging
- ❌ Can't disable in production
- ❌ Poor performance
When to use:
- Quick debugging
- Prototypes
- Very simple apps
Pros:
- ✅ Built into Flutter
- ✅ Better than print()
- ✅ Supports log levels
Cons:
- ❌ No file logging
- ❌ No remote logging
- ❌ Limited features
- ❌ Not structured
When to use:
- Simple logging needs
- Don't need file/remote logging
- Want built-in solution
Pros:
- ✅ Multiple outputs (console, file, remote)
- ✅ Log levels (debug, info, warning, error)
- ✅ Structured logging
- ✅ Configurable per environment
- ✅ File rotation
- ✅ Good performance
- ✅ Extensible
Cons:
- ❌ Requires custom implementation
- ❌ Some setup needed
- ❌ Additional dependency
When to use:
- Production apps
- Need file/remote logging
- Want structured logging
- Need environment-specific configuration
Custom LoggingService using the logger package is used in this template.
Rationale:
- Flexibility: Can add multiple outputs (console, file, remote)
- Environment-aware: Different behavior in dev vs production
- Structured Logging: Supports context/metadata
- File Logging: Logs to files for production debugging
- Performance: Can be disabled in production
- Extensible: Easy to add remote logging (e.g., Sentry, Firebase)
Implementation:
// LoggingService with multiple outputs
class LoggingService {
LoggingService({
bool? enableLogging,
bool? enableFileLogging,
bool? enableRemoteLogging,
}) {
final outputs = <LogOutput>[];
if (kDebugMode) {
outputs.add(ConsoleOutput());
}
if (enableFileLogging) {
outputs.add(FileLogOutput());
}
if (enableRemoteLogging) {
outputs.add(RemoteLogOutput());
}
_logger = Logger(
output: MultiOutput(outputs),
printer: AppConfig.isProduction
? JsonLogFormatter()
: PrettyPrinter(),
);
}
void debug(String message, {Map<String, dynamic>? context}) {
_logger.d(_formatMessage(message, context));
}
// ... other log levels
}Advantages:
- ✅ Multiple outputs
- ✅ Environment-aware
- ✅ Structured logging
- ✅ File rotation
- ✅ Extensible
Disadvantages:
⚠️ Custom implementation needed⚠️ Some setup required⚠️ Additional dependency
Consider alternatives if:
- Very simple logging → Use developer.log()
- No file logging needed → Use developer.log()
- Want built-in only → Use developer.log()
- Need specific logging service → Integrate directly (e.g., Sentry)
Apps need to store data locally. Different data has different security requirements:
- Sensitive data (tokens, passwords) → Needs encryption
- Non-sensitive data (preferences, cache) → Can use simple storage
Using one storage solution for everything is either:
- Overkill (encrypting everything)
- Insecure (storing tokens in plain text)
Pros:
- ✅ Simple API
- ✅ No encryption overhead
- ✅ Fast
Cons:
- ❌ Not secure (plain text)
- ❌ Can't store sensitive data safely
- ❌ Security risk
When to use:
- No sensitive data
- Prototypes
- Internal tools
Pros:
- ✅ Secure for all data
- ✅ Encrypted storage
Cons:
- ❌ Slower (encryption overhead)
- ❌ Overkill for non-sensitive data
- ❌ More complex API
When to use:
- All data is sensitive
- Security is top priority
- Don't mind performance impact
Pros:
- ✅ Right tool for the job
- ✅ Secure for sensitive data
- ✅ Fast for non-sensitive data
- ✅ Clear separation of concerns
- ✅ Unified interface (IStorageService)
Cons:
- ❌ Need to choose which storage to use
- ❌ Two storage systems to manage
- ❌ Some complexity
When to use:
- Production apps
- Mix of sensitive and non-sensitive data
- Want optimal performance and security
Dual Storage System is used in this template:
- StorageService (SharedPreferences) → Non-sensitive data
- SecureStorageService (flutter_secure_storage) → Sensitive data
Rationale:
- Security: Sensitive data is encrypted
- Performance: Non-sensitive data is fast
- Unified Interface: Both implement
IStorageService - Clear Separation: Easy to know which to use
- Best of Both: Security where needed, performance where possible
Implementation:
// Unified interface
abstract class IStorageService {
Future<String?> getString(String key);
Future<bool> setString(String key, String value);
// ... other methods
}
// Non-sensitive storage
class StorageService implements IStorageService {
// Uses SharedPreferences
}
// Sensitive storage
class SecureStorageService implements IStorageService {
// Uses flutter_secure_storage
// Android: EncryptedSharedPreferences
// iOS: Keychain
}
// Usage
// Non-sensitive
final storage = ref.read(storageServiceProvider);
await storage.setString('theme', 'dark');
// Sensitive
final secureStorage = ref.read(secureStorageServiceProvider);
await secureStorage.setString('auth_token', token);Advantages:
- ✅ Secure for sensitive data
- ✅ Fast for non-sensitive data
- ✅ Unified interface
- ✅ Clear separation
Disadvantages:
⚠️ Need to choose which storage⚠️ Two systems to manage⚠️ Some complexity
Consider alternatives if:
- No sensitive data → Use SharedPreferences only
- All data sensitive → Use SecureStorage only
- Want simplicity → Choose one, accept trade-offs
Apps need to make HTTP requests. The basic http package:
- Limited interceptor support
- No request/response transformation
- Basic error handling
- No built-in retry logic
Pros:
- ✅ No dependencies
- ✅ Simple API
- ✅ Built into Flutter
Cons:
- ❌ Limited interceptor support
- ❌ Basic error handling
- ❌ No request/response transformation
- ❌ No retry logic
When to use:
- Simple HTTP requests
- No interceptors needed
- Want minimal dependencies
Pros:
- ✅ Powerful interceptor system
- ✅ Request/response transformation
- ✅ Built-in retry logic
- ✅ Good error handling
- ✅ Cancel tokens
- ✅ Form data support
- ✅ Active maintenance
Cons:
- ❌ Additional dependency
- ❌ More complex than http
- ❌ Learning curve
When to use:
- Production apps
- Need interceptors
- Want advanced features
- Need retry logic
Dio is used as the HTTP client in this template.
Rationale:
- Interceptors: Perfect for auth, logging, caching, error handling
- Error Handling: Easy to convert DioException to domain exceptions
- Retry Logic: Built-in support for retrying failed requests
- Active Maintenance: Well-maintained package
- Features: Cancel tokens, form data, file uploads
Implementation:
// ApiClient with interceptors
class ApiClient {
ApiClient({
required StorageService storageService,
required SecureStorageService secureStorageService,
required AuthInterceptor authInterceptor,
LoggingService? loggingService,
}) : _dio = _createDio(
storageService,
secureStorageService,
authInterceptor,
loggingService,
);
static Dio _createDio(...) {
final dio = Dio(BaseOptions(
baseUrl: AppConfig.baseUrl,
connectTimeout: Duration(seconds: AppConfig.apiConnectTimeout),
));
// Add interceptors
dio.interceptors.addAll([
ErrorInterceptor(), // Convert to domain exceptions
PerformanceInterceptor(), // Track performance
CacheInterceptor(), // Cache responses
AuthInterceptor(), // Add auth tokens
ApiLoggingInterceptor(), // Log requests/responses
]);
return dio;
}
}Advantages:
- ✅ Powerful interceptor system
- ✅ Good error handling
- ✅ Retry logic
- ✅ Active maintenance
Disadvantages:
⚠️ Additional dependency⚠️ More complex than http⚠️ Learning curve
Consider alternatives if:
- Simple HTTP requests → Use http package
- No interceptors needed → http might be sufficient
- Want minimal dependencies → Use http package
| Feature | Navigator | go_router | AutoRoute |
|---|---|---|---|
| Deep Linking | ❌ Manual | ✅ Built-in | ✅ Built-in |
| Type Safety | ❌ | ✅ Full (code gen) | |
| Declarative | ❌ | ✅ | ✅ |
| Auth Redirects | ❌ Manual | ✅ Built-in | ✅ Built-in |
| Code Generation | ❌ | ❌ | ✅ Required |
| Learning Curve | ✅ Low | ||
| Dependencies | ✅ None | ||
| Maintenance | ✅ Flutter team | ✅ Flutter team | |
| Best For | Simple apps | Production apps | Type-safe apps |
| Feature | Provider | Riverpod | BLoC |
|---|---|---|---|
| Compile-time Safety | ❌ | ✅ | |
| Dependency Injection | ✅ Built-in | ||
| Boilerplate | ✅ Low | ❌ High | |
| Learning Curve | ✅ Low | ❌ High | |
| Performance | ✅ Excellent | ✅ Excellent | |
| Testing | ✅ Easy | ✅ Easy | ✅ Easy |
| Event-driven | ❌ | ❌ | ✅ |
| Best For | Simple apps | Production apps | Complex state |
| Feature | Exceptions | Result Pattern | Either Pattern |
|---|---|---|---|
| Type Safety | ❌ | ✅ | ✅ |
| Forces Handling | ❌ | ✅ | ✅ |
| Familiar | ✅ | ❌ Low | |
| Dependencies | ✅ None | ✅ None | |
| Pattern Matching | ✅ Full | ✅ Full | |
| Functional | ❌ | ✅ Full | |
| Best For | Simple apps | Production apps | Functional codebases |
-
Remove go_router dependency
# pubspec.yaml dependencies: # go_router: ^17.0.0 # Remove this
-
Create Navigator wrapper
// lib/core/routing/navigator_service.dart class NavigatorService { static void push(BuildContext context, Widget screen) { Navigator.push(context, MaterialPageRoute(builder: (_) => screen)); } static void pushReplacement(BuildContext context, Widget screen) { Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => screen), ); } }
-
Update navigation calls
// Before (go_router) context.go(AppRoutes.home); // After (Navigator) NavigatorService.push(context, HomeScreen());
-
Remove GoRouter setup
- Remove
goRouterProviderfromlib/core/routing/app_router.dart - Update
main.dartto useMaterialAppinstead ofMaterialApp.router
- Remove
-
Add AutoRoute dependencies
dependencies: auto_route: ^7.0.0 dev_dependencies: auto_route_generator: ^7.0.0 build_runner: ^2.4.0
-
Create route definitions
// lib/core/routing/app_router.gr.dart (generated) @AutoRouterConfig() class AppRouter extends _$AppRouter { @override List<AutoRoute> get routes => [ AutoRoute(page: LoginRoute.page, initial: true), AutoRoute(page: HomeRoute.page), ]; }
-
Update navigation
// Before (go_router) context.go(AppRoutes.home); // After (AutoRoute) context.router.push(const HomeRoute());
-
Replace dependencies
dependencies: # flutter_riverpod: ^3.0.3 # Remove provider: ^6.0.0 # Add
-
Convert providers
// Before (Riverpod) final loginUseCaseProvider = Provider<LoginUseCase>((ref) { final repository = ref.watch(authRepositoryProvider); return LoginUseCase(repository); }); // After (Provider) final loginUseCaseProvider = Provider<LoginUseCase>((ref) { final repository = ref.watch(authRepositoryProvider); return LoginUseCase(repository); });
-
Update widgets
// Before (Riverpod) class LoginScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final useCase = ref.watch(loginUseCaseProvider); } } // After (Provider) class LoginScreen extends StatelessWidget { @override Widget build(BuildContext context) { final useCase = Provider.of<LoginUseCase>(context); } }
-
Update main.dart
// Before (Riverpod) runApp( const ProviderScope( child: MyApp(), ), ); // After (Provider) runApp( MultiProvider( providers: [ Provider<LoginUseCase>(create: (_) => LoginUseCase(...)), ], child: const MyApp(), ), );
See the comprehensive migration guide: docs/guides/migration/from-bloc-to-riverpod.md (reverse the steps)
-
Replace dependencies
dependencies: # dio: ^5.9.0 # Remove http: ^1.0.0 # Add
-
Create HTTP client wrapper
// lib/core/network/api_client.dart import 'package:http/http.dart' as http; class ApiClient { final String baseUrl; ApiClient({required this.baseUrl}); Future<http.Response> get(String path) async { final uri = Uri.parse('$baseUrl$path'); return await http.get(uri); } // ... other methods }
-
Remove interceptors
- Interceptors are Dio-specific
- Implement equivalent logic in ApiClient methods
- Or use middleware pattern
-
Update error handling
// Before (Dio) try { return await _dio.get(path); } on DioException catch (e) { throw ServerException(e.message); } // After (http) try { final response = await http.get(uri); if (response.statusCode >= 400) { throw ServerException('Server error', statusCode: response.statusCode); } return response; } catch (e) { throw NetworkException(e.toString()); }
-
Update use cases
// Before (Result Pattern) Future<Result<User>> call(String email, String password) async { try { final user = await repository.login(email, password); return Success(user); } on AuthException catch (e) { return ResultFailure(AuthFailure(e.message)); } } // After (Exceptions) Future<User> call(String email, String password) async { return await repository.login(email, password); // Exceptions bubble up automatically }
-
Update UI handling
// Before (Result Pattern) final result = await loginUseCase(email, password); result.when( success: (user) => navigateToHome(), failureCallback: (failure) => showError(failure.message), ); // After (Exceptions) try { final user = await loginUseCase(email, password); navigateToHome(); } on AuthException catch (e) { showError(e.message); } catch (e) { showError('Unexpected error'); }
-
Remove Result type
- Remove
lib/core/utils/result.dart - Update all repository interfaces
- Update all use cases
- Remove
-
Add fpdart dependency
dependencies: fpdart: ^1.0.0
-
Update Result to Either
// Before (Result Pattern) sealed class Result<T> { const Result(); } // After (Either Pattern) import 'package:fpdart/fpdart.dart'; // Use Either<Failure, T> instead of Result<T>
-
Update use cases
// Before (Result Pattern) Future<Result<User>> call(String email, String password) async { try { final user = await repository.login(email, password); return Success(user); } on AuthException catch (e) { return ResultFailure(AuthFailure(e.message)); } } // After (Either Pattern) Future<Either<Failure, User>> call(String email, String password) async { try { final user = await repository.login(email, password); return Right(user); } on AuthException catch (e) { return Left(AuthFailure(e.message)); } }
-
Update UI handling
// Before (Result Pattern) result.when( success: (user) => navigateToHome(), failureCallback: (failure) => showError(failure.message), ); // After (Either Pattern) result.fold( (failure) => showError(failure.message), (user) => navigateToHome(), );
This template makes deliberate choices for each major decision:
- Routing: go_router for declarative, deep-linkable routing
- State Management: Riverpod for compile-time safety and DI
- Error Handling: Result pattern for type-safe error handling
- Logging: Custom LoggingService for flexibility
- Storage: Dual system for optimal security and performance
- HTTP Client: Dio for powerful interceptors
Each decision includes:
- ✅ Problem statement
- ✅ Alternatives considered
- ✅ Chosen solution and rationale
- ✅ Trade-offs
- ✅ When to reconsider
Use this template as a starting point, understand the decisions, and adapt as needed for your specific requirements.
- Architecture Overview - Why Clean Architecture, benefits, trade-offs, and learning resources
- Understanding the Codebase - Architecture and code organization
- Common Patterns - Common usage patterns and best practices
- Routing Guide - GoRouter navigation and deep linking
- Migration Guides - Guides for migrating from other architectures
- API Documentation - Complete API reference