init cc
This commit is contained in:
413
lib/core/routing/app_router.dart
Normal file
413
lib/core/routing/app_router.dart
Normal file
@@ -0,0 +1,413 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'route_names.dart';
|
||||
import 'route_paths.dart';
|
||||
import 'route_guards.dart';
|
||||
import 'error_page.dart';
|
||||
import '../../features/home/presentation/pages/home_page.dart';
|
||||
import '../../features/settings/presentation/pages/settings_page.dart';
|
||||
import '../../features/todos/presentation/screens/home_screen.dart';
|
||||
|
||||
/// GoRouter provider for the entire app
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
final authState = ref.watch(authStateProvider);
|
||||
|
||||
return GoRouter(
|
||||
debugLogDiagnostics: true,
|
||||
initialLocation: RoutePaths.home,
|
||||
redirect: (context, state) {
|
||||
return RouteGuard.getRedirectPath(state.fullPath ?? '', authState);
|
||||
},
|
||||
routes: [
|
||||
// Home route
|
||||
GoRoute(
|
||||
path: RoutePaths.home,
|
||||
name: RouteNames.home,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const HomePage(),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
|
||||
// Settings routes with nested navigation
|
||||
GoRoute(
|
||||
path: RoutePaths.settings,
|
||||
name: RouteNames.settings,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const SettingsPage(),
|
||||
state: state,
|
||||
),
|
||||
routes: [
|
||||
// Settings sub-pages
|
||||
GoRoute(
|
||||
path: '/theme',
|
||||
name: RouteNames.settingsTheme,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const ThemeSettingsPage(),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/general',
|
||||
name: RouteNames.settingsGeneral,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const _PlaceholderPage(title: 'General Settings'),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/privacy',
|
||||
name: RouteNames.settingsPrivacy,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const _PlaceholderPage(title: 'Privacy Settings'),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notifications',
|
||||
name: RouteNames.settingsNotifications,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const _PlaceholderPage(title: 'Notification Settings'),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Profile route
|
||||
GoRoute(
|
||||
path: RoutePaths.profile,
|
||||
name: RouteNames.profile,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const _PlaceholderPage(title: 'Profile'),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
|
||||
// About route
|
||||
GoRoute(
|
||||
path: RoutePaths.about,
|
||||
name: RouteNames.about,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const _AboutPage(),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
|
||||
// Auth routes
|
||||
GoRoute(
|
||||
path: RoutePaths.login,
|
||||
name: RouteNames.login,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const _PlaceholderPage(title: 'Login'),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: RoutePaths.register,
|
||||
name: RouteNames.register,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const _PlaceholderPage(title: 'Register'),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: RoutePaths.forgotPassword,
|
||||
name: RouteNames.forgotPassword,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const _PlaceholderPage(title: 'Forgot Password'),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
|
||||
// Todo routes (keeping existing functionality)
|
||||
GoRoute(
|
||||
path: RoutePaths.todos,
|
||||
name: RouteNames.todos,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const HomeScreen(), // Using existing TodoScreen
|
||||
state: state,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/add',
|
||||
name: RouteNames.addTodo,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const _PlaceholderPage(title: 'Add Todo'),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:id',
|
||||
name: RouteNames.todoDetails,
|
||||
pageBuilder: (context, state) {
|
||||
final id = state.pathParameters['id']!;
|
||||
return _buildPageWithTransition(
|
||||
child: _PlaceholderPage(title: 'Todo Details: $id'),
|
||||
state: state,
|
||||
);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/edit',
|
||||
name: RouteNames.editTodo,
|
||||
pageBuilder: (context, state) {
|
||||
final id = state.pathParameters['id']!;
|
||||
return _buildPageWithTransition(
|
||||
child: _PlaceholderPage(title: 'Edit Todo: $id'),
|
||||
state: state,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Onboarding routes
|
||||
GoRoute(
|
||||
path: RoutePaths.onboarding,
|
||||
name: RouteNames.onboarding,
|
||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||
child: const _PlaceholderPage(title: 'Onboarding'),
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
],
|
||||
errorPageBuilder: (context, state) => MaterialPage<void>(
|
||||
key: state.pageKey,
|
||||
child: ErrorPage(
|
||||
error: state.error.toString(),
|
||||
path: state.fullPath,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
/// Helper function to build pages with transitions
|
||||
Page<T> _buildPageWithTransition<T>({
|
||||
required Widget child,
|
||||
required GoRouterState state,
|
||||
Duration transitionDuration = const Duration(milliseconds: 250),
|
||||
}) {
|
||||
return CustomTransitionPage<T>(
|
||||
key: state.pageKey,
|
||||
child: child,
|
||||
transitionDuration: transitionDuration,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
// Slide transition from right to left
|
||||
const begin = Offset(1.0, 0.0);
|
||||
const end = Offset.zero;
|
||||
const curve = Curves.ease;
|
||||
|
||||
var tween = Tween(begin: begin, end: end).chain(
|
||||
CurveTween(curve: curve),
|
||||
);
|
||||
|
||||
return SlideTransition(
|
||||
position: animation.drive(tween),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Extension methods for GoRouter navigation
|
||||
extension GoRouterExtension on GoRouter {
|
||||
/// Navigate to a named route with parameters
|
||||
void goNamed(
|
||||
String name, {
|
||||
Map<String, String> pathParameters = const {},
|
||||
Map<String, dynamic> queryParameters = const {},
|
||||
Object? extra,
|
||||
}) {
|
||||
pushNamed(
|
||||
name,
|
||||
pathParameters: pathParameters,
|
||||
queryParameters: queryParameters,
|
||||
extra: extra,
|
||||
);
|
||||
}
|
||||
|
||||
/// Check if we can pop the current route
|
||||
bool get canPop => routerDelegate.currentConfiguration.matches.length > 1;
|
||||
}
|
||||
|
||||
/// Extension methods for BuildContext navigation
|
||||
extension BuildContextGoRouterExtension on BuildContext {
|
||||
/// Get the GoRouter instance
|
||||
GoRouter get router => GoRouter.of(this);
|
||||
|
||||
/// Navigate with typed route names
|
||||
void goToHome() => go(RoutePaths.home);
|
||||
void goToSettings() => go(RoutePaths.settings);
|
||||
void goToLogin() => go(RoutePaths.login);
|
||||
void goToProfile() => go(RoutePaths.profile);
|
||||
|
||||
/// Navigate to todo details with ID
|
||||
void goToTodoDetails(String id) => go(RoutePaths.todoDetailsPath(id));
|
||||
|
||||
/// Navigate to edit todo with ID
|
||||
void goToEditTodo(String id) => go(RoutePaths.editTodoPath(id));
|
||||
|
||||
/// Push with typed route names
|
||||
void pushHome() => push(RoutePaths.home);
|
||||
void pushSettings() => push(RoutePaths.settings);
|
||||
void pushLogin() => push(RoutePaths.login);
|
||||
void pushProfile() => push(RoutePaths.profile);
|
||||
|
||||
/// Get current route information
|
||||
String get currentPath => GoRouterState.of(this).fullPath ?? '/';
|
||||
String get currentName => GoRouterState.of(this).name ?? '';
|
||||
Map<String, String> get pathParameters => GoRouterState.of(this).pathParameters;
|
||||
Map<String, dynamic> get queryParameters => GoRouterState.of(this).uri.queryParameters;
|
||||
}
|
||||
|
||||
/// About page implementation
|
||||
class _AboutPage extends StatelessWidget {
|
||||
const _AboutPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('About'),
|
||||
),
|
||||
body: const SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.flutter_dash,
|
||||
size: 80,
|
||||
color: Colors.blue,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Base Flutter App',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Version 1.0.0+1',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 32),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'About This App',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'A foundational Flutter application with clean architecture, '
|
||||
'state management using Riverpod, local storage with Hive, '
|
||||
'and navigation using GoRouter.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Features',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text('• Clean Architecture with feature-first structure'),
|
||||
Text('• State management with Riverpod'),
|
||||
Text('• Local storage with Hive'),
|
||||
Text('• Navigation with GoRouter'),
|
||||
Text('• Material 3 design system'),
|
||||
Text('• Theme switching (Light/Dark/System)'),
|
||||
Text('• Secure storage for sensitive data'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Placeholder page for routes that aren't implemented yet
|
||||
class _PlaceholderPage extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const _PlaceholderPage({
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.construction,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'This page is coming soon!',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: () => context.pop(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
label: const Text('Go Back'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
271
lib/core/routing/error_page.dart
Normal file
271
lib/core/routing/error_page.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'route_paths.dart';
|
||||
|
||||
/// 404 Error page widget
|
||||
class ErrorPage extends StatelessWidget {
|
||||
final String? error;
|
||||
final String? path;
|
||||
|
||||
const ErrorPage({
|
||||
super.key,
|
||||
this.error,
|
||||
this.path,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Page Not Found'),
|
||||
backgroundColor: Theme.of(context).colorScheme.errorContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Error illustration
|
||||
Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
size: 96,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Error title
|
||||
Text(
|
||||
'404',
|
||||
style: Theme.of(context).textTheme.displayLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Error message
|
||||
Text(
|
||||
'Page Not Found',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Error description
|
||||
Text(
|
||||
'The page you are looking for doesn\'t exist or has been moved.',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
if (path != null) ...[
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Requested Path:',
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
path!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (error != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
ExpansionTile(
|
||||
title: Text(
|
||||
'Error Details',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
error!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Go back button
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
context.pop();
|
||||
} else {
|
||||
context.go(RoutePaths.home);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
label: const Text('Go Back'),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Home button
|
||||
FilledButton.icon(
|
||||
onPressed: () => context.go(RoutePaths.home),
|
||||
icon: const Icon(Icons.home),
|
||||
label: const Text('Home'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Help text
|
||||
Text(
|
||||
'If this problem persists, please contact support.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Network error page widget
|
||||
class NetworkErrorPage extends StatelessWidget {
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
const NetworkErrorPage({
|
||||
super.key,
|
||||
this.onRetry,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Connection Error'),
|
||||
backgroundColor: Theme.of(context).colorScheme.errorContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Error illustration
|
||||
Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.wifi_off,
|
||||
size: 96,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Error title
|
||||
Text(
|
||||
'No Connection',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Error description
|
||||
Text(
|
||||
'Please check your internet connection and try again.',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (onRetry != null) ...[
|
||||
FilledButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.go(RoutePaths.home),
|
||||
icon: const Icon(Icons.home),
|
||||
label: const Text('Home'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
312
lib/core/routing/navigation_shell.dart
Normal file
312
lib/core/routing/navigation_shell.dart
Normal file
@@ -0,0 +1,312 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'route_paths.dart';
|
||||
import 'route_guards.dart';
|
||||
|
||||
/// Navigation shell with bottom navigation or navigation drawer
|
||||
class NavigationShell extends ConsumerStatefulWidget {
|
||||
final Widget child;
|
||||
final GoRouterState state;
|
||||
|
||||
const NavigationShell({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.state,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<NavigationShell> createState() => _NavigationShellState();
|
||||
}
|
||||
|
||||
class _NavigationShellState extends ConsumerState<NavigationShell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = ref.watch(authStateProvider);
|
||||
final currentPath = widget.state.fullPath ?? '/';
|
||||
|
||||
// Determine if we should show bottom navigation
|
||||
final showBottomNav = _shouldShowBottomNavigation(currentPath, authState);
|
||||
|
||||
if (!showBottomNav) {
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
// Get current navigation index
|
||||
final currentIndex = _getCurrentNavigationIndex(currentPath);
|
||||
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: currentIndex,
|
||||
onDestinationSelected: (index) => _onNavigationTapped(context, index),
|
||||
destinations: _getNavigationDestinations(authState),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Determine if bottom navigation should be shown
|
||||
bool _shouldShowBottomNavigation(String path, AuthState authState) {
|
||||
// Don't show on auth pages
|
||||
if (RoutePaths.isAuthPath(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't show on onboarding
|
||||
if (path.startsWith(RoutePaths.onboarding) || path.startsWith(RoutePaths.welcome)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't show on error pages
|
||||
if (path.startsWith(RoutePaths.error) || path.startsWith(RoutePaths.notFound)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't show on specific detail pages
|
||||
final hideOnPaths = [
|
||||
'/todos/add',
|
||||
'/todos/',
|
||||
'/settings/',
|
||||
];
|
||||
|
||||
for (final hidePath in hideOnPaths) {
|
||||
if (path.contains(hidePath) && path != RoutePaths.todos && path != RoutePaths.settings) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Get current navigation index based on path
|
||||
int _getCurrentNavigationIndex(String path) {
|
||||
if (path.startsWith(RoutePaths.todos)) {
|
||||
return 1;
|
||||
} else if (path.startsWith(RoutePaths.settings)) {
|
||||
return 2;
|
||||
} else if (path.startsWith(RoutePaths.profile)) {
|
||||
return 3;
|
||||
}
|
||||
return 0; // Home
|
||||
}
|
||||
|
||||
/// Get navigation destinations based on auth state
|
||||
List<NavigationDestination> _getNavigationDestinations(AuthState authState) {
|
||||
return [
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.check_circle_outline),
|
||||
selectedIcon: Icon(Icons.check_circle),
|
||||
label: 'Todos',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
selectedIcon: Icon(Icons.settings),
|
||||
label: 'Settings',
|
||||
),
|
||||
if (authState == AuthState.authenticated)
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.person_outline),
|
||||
selectedIcon: Icon(Icons.person),
|
||||
label: 'Profile',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Handle navigation tap
|
||||
void _onNavigationTapped(BuildContext context, int index) {
|
||||
final authState = ref.read(authStateProvider);
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
if (GoRouterState.of(context).fullPath != RoutePaths.home) {
|
||||
context.go(RoutePaths.home);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (!GoRouterState.of(context).fullPath!.startsWith(RoutePaths.todos)) {
|
||||
context.go(RoutePaths.todos);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (!GoRouterState.of(context).fullPath!.startsWith(RoutePaths.settings)) {
|
||||
context.go(RoutePaths.settings);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if (authState == AuthState.authenticated) {
|
||||
if (GoRouterState.of(context).fullPath != RoutePaths.profile) {
|
||||
context.go(RoutePaths.profile);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adaptive navigation shell that changes based on screen size
|
||||
class AdaptiveNavigationShell extends ConsumerWidget {
|
||||
final Widget child;
|
||||
final GoRouterState state;
|
||||
|
||||
const AdaptiveNavigationShell({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.state,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
// Use rail navigation on tablets and desktop
|
||||
if (screenWidth >= 840) {
|
||||
return _NavigationRailShell(
|
||||
child: child,
|
||||
state: state,
|
||||
);
|
||||
}
|
||||
|
||||
// Use bottom navigation on mobile
|
||||
return NavigationShell(
|
||||
child: child,
|
||||
state: state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigation rail for larger screens
|
||||
class _NavigationRailShell extends ConsumerStatefulWidget {
|
||||
final Widget child;
|
||||
final GoRouterState state;
|
||||
|
||||
const _NavigationRailShell({
|
||||
required this.child,
|
||||
required this.state,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_NavigationRailShell> createState() => _NavigationRailShellState();
|
||||
}
|
||||
|
||||
class _NavigationRailShellState extends ConsumerState<_NavigationRailShell> {
|
||||
bool isExtended = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = ref.watch(authStateProvider);
|
||||
final currentPath = widget.state.fullPath ?? '/';
|
||||
|
||||
// Determine if we should show navigation rail
|
||||
final showNavRail = _shouldShowNavigationRail(currentPath, authState);
|
||||
|
||||
if (!showNavRail) {
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
// Get current navigation index
|
||||
final currentIndex = _getCurrentNavigationIndex(currentPath);
|
||||
|
||||
return Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
NavigationRail(
|
||||
extended: isExtended,
|
||||
selectedIndex: currentIndex,
|
||||
onDestinationSelected: (index) => _onNavigationTapped(context, index),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isExtended = !isExtended;
|
||||
});
|
||||
},
|
||||
),
|
||||
destinations: _getNavigationDestinations(authState),
|
||||
),
|
||||
const VerticalDivider(thickness: 1, width: 1),
|
||||
Expanded(child: widget.child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _shouldShowNavigationRail(String path, AuthState authState) {
|
||||
// Same logic as bottom navigation
|
||||
if (RoutePaths.isAuthPath(path)) return false;
|
||||
if (path.startsWith(RoutePaths.onboarding) || path.startsWith(RoutePaths.welcome)) return false;
|
||||
if (path.startsWith(RoutePaths.error) || path.startsWith(RoutePaths.notFound)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int _getCurrentNavigationIndex(String path) {
|
||||
if (path.startsWith(RoutePaths.todos)) {
|
||||
return 1;
|
||||
} else if (path.startsWith(RoutePaths.settings)) {
|
||||
return 2;
|
||||
} else if (path.startsWith(RoutePaths.profile)) {
|
||||
return 3;
|
||||
}
|
||||
return 0; // Home
|
||||
}
|
||||
|
||||
List<NavigationRailDestination> _getNavigationDestinations(AuthState authState) {
|
||||
return [
|
||||
const NavigationRailDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: Text('Home'),
|
||||
),
|
||||
const NavigationRailDestination(
|
||||
icon: Icon(Icons.check_circle_outline),
|
||||
selectedIcon: Icon(Icons.check_circle),
|
||||
label: Text('Todos'),
|
||||
),
|
||||
const NavigationRailDestination(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
selectedIcon: Icon(Icons.settings),
|
||||
label: Text('Settings'),
|
||||
),
|
||||
if (authState == AuthState.authenticated)
|
||||
const NavigationRailDestination(
|
||||
icon: Icon(Icons.person_outline),
|
||||
selectedIcon: Icon(Icons.person),
|
||||
label: Text('Profile'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _onNavigationTapped(BuildContext context, int index) {
|
||||
final authState = ref.read(authStateProvider);
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
if (GoRouterState.of(context).fullPath != RoutePaths.home) {
|
||||
context.go(RoutePaths.home);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (!GoRouterState.of(context).fullPath!.startsWith(RoutePaths.todos)) {
|
||||
context.go(RoutePaths.todos);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (!GoRouterState.of(context).fullPath!.startsWith(RoutePaths.settings)) {
|
||||
context.go(RoutePaths.settings);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if (authState == AuthState.authenticated) {
|
||||
if (GoRouterState.of(context).fullPath != RoutePaths.profile) {
|
||||
context.go(RoutePaths.profile);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
168
lib/core/routing/route_guards.dart
Normal file
168
lib/core/routing/route_guards.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'route_paths.dart';
|
||||
|
||||
/// Authentication state provider
|
||||
final authStateProvider = StateNotifierProvider<AuthStateNotifier, AuthState>(
|
||||
(ref) => AuthStateNotifier(),
|
||||
);
|
||||
|
||||
/// Authentication state
|
||||
enum AuthState {
|
||||
unknown,
|
||||
authenticated,
|
||||
unauthenticated,
|
||||
}
|
||||
|
||||
/// Authentication state notifier
|
||||
class AuthStateNotifier extends StateNotifier<AuthState> {
|
||||
AuthStateNotifier() : super(AuthState.unknown) {
|
||||
_checkInitialAuth();
|
||||
}
|
||||
|
||||
Future<void> _checkInitialAuth() async {
|
||||
// TODO: Implement actual auth check logic
|
||||
// For now, simulate checking stored auth token
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Mock authentication check
|
||||
// In a real app, you would check secure storage for auth token
|
||||
state = AuthState.unauthenticated;
|
||||
}
|
||||
|
||||
Future<void> login(String email, String password) async {
|
||||
// TODO: Implement actual login logic
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
state = AuthState.authenticated;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
// TODO: Implement actual logout logic
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
state = AuthState.unauthenticated;
|
||||
}
|
||||
|
||||
Future<void> register(String email, String password) async {
|
||||
// TODO: Implement actual registration logic
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
state = AuthState.authenticated;
|
||||
}
|
||||
}
|
||||
|
||||
/// Route guard utility class
|
||||
class RouteGuard {
|
||||
/// Check if user can access the given route
|
||||
static bool canAccess(String path, AuthState authState) {
|
||||
// Allow access during unknown state (loading)
|
||||
if (authState == AuthState.unknown) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if route requires authentication
|
||||
final requiresAuth = RoutePaths.requiresAuth(path);
|
||||
final isAuthenticated = authState == AuthState.authenticated;
|
||||
|
||||
if (requiresAuth && !isAuthenticated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prevent authenticated users from accessing auth pages
|
||||
if (RoutePaths.isAuthPath(path) && isAuthenticated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Get redirect path based on current route and auth state
|
||||
static String? getRedirectPath(String path, AuthState authState) {
|
||||
if (authState == AuthState.unknown) {
|
||||
return null; // Don't redirect during loading
|
||||
}
|
||||
|
||||
final requiresAuth = RoutePaths.requiresAuth(path);
|
||||
final isAuthenticated = authState == AuthState.authenticated;
|
||||
|
||||
// Redirect unauthenticated users to login
|
||||
if (requiresAuth && !isAuthenticated) {
|
||||
return RoutePaths.login;
|
||||
}
|
||||
|
||||
// Redirect authenticated users away from auth pages
|
||||
if (RoutePaths.isAuthPath(path) && isAuthenticated) {
|
||||
return RoutePaths.home;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Onboarding state provider
|
||||
final onboardingStateProvider = StateNotifierProvider<OnboardingStateNotifier, bool>(
|
||||
(ref) => OnboardingStateNotifier(),
|
||||
);
|
||||
|
||||
/// Onboarding state notifier
|
||||
class OnboardingStateNotifier extends StateNotifier<bool> {
|
||||
OnboardingStateNotifier() : super(true) {
|
||||
_checkOnboardingStatus();
|
||||
}
|
||||
|
||||
Future<void> _checkOnboardingStatus() async {
|
||||
// TODO: Check if user has completed onboarding
|
||||
// For now, simulate that onboarding is not completed
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
state = false; // false means onboarding is completed
|
||||
}
|
||||
|
||||
void completeOnboarding() {
|
||||
state = false;
|
||||
// TODO: Save onboarding completion status to storage
|
||||
}
|
||||
}
|
||||
|
||||
/// Permission types
|
||||
enum Permission {
|
||||
camera,
|
||||
microphone,
|
||||
location,
|
||||
storage,
|
||||
notifications,
|
||||
}
|
||||
|
||||
/// Permission state provider
|
||||
final permissionStateProvider = StateNotifierProvider<PermissionStateNotifier, Map<Permission, bool>>(
|
||||
(ref) => PermissionStateNotifier(),
|
||||
);
|
||||
|
||||
/// Permission state notifier
|
||||
class PermissionStateNotifier extends StateNotifier<Map<Permission, bool>> {
|
||||
PermissionStateNotifier() : super({}) {
|
||||
_initializePermissions();
|
||||
}
|
||||
|
||||
void _initializePermissions() {
|
||||
// Initialize all permissions as not granted
|
||||
state = {
|
||||
for (final permission in Permission.values) permission: false,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> requestPermission(Permission permission) async {
|
||||
// TODO: Implement actual permission request logic
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Mock permission grant
|
||||
state = {
|
||||
...state,
|
||||
permission: true,
|
||||
};
|
||||
}
|
||||
|
||||
bool hasPermission(Permission permission) {
|
||||
return state[permission] ?? false;
|
||||
}
|
||||
|
||||
bool hasAllPermissions(List<Permission> permissions) {
|
||||
return permissions.every((permission) => hasPermission(permission));
|
||||
}
|
||||
}
|
||||
37
lib/core/routing/route_names.dart
Normal file
37
lib/core/routing/route_names.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
/// Route name constants for type-safe navigation
|
||||
class RouteNames {
|
||||
RouteNames._();
|
||||
|
||||
// Main routes
|
||||
static const String home = 'home';
|
||||
static const String settings = 'settings';
|
||||
static const String profile = 'profile';
|
||||
static const String about = 'about';
|
||||
|
||||
// Auth routes
|
||||
static const String login = 'login';
|
||||
static const String register = 'register';
|
||||
static const String forgotPassword = 'forgot_password';
|
||||
static const String resetPassword = 'reset_password';
|
||||
static const String verifyEmail = 'verify_email';
|
||||
|
||||
// Onboarding routes
|
||||
static const String onboarding = 'onboarding';
|
||||
static const String welcome = 'welcome';
|
||||
|
||||
// Todo routes (keeping existing feature)
|
||||
static const String todos = 'todos';
|
||||
static const String todoDetails = 'todo_details';
|
||||
static const String addTodo = 'add_todo';
|
||||
static const String editTodo = 'edit_todo';
|
||||
|
||||
// Error routes
|
||||
static const String error = 'error';
|
||||
static const String notFound = '404';
|
||||
|
||||
// Nested routes
|
||||
static const String settingsGeneral = 'settings_general';
|
||||
static const String settingsPrivacy = 'settings_privacy';
|
||||
static const String settingsNotifications = 'settings_notifications';
|
||||
static const String settingsTheme = 'settings_theme';
|
||||
}
|
||||
70
lib/core/routing/route_paths.dart
Normal file
70
lib/core/routing/route_paths.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
/// Route path constants for URL patterns
|
||||
class RoutePaths {
|
||||
RoutePaths._();
|
||||
|
||||
// Main routes
|
||||
static const String home = '/';
|
||||
static const String settings = '/settings';
|
||||
static const String profile = '/profile';
|
||||
static const String about = '/about';
|
||||
|
||||
// Auth routes
|
||||
static const String login = '/auth/login';
|
||||
static const String register = '/auth/register';
|
||||
static const String forgotPassword = '/auth/forgot-password';
|
||||
static const String resetPassword = '/auth/reset-password';
|
||||
static const String verifyEmail = '/auth/verify-email';
|
||||
|
||||
// Onboarding routes
|
||||
static const String onboarding = '/onboarding';
|
||||
static const String welcome = '/welcome';
|
||||
|
||||
// Todo routes (keeping existing feature)
|
||||
static const String todos = '/todos';
|
||||
static const String todoDetails = '/todos/:id';
|
||||
static const String addTodo = '/todos/add';
|
||||
static const String editTodo = '/todos/:id/edit';
|
||||
|
||||
// Error routes
|
||||
static const String error = '/error';
|
||||
static const String notFound = '/404';
|
||||
|
||||
// Nested settings routes
|
||||
static const String settingsGeneral = '/settings/general';
|
||||
static const String settingsPrivacy = '/settings/privacy';
|
||||
static const String settingsNotifications = '/settings/notifications';
|
||||
static const String settingsTheme = '/settings/theme';
|
||||
|
||||
/// Helper method to build paths with parameters
|
||||
static String todoDetailsPath(String id) => '/todos/$id';
|
||||
static String editTodoPath(String id) => '/todos/$id/edit';
|
||||
|
||||
/// Helper method to check if path requires authentication
|
||||
static bool requiresAuth(String path) {
|
||||
const publicPaths = [
|
||||
home,
|
||||
login,
|
||||
register,
|
||||
forgotPassword,
|
||||
resetPassword,
|
||||
verifyEmail,
|
||||
onboarding,
|
||||
welcome,
|
||||
error,
|
||||
notFound,
|
||||
];
|
||||
return !publicPaths.contains(path);
|
||||
}
|
||||
|
||||
/// Helper method to check if path is auth related
|
||||
static bool isAuthPath(String path) {
|
||||
const authPaths = [
|
||||
login,
|
||||
register,
|
||||
forgotPassword,
|
||||
resetPassword,
|
||||
verifyEmail,
|
||||
];
|
||||
return authPaths.contains(path);
|
||||
}
|
||||
}
|
||||
12
lib/core/routing/routing.dart
Normal file
12
lib/core/routing/routing.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
/// Routing module barrel file
|
||||
///
|
||||
/// This file exports all routing-related classes and functions
|
||||
/// for easier imports throughout the application.
|
||||
library routing;
|
||||
|
||||
export 'app_router.dart';
|
||||
export 'route_names.dart';
|
||||
export 'route_paths.dart';
|
||||
export 'route_guards.dart';
|
||||
export 'error_page.dart';
|
||||
export 'navigation_shell.dart';
|
||||
Reference in New Issue
Block a user