This commit is contained in:
2025-09-26 18:48:14 +07:00
parent 382a0e7909
commit 30ed6b39b5
85 changed files with 20722 additions and 112 deletions

View 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'),
),
],
),
),
);
}
}

View 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'),
),
],
),
],
),
),
),
);
}
}

View 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;
}
}
}

View 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));
}
}

View 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';
}

View 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);
}
}

View 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';