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,35 @@
/// Application-wide constants
class AppConstants {
// App Information
static const String appName = 'Base Flutter';
static const String appVersion = '1.0.0';
// API Configuration
static const Duration connectionTimeout = Duration(seconds: 30);
static const Duration receiveTimeout = Duration(seconds: 30);
static const Duration sendTimeout = Duration(seconds: 30);
// Pagination
static const int defaultPageSize = 20;
static const int maxPageSize = 100;
// Cache Configuration
static const Duration cacheExpiration = Duration(hours: 1);
static const int maxCacheSize = 50 * 1024 * 1024; // 50MB
// Animation Durations
static const Duration shortAnimation = Duration(milliseconds: 200);
static const Duration mediumAnimation = Duration(milliseconds: 300);
static const Duration longAnimation = Duration(milliseconds: 500);
// UI Constants
static const double defaultPadding = 16.0;
static const double smallPadding = 8.0;
static const double largePadding = 24.0;
static const double defaultRadius = 8.0;
// Error Messages
static const String networkErrorMessage = 'Network connection error';
static const String unknownErrorMessage = 'An unknown error occurred';
static const String timeoutErrorMessage = 'Request timeout';
}

View File

@@ -0,0 +1,3 @@
// Barrel export file for constants
export 'app_constants.dart';
export 'storage_constants.dart';

View File

@@ -0,0 +1,24 @@
/// Storage-related constants for Hive boxes and secure storage keys
class StorageConstants {
// Hive Box Names
static const String appSettingsBox = 'app_settings';
static const String userDataBox = 'user_data';
static const String cacheBox = 'cache_box';
// Secure Storage Keys
static const String authTokenKey = 'auth_token';
static const String refreshTokenKey = 'refresh_token';
static const String userCredentialsKey = 'user_credentials';
static const String biometricKey = 'biometric_enabled';
// Cache Keys
static const String userPreferencesKey = 'user_preferences';
static const String appThemeKey = 'app_theme';
static const String languageKey = 'selected_language';
static const String firstRunKey = 'is_first_run';
// Settings Keys
static const String isDarkModeKey = 'is_dark_mode';
static const String notificationsEnabledKey = 'notifications_enabled';
static const String autoSyncKey = 'auto_sync_enabled';
}

369
lib/core/database/README.md Normal file
View File

@@ -0,0 +1,369 @@
# Hive Database Configuration
This directory contains the complete Hive database setup for local storage and caching in the Flutter app.
## Architecture Overview
The Hive database system is organized into the following components:
```
lib/core/database/
├── hive_service.dart # Hive initialization and box management
├── models/
│ ├── app_settings.dart # App settings model with Hive adapter
│ ├── app_settings.g.dart # Generated Hive adapter
│ ├── cache_item.dart # Generic cache wrapper model
│ ├── cache_item.g.dart # Generated Hive adapter
│ ├── user_preferences.dart # User preferences model
│ └── user_preferences.g.dart # Generated Hive adapter
├── repositories/
│ ├── settings_repository.dart # Settings repository implementation
│ ├── cache_repository.dart # Cache repository implementation
│ └── user_preferences_repository.dart # User preferences repository
├── providers/
│ └── database_providers.dart # Riverpod providers for database access
└── examples/
└── database_usage_example.dart # Usage examples
```
## Features
### 1. HiveService
- **Initialization**: Sets up Hive for Flutter and registers type adapters
- **Box Management**: Manages three main boxes (appSettingsBox, cacheBox, userDataBox)
- **Migration Support**: Handles database schema migrations
- **Error Handling**: Comprehensive error handling and logging
- **Maintenance**: Database compaction and cleanup utilities
### 2. Models with Type Adapters
#### AppSettings (TypeId: 0)
- Application-wide configuration
- Theme mode, locale, notifications
- Cache settings and auto-update preferences
- Custom settings support
- Expiration logic for settings refresh
#### CacheItem (TypeId: 1)
- Generic cache wrapper for any data type
- Expiration logic with TTL support
- Metadata support for cache categorization
- Version control for cache migrations
- Statistics and performance monitoring
#### UserPreferences (TypeId: 2)
- User-specific settings and data
- Favorites management
- Last accessed tracking
- Preference categories with type safety
- User activity statistics
### 3. Repository Pattern
- **SettingsRepository**: Application settings management
- **CacheRepository**: Generic caching with TTL and expiration
- **UserPreferencesRepository**: User-specific data management
### 4. Riverpod Integration
- State management with providers
- Reactive updates when data changes
- Type-safe access to cached data
- Automatic cache invalidation
## Usage Examples
### Basic Initialization
```dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive database service
await HiveService.initialize();
runApp(const ProviderScope(child: MyApp()));
}
```
### Settings Management
```dart
class SettingsScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(appSettingsProvider);
final notifier = ref.read(appSettingsProvider.notifier);
return SwitchListTile(
title: const Text('Dark Mode'),
value: settings.themeMode == 'dark',
onChanged: (value) => notifier.updateThemeMode(value ? 'dark' : 'light'),
);
}
}
```
### Caching Data
```dart
class DataService {
final CacheRepository _cache;
Future<UserData> getUserData(String userId) async {
// Try cache first
final cachedData = _cache.get<UserData>('user_$userId');
if (cachedData != null) {
return cachedData;
}
// Fetch from API
final userData = await api.fetchUserData(userId);
// Cache with 1 hour expiration
await _cache.put<UserData>(
key: 'user_$userId',
data: userData,
expirationDuration: const Duration(hours: 1),
metadata: {'type': 'user_data', 'userId': userId},
);
return userData;
}
}
```
### User Preferences
```dart
class ProfileScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final preferences = ref.watch(userPreferencesProvider(null));
final notifier = ref.read(userPreferencesProvider(null).notifier);
return Column(
children: [
SwitchListTile(
title: const Text('Compact Mode'),
value: preferences?.getPreference<bool>('compactMode', false) ?? false,
onChanged: (value) => notifier.setPreference('compactMode', value),
),
// Favorites management
IconButton(
icon: Icon(preferences?.isFavorite('item123') == true
? Icons.favorite : Icons.favorite_border),
onPressed: () {
if (preferences?.isFavorite('item123') == true) {
notifier.removeFavorite('item123');
} else {
notifier.addFavorite('item123');
}
},
),
],
);
}
}
```
## Database Boxes
### appSettingsBox
- **Purpose**: Application-wide settings
- **Key**: String-based keys (e.g., 'app_settings')
- **Data**: AppSettings objects
- **Usage**: Theme, language, cache strategy, feature flags
### cacheBox
- **Purpose**: Generic caching with TTL
- **Key**: String-based cache keys
- **Data**: CacheItem objects wrapping any data type
- **Usage**: API responses, user data, computed values
### userDataBox
- **Purpose**: User-specific preferences and data
- **Key**: User ID or 'current_user_preferences' for default
- **Data**: UserPreferences objects
- **Usage**: User settings, favorites, activity tracking
## Cache Strategies
### Write-Through Cache
```dart
Future<void> updateUserData(UserData data) async {
// Update API
await api.updateUser(data);
// Update cache
await cacheRepository.put<UserData>(
key: 'user_${data.id}',
data: data,
expirationDuration: const Duration(hours: 1),
);
}
```
### Cache-Aside Pattern
```dart
Future<UserData> getUserData(String userId) async {
// Check cache first
var userData = cacheRepository.get<UserData>('user_$userId');
if (userData == null) {
// Cache miss - fetch from API
userData = await api.fetchUser(userId);
// Store in cache
await cacheRepository.put<UserData>(
key: 'user_$userId',
data: userData,
expirationDuration: const Duration(minutes: 30),
);
}
return userData;
}
```
## Performance Optimizations
### 1. Lazy Loading
- Boxes are opened only when needed
- Data is loaded on-demand
### 2. Batch Operations
```dart
Future<void> cacheMultipleItems(Map<String, dynamic> items) async {
final futures = items.entries.map((entry) =>
cacheRepository.put<dynamic>(
key: entry.key,
data: entry.value,
expirationDuration: const Duration(hours: 1),
)
);
await Future.wait(futures);
}
```
### 3. Cache Maintenance
```dart
// Automatic cleanup of expired items
final result = await cacheRepository.performMaintenance();
print('Cleaned ${result['expiredItemsRemoved']} expired items');
```
## Error Handling
### Repository Level
- All repository methods have comprehensive try-catch blocks
- Errors are logged with stack traces
- Graceful degradation (return defaults on errors)
### Provider Level
- State management handles loading/error states
- Automatic retry mechanisms
- User-friendly error messages
### Service Level
- Database initialization errors are handled gracefully
- Box corruption recovery
- Migration failure handling
## Migration Strategy
### Version Control
```dart
class AppSettings {
final int version; // Used for migrations
// Migration logic in repository
AppSettings _migrateSettings(AppSettings oldSettings) {
if (oldSettings.version < 2) {
// Perform migration to version 2
return oldSettings.copyWith(
version: 2,
// Add new fields with defaults
newField: defaultValue,
);
}
return oldSettings;
}
}
```
### Data Backup
```dart
// Export data before migration
final backup = settingsRepository.exportSettings();
final userBackup = userPreferencesRepository.exportUserPreferences();
// Perform migration
await HiveService.migrate();
// Restore if migration fails
if (migrationFailed) {
await settingsRepository.importSettings(backup);
await userPreferencesRepository.importUserPreferences(userBackup);
}
```
## Monitoring and Analytics
### Database Statistics
```dart
final stats = ref.watch(databaseStatsProvider);
print('Total items: ${stats['totalItems']}');
print('Cache hit rate: ${stats['cache']['hitRate']}%');
```
### Performance Metrics
```dart
final cacheStats = await cacheRepository.getStats();
print('Cache performance:');
print('- Hit rate: ${(cacheStats.hitRate * 100).toStringAsFixed(1)}%');
print('- Valid items: ${cacheStats.validItems}');
print('- Expired items: ${cacheStats.expiredItems}');
```
## Best Practices
### 1. Key Naming Convention
- Use descriptive, hierarchical keys: `user_123_profile`
- Include type information: `api_response_movies_popular`
- Use consistent separators: underscores or colons
### 2. TTL Management
- Short TTL for frequently changing data (5-30 minutes)
- Medium TTL for stable data (1-24 hours)
- Long TTL for static data (1-7 days)
- Permanent cache only for user preferences
### 3. Data Validation
- Validate data before caching
- Check data integrity on retrieval
- Handle corrupted data gracefully
### 4. Memory Management
- Regular cleanup of expired items
- Monitor cache size and performance
- Use appropriate data structures
### 5. Security
- Don't cache sensitive data without encryption
- Clear caches on logout
- Validate user access to cached data
## Testing Strategy
### Unit Tests
- Test repository methods with mock boxes
- Validate data serialization/deserialization
- Test error handling scenarios
### Integration Tests
- Test full database workflows
- Validate Riverpod provider interactions
- Test migration scenarios
### Performance Tests
- Measure cache hit rates
- Test with large datasets
- Monitor memory usage
This Hive database setup provides a robust, scalable foundation for local data storage and caching in your Flutter application.

View File

@@ -0,0 +1,412 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/database_providers.dart';
/// Example widget showing how to use the Hive database system
class DatabaseUsageExample extends ConsumerWidget {
const DatabaseUsageExample({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('Database Usage Example'),
),
body: const SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_AppSettingsSection(),
SizedBox(height: 24),
_CacheSection(),
SizedBox(height: 24),
_UserPreferencesSection(),
SizedBox(height: 24),
_DatabaseStatsSection(),
],
),
),
);
}
}
/// App Settings Section
class _AppSettingsSection extends ConsumerWidget {
const _AppSettingsSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(appSettingsProvider);
final notifier = ref.read(appSettingsProvider.notifier);
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('App Settings', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
// Theme Mode
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Theme Mode'),
DropdownButton<String>(
value: settings.themeMode,
onChanged: (value) => notifier.updateThemeMode(value!),
items: const [
DropdownMenuItem(value: 'system', child: Text('System')),
DropdownMenuItem(value: 'light', child: Text('Light')),
DropdownMenuItem(value: 'dark', child: Text('Dark')),
],
),
],
),
// Notifications
SwitchListTile(
title: const Text('Notifications'),
value: settings.notificationsEnabled,
onChanged: (value) => notifier.updateNotificationsEnabled(value),
),
// Analytics
SwitchListTile(
title: const Text('Analytics'),
value: settings.analyticsEnabled,
onChanged: (value) => notifier.updateAnalyticsEnabled(value),
),
// Cache Strategy
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Cache Strategy'),
DropdownButton<String>(
value: settings.cacheStrategy,
onChanged: (value) => notifier.updateCacheStrategy(value!),
items: const [
DropdownMenuItem(value: 'minimal', child: Text('Minimal')),
DropdownMenuItem(value: 'normal', child: Text('Normal')),
DropdownMenuItem(value: 'aggressive', child: Text('Aggressive')),
],
),
],
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => notifier.resetToDefault(),
child: const Text('Reset to Default'),
),
],
),
),
);
}
}
/// Cache Section
class _CacheSection extends ConsumerWidget {
const _CacheSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheRepository = ref.watch(cacheRepositoryProvider);
final cacheStatsAsyncValue = ref.watch(cacheStatsProvider);
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Cache Management', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
// Cache Statistics
cacheStatsAsyncValue.when(
data: (stats) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Total Items: ${stats.totalItems}'),
Text('Valid Items: ${stats.validItems}'),
Text('Expired Items: ${stats.expiredItems}'),
Text('Hit Rate: ${(stats.hitRate * 100).toStringAsFixed(1)}%'),
],
),
loading: () => const CircularProgressIndicator(),
error: (error, _) => Text('Error: $error'),
),
const SizedBox(height: 12),
// Cache Actions
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () async {
// Example: Cache some data
await cacheRepository.put<String>(
key: 'example_key',
data: 'Hello, Cache!',
expirationDuration: const Duration(minutes: 30),
metadata: {'type': 'example', 'created_by': 'user'},
);
ref.invalidate(cacheStatsProvider);
},
child: const Text('Add Cache Item'),
),
ElevatedButton(
onPressed: () async {
final data = cacheRepository.get<String>('example_key');
if (data != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Cached data: $data')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No cached data found')),
);
}
},
child: const Text('Get Cache Item'),
),
ElevatedButton(
onPressed: () async {
final expiredCount = await cacheRepository.clearExpired();
ref.invalidate(cacheStatsProvider);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Cleared $expiredCount expired items')),
);
}
},
child: const Text('Clear Expired'),
),
],
),
],
),
),
);
}
}
/// User Preferences Section
class _UserPreferencesSection extends ConsumerWidget {
const _UserPreferencesSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final userPreferences = ref.watch(userPreferencesProvider(null));
final notifier = ref.read(userPreferencesProvider(null).notifier);
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('User Preferences', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
if (userPreferences == null)
Column(
children: [
const Text('No user preferences found'),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () async {
await notifier.createUserPreferences(
userId: 'demo_user',
displayName: 'Demo User',
email: 'demo@example.com',
preferences: {
'compactMode': false,
'sortBy': 'name',
'gridColumns': 2,
},
);
},
child: const Text('Create Demo User'),
),
],
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('User ID: ${userPreferences.userId}'),
Text('Display Name: ${userPreferences.displayName}'),
Text('Email: ${userPreferences.email ?? 'Not set'}'),
Text('Favorites: ${userPreferences.favoriteItems.length}'),
Text('Preferences: ${userPreferences.preferences.length}'),
const SizedBox(height: 12),
// Preference Controls
SwitchListTile(
title: const Text('Compact Mode'),
value: userPreferences.getPreference<bool>('compactMode', false),
onChanged: (value) => notifier.setPreference('compactMode', value),
),
const SizedBox(height: 12),
// Actions
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () async {
await notifier.addFavorite('item_${DateTime.now().millisecondsSinceEpoch}');
},
child: const Text('Add Random Favorite'),
),
ElevatedButton(
onPressed: () async {
await notifier.updateLastAccessed('demo_item');
},
child: const Text('Update Last Accessed'),
),
ElevatedButton(
onPressed: () async {
await notifier.clearPreferences();
},
child: const Text('Clear User Data'),
),
],
),
],
),
],
),
),
);
}
}
/// Database Statistics Section
class _DatabaseStatsSection extends ConsumerWidget {
const _DatabaseStatsSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final databaseStats = ref.watch(databaseStatsProvider);
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Database Statistics', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text('Total Items: ${databaseStats['totalItems']}'),
if (databaseStats['settings'] != null) ...[
const SizedBox(height: 8),
const Text('Settings:', style: TextStyle(fontWeight: FontWeight.w500)),
...((databaseStats['settings'] as Map<String, dynamic>).entries.map(
(entry) => Text(' ${entry.key}: ${entry.value}'),
)),
],
if (databaseStats['cache'] != null) ...[
const SizedBox(height: 8),
const Text('Cache:', style: TextStyle(fontWeight: FontWeight.w500)),
...((databaseStats['cache'] as Map<String, dynamic>).entries.map(
(entry) => Text(' ${entry.key}: ${entry.value}'),
)),
],
],
),
),
);
}
}
/// Helper functions for demonstrating cache operations
class CacheExamples {
static Future<void> demonstrateCaching(WidgetRef ref) async {
final cacheRepository = ref.read(cacheRepositoryProvider);
// Store different types of data
await cacheRepository.put<String>(
key: 'user_token',
data: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
expirationDuration: const Duration(hours: 1),
metadata: {'type': 'auth_token'},
);
await cacheRepository.put<Map<String, dynamic>>(
key: 'user_profile',
data: {
'id': 123,
'name': 'John Doe',
'email': 'john@example.com',
},
expirationDuration: const Duration(minutes: 30),
metadata: {'type': 'user_data'},
);
await cacheRepository.put<List<String>>(
key: 'recent_searches',
data: ['flutter', 'dart', 'hive'],
expirationDuration: const Duration(days: 7),
metadata: {'type': 'user_activity'},
);
// Retrieve data
final token = cacheRepository.get<String>('user_token');
final profile = cacheRepository.get<Map<String, dynamic>>('user_profile');
final searches = cacheRepository.get<List<String>>('recent_searches');
print('Token: $token');
print('Profile: $profile');
print('Searches: $searches');
}
static Future<void> demonstrateUserPreferences(WidgetRef ref) async {
final repository = ref.read(userPreferencesRepositoryProvider);
// Create user preferences
final preferences = await repository.createUserPreferences(
userId: 'user_123',
displayName: 'Alice Smith',
email: 'alice@example.com',
preferences: {
'theme': 'dark',
'notifications': true,
'language': 'en',
'autoBackup': false,
},
);
print('Created preferences: $preferences');
// Update preferences
await repository.setPreference('theme', 'light', 'user_123');
await repository.addFavorite('movie_456', 'user_123');
await repository.updateLastAccessed('series_789', 'user_123');
// Retrieve data
final theme = repository.getPreference<String>('theme', 'system', 'user_123');
final favorites = repository.getFavorites('user_123');
final recentItems = repository.getRecentlyAccessed(userId: 'user_123');
print('Theme: $theme');
print('Favorites: $favorites');
print('Recent items: $recentItems');
}
}

View File

@@ -0,0 +1,180 @@
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'models/app_settings.dart';
import 'models/cache_item.dart';
import 'models/user_preferences.dart';
/// Hive database service for initialization and box management
class HiveService {
static const String _appSettingsBoxName = 'appSettingsBox';
static const String _cacheBoxName = 'cacheBox';
static const String _userDataBoxName = 'userDataBox';
// Private boxes - access through getters
static Box<AppSettings>? _appSettingsBox;
static Box<CacheItem>? _cacheBox;
static Box<UserPreferences>? _userDataBox;
/// Initialize Hive database
static Future<void> initialize() async {
try {
// Initialize Hive for Flutter
await Hive.initFlutter();
// Register type adapters
await _registerAdapters();
// Open boxes
await _openBoxes();
debugPrint('✅ Hive initialized successfully');
} catch (e, stackTrace) {
debugPrint('❌ Failed to initialize Hive: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
/// Register all Hive type adapters
static Future<void> _registerAdapters() async {
// Register adapters only if not already registered
if (!Hive.isAdapterRegistered(0)) {
Hive.registerAdapter(AppSettingsAdapter());
}
if (!Hive.isAdapterRegistered(1)) {
Hive.registerAdapter(CacheItemAdapter());
}
if (!Hive.isAdapterRegistered(2)) {
Hive.registerAdapter(UserPreferencesAdapter());
}
}
/// Open all required Hive boxes
static Future<void> _openBoxes() async {
try {
_appSettingsBox = await Hive.openBox<AppSettings>(_appSettingsBoxName);
_cacheBox = await Hive.openBox<CacheItem>(_cacheBoxName);
_userDataBox = await Hive.openBox<UserPreferences>(_userDataBoxName);
} catch (e) {
debugPrint('Error opening Hive boxes: $e');
rethrow;
}
}
/// Get app settings box
static Box<AppSettings> get appSettingsBox {
if (_appSettingsBox == null || !_appSettingsBox!.isOpen) {
throw StateError('AppSettings box is not initialized. Call HiveService.initialize() first.');
}
return _appSettingsBox!;
}
/// Get cache box
static Box<CacheItem> get cacheBox {
if (_cacheBox == null || !_cacheBox!.isOpen) {
throw StateError('Cache box is not initialized. Call HiveService.initialize() first.');
}
return _cacheBox!;
}
/// Get user data box
static Box<UserPreferences> get userDataBox {
if (_userDataBox == null || !_userDataBox!.isOpen) {
throw StateError('UserData box is not initialized. Call HiveService.initialize() first.');
}
return _userDataBox!;
}
/// Check if Hive is initialized
static bool get isInitialized {
return _appSettingsBox != null &&
_cacheBox != null &&
_userDataBox != null &&
_appSettingsBox!.isOpen &&
_cacheBox!.isOpen &&
_userDataBox!.isOpen;
}
/// Close all boxes (call this when app is terminated)
static Future<void> closeAll() async {
try {
await _appSettingsBox?.close();
await _cacheBox?.close();
await _userDataBox?.close();
debugPrint('✅ All Hive boxes closed successfully');
} catch (e) {
debugPrint('❌ Error closing Hive boxes: $e');
}
}
/// Clear all data (use with caution - for testing or reset functionality)
static Future<void> clearAll() async {
try {
await _appSettingsBox?.clear();
await _cacheBox?.clear();
await _userDataBox?.clear();
debugPrint('✅ All Hive boxes cleared successfully');
} catch (e) {
debugPrint('❌ Error clearing Hive boxes: $e');
rethrow;
}
}
/// Compact all boxes to optimize storage
static Future<void> compactAll() async {
try {
await _appSettingsBox?.compact();
await _cacheBox?.compact();
await _userDataBox?.compact();
debugPrint('✅ All Hive boxes compacted successfully');
} catch (e) {
debugPrint('❌ Error compacting Hive boxes: $e');
}
}
/// Get database statistics
static Map<String, dynamic> getStats() {
return {
'isInitialized': isInitialized,
'appSettingsCount': _appSettingsBox?.length ?? 0,
'cacheItemsCount': _cacheBox?.length ?? 0,
'userDataCount': _userDataBox?.length ?? 0,
};
}
/// Perform database migration if needed
static Future<void> migrate() async {
try {
// Check current version and migrate if needed
final currentVersion = _appSettingsBox?.get('db_version')?.version ?? 1;
const latestVersion = 1;
if (currentVersion < latestVersion) {
await _performMigration(currentVersion, latestVersion);
}
} catch (e) {
debugPrint('❌ Error during database migration: $e');
}
}
/// Perform database migration from one version to another
static Future<void> _performMigration(int fromVersion, int toVersion) async {
debugPrint('🔄 Migrating database from version $fromVersion to $toVersion');
// Add migration logic here as needed
// Example:
// if (fromVersion < 2) {
// await _migrateToVersion2();
// }
// Update database version
await _appSettingsBox?.put('db_version', AppSettings(
version: toVersion,
themeMode: 'system',
locale: 'en',
lastUpdated: DateTime.now(),
));
debugPrint('✅ Database migration completed');
}
}

View File

@@ -0,0 +1,212 @@
import 'package:hive/hive.dart';
part 'app_settings.g.dart';
/// App settings model for storing application-wide configuration
@HiveType(typeId: 0)
class AppSettings extends HiveObject {
@HiveField(0)
final int version;
@HiveField(1)
final String themeMode; // 'light', 'dark', 'system'
@HiveField(2)
final String locale; // Language code (e.g., 'en', 'es')
@HiveField(3)
final bool notificationsEnabled;
@HiveField(4)
final bool analyticsEnabled;
@HiveField(5)
final String cacheStrategy; // 'aggressive', 'normal', 'minimal'
@HiveField(6)
final int cacheExpirationHours;
@HiveField(7)
final bool autoUpdateEnabled;
@HiveField(8)
final DateTime lastUpdated;
@HiveField(9)
final Map<String, dynamic>? customSettings;
AppSettings({
this.version = 1,
this.themeMode = 'system',
this.locale = 'en',
this.notificationsEnabled = true,
this.analyticsEnabled = false,
this.cacheStrategy = 'normal',
this.cacheExpirationHours = 24,
this.autoUpdateEnabled = true,
required this.lastUpdated,
this.customSettings,
});
/// Create a copy with modified fields
AppSettings copyWith({
int? version,
String? themeMode,
String? locale,
bool? notificationsEnabled,
bool? analyticsEnabled,
String? cacheStrategy,
int? cacheExpirationHours,
bool? autoUpdateEnabled,
DateTime? lastUpdated,
Map<String, dynamic>? customSettings,
}) {
return AppSettings(
version: version ?? this.version,
themeMode: themeMode ?? this.themeMode,
locale: locale ?? this.locale,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
analyticsEnabled: analyticsEnabled ?? this.analyticsEnabled,
cacheStrategy: cacheStrategy ?? this.cacheStrategy,
cacheExpirationHours: cacheExpirationHours ?? this.cacheExpirationHours,
autoUpdateEnabled: autoUpdateEnabled ?? this.autoUpdateEnabled,
lastUpdated: lastUpdated ?? this.lastUpdated,
customSettings: customSettings ?? this.customSettings,
);
}
/// Create default app settings
factory AppSettings.defaultSettings() {
return AppSettings(
version: 1,
themeMode: 'system',
locale: 'en',
notificationsEnabled: true,
analyticsEnabled: false,
cacheStrategy: 'normal',
cacheExpirationHours: 24,
autoUpdateEnabled: true,
lastUpdated: DateTime.now(),
);
}
/// Convert to Map for JSON serialization
Map<String, dynamic> toMap() {
return {
'version': version,
'themeMode': themeMode,
'locale': locale,
'notificationsEnabled': notificationsEnabled,
'analyticsEnabled': analyticsEnabled,
'cacheStrategy': cacheStrategy,
'cacheExpirationHours': cacheExpirationHours,
'autoUpdateEnabled': autoUpdateEnabled,
'lastUpdated': lastUpdated.toIso8601String(),
'customSettings': customSettings,
};
}
/// Create from Map for JSON deserialization
factory AppSettings.fromMap(Map<String, dynamic> map) {
return AppSettings(
version: map['version'] ?? 1,
themeMode: map['themeMode'] ?? 'system',
locale: map['locale'] ?? 'en',
notificationsEnabled: map['notificationsEnabled'] ?? true,
analyticsEnabled: map['analyticsEnabled'] ?? false,
cacheStrategy: map['cacheStrategy'] ?? 'normal',
cacheExpirationHours: map['cacheExpirationHours'] ?? 24,
autoUpdateEnabled: map['autoUpdateEnabled'] ?? true,
lastUpdated: DateTime.parse(map['lastUpdated'] ?? DateTime.now().toIso8601String()),
customSettings: map['customSettings']?.cast<String, dynamic>(),
);
}
/// Check if settings are expired (for auto-refresh logic)
bool isExpired({int maxAgeHours = 168}) { // Default 1 week
final now = DateTime.now();
final difference = now.difference(lastUpdated);
return difference.inHours > maxAgeHours;
}
/// Get custom setting value
T? getCustomSetting<T>(String key) {
return customSettings?[key] as T?;
}
/// Set custom setting value
AppSettings setCustomSetting(String key, dynamic value) {
final updatedCustomSettings = Map<String, dynamic>.from(customSettings ?? {});
updatedCustomSettings[key] = value;
return copyWith(
customSettings: updatedCustomSettings,
lastUpdated: DateTime.now(),
);
}
/// Remove custom setting
AppSettings removeCustomSetting(String key) {
if (customSettings == null) return this;
final updatedCustomSettings = Map<String, dynamic>.from(customSettings!);
updatedCustomSettings.remove(key);
return copyWith(
customSettings: updatedCustomSettings.isNotEmpty ? updatedCustomSettings : null,
lastUpdated: DateTime.now(),
);
}
@override
String toString() {
return 'AppSettings{version: $version, themeMode: $themeMode, locale: $locale, '
'notificationsEnabled: $notificationsEnabled, analyticsEnabled: $analyticsEnabled, '
'cacheStrategy: $cacheStrategy, cacheExpirationHours: $cacheExpirationHours, '
'autoUpdateEnabled: $autoUpdateEnabled, lastUpdated: $lastUpdated, '
'customSettings: $customSettings}';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AppSettings &&
other.version == version &&
other.themeMode == themeMode &&
other.locale == locale &&
other.notificationsEnabled == notificationsEnabled &&
other.analyticsEnabled == analyticsEnabled &&
other.cacheStrategy == cacheStrategy &&
other.cacheExpirationHours == cacheExpirationHours &&
other.autoUpdateEnabled == autoUpdateEnabled &&
other.lastUpdated == lastUpdated &&
_mapEquals(other.customSettings, customSettings);
}
bool _mapEquals(Map<String, dynamic>? a, Map<String, dynamic>? b) {
if (a == null) return b == null;
if (b == null) return false;
if (a.length != b.length) return false;
for (final key in a.keys) {
if (!b.containsKey(key) || a[key] != b[key]) return false;
}
return true;
}
@override
int get hashCode {
return Object.hash(
version,
themeMode,
locale,
notificationsEnabled,
analyticsEnabled,
cacheStrategy,
cacheExpirationHours,
autoUpdateEnabled,
lastUpdated,
customSettings?.hashCode ?? 0,
);
}
}

View File

@@ -0,0 +1,68 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_settings.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class AppSettingsAdapter extends TypeAdapter<AppSettings> {
@override
final int typeId = 0;
@override
AppSettings read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return AppSettings(
version: fields[0] as int,
themeMode: fields[1] as String,
locale: fields[2] as String,
notificationsEnabled: fields[3] as bool,
analyticsEnabled: fields[4] as bool,
cacheStrategy: fields[5] as String,
cacheExpirationHours: fields[6] as int,
autoUpdateEnabled: fields[7] as bool,
lastUpdated: fields[8] as DateTime,
customSettings: (fields[9] as Map?)?.cast<String, dynamic>(),
);
}
@override
void write(BinaryWriter writer, AppSettings obj) {
writer
..writeByte(10)
..writeByte(0)
..write(obj.version)
..writeByte(1)
..write(obj.themeMode)
..writeByte(2)
..write(obj.locale)
..writeByte(3)
..write(obj.notificationsEnabled)
..writeByte(4)
..write(obj.analyticsEnabled)
..writeByte(5)
..write(obj.cacheStrategy)
..writeByte(6)
..write(obj.cacheExpirationHours)
..writeByte(7)
..write(obj.autoUpdateEnabled)
..writeByte(8)
..write(obj.lastUpdated)
..writeByte(9)
..write(obj.customSettings);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppSettingsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,274 @@
import 'package:hive/hive.dart';
part 'cache_item.g.dart';
/// Generic cache wrapper for storing any data with expiration logic
@HiveType(typeId: 1)
class CacheItem extends HiveObject {
@HiveField(0)
@override
final String key;
@HiveField(1)
final dynamic data;
@HiveField(2)
final DateTime createdAt;
@HiveField(3)
final DateTime expiresAt;
@HiveField(4)
final String dataType; // Type identifier for runtime type safety
@HiveField(5)
final Map<String, dynamic>? metadata; // Additional cache metadata
@HiveField(6)
final int version; // Cache version for migration support
CacheItem({
required this.key,
required this.data,
required this.createdAt,
required this.expiresAt,
required this.dataType,
this.metadata,
this.version = 1,
});
/// Create a new cache item with expiration time
factory CacheItem.create({
required String key,
required dynamic data,
required Duration expirationDuration,
Map<String, dynamic>? metadata,
int version = 1,
}) {
final now = DateTime.now();
return CacheItem(
key: key,
data: data,
createdAt: now,
expiresAt: now.add(expirationDuration),
dataType: data.runtimeType.toString(),
metadata: metadata,
version: version,
);
}
/// Create a cache item that never expires
factory CacheItem.permanent({
required String key,
required dynamic data,
Map<String, dynamic>? metadata,
int version = 1,
}) {
final now = DateTime.now();
return CacheItem(
key: key,
data: data,
createdAt: now,
expiresAt: DateTime(9999), // Far future date
dataType: data.runtimeType.toString(),
metadata: metadata,
version: version,
);
}
/// Check if the cache item is expired
bool get isExpired {
return DateTime.now().isAfter(expiresAt);
}
/// Check if the cache item is still valid
bool get isValid {
return !isExpired;
}
/// Get the age of the cache item
Duration get age {
return DateTime.now().difference(createdAt);
}
/// Get time remaining until expiration
Duration get timeUntilExpiry {
final now = DateTime.now();
if (now.isAfter(expiresAt)) {
return Duration.zero;
}
return expiresAt.difference(now);
}
/// Check if cache item will expire within the given duration
bool willExpireWithin(Duration duration) {
final checkTime = DateTime.now().add(duration);
return expiresAt.isBefore(checkTime);
}
/// Create a refreshed copy with new expiration time
CacheItem refresh(Duration newExpirationDuration) {
final now = DateTime.now();
return CacheItem(
key: key,
data: data,
createdAt: createdAt, // Keep original creation time
expiresAt: now.add(newExpirationDuration),
dataType: dataType,
metadata: metadata,
version: version,
);
}
/// Create a copy with updated data
CacheItem updateData(dynamic newData, {Duration? newExpirationDuration}) {
final now = DateTime.now();
return CacheItem(
key: key,
data: newData,
createdAt: now, // Update creation time for new data
expiresAt: newExpirationDuration != null
? now.add(newExpirationDuration)
: expiresAt,
dataType: dataType,
metadata: metadata,
version: version,
);
}
/// Create a copy with updated metadata
CacheItem updateMetadata(Map<String, dynamic> newMetadata) {
return CacheItem(
key: key,
data: data,
createdAt: createdAt,
expiresAt: expiresAt,
dataType: dataType,
metadata: {...(metadata ?? {}), ...newMetadata},
version: version,
);
}
/// Get metadata value
V? getMetadata<V>(String key) {
return metadata?[key] as V?;
}
/// Convert to Map for JSON serialization
Map<String, dynamic> toMap() {
return {
'key': key,
'data': data,
'createdAt': createdAt.toIso8601String(),
'expiresAt': expiresAt.toIso8601String(),
'dataType': dataType,
'metadata': metadata,
'version': version,
};
}
/// Create from Map (useful for migration or external data)
static CacheItem fromMap(Map<String, dynamic> map) {
return CacheItem(
key: map['key'] as String,
data: map['data'],
createdAt: DateTime.parse(map['createdAt'] as String),
expiresAt: DateTime.parse(map['expiresAt'] as String),
dataType: map['dataType'] as String,
metadata: map['metadata']?.cast<String, dynamic>(),
version: map['version'] as int? ?? 1,
);
}
@override
String toString() {
return 'CacheItem{key: $key, dataType: $dataType, createdAt: $createdAt, '
'expiresAt: $expiresAt, isExpired: $isExpired, version: $version}';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CacheItem &&
other.key == key &&
other.data == data &&
other.createdAt == createdAt &&
other.expiresAt == expiresAt &&
other.dataType == dataType &&
_mapEquals(other.metadata, metadata) &&
other.version == version;
}
bool _mapEquals(Map<String, dynamic>? a, Map<String, dynamic>? b) {
if (a == null) return b == null;
if (b == null) return false;
if (a.length != b.length) return false;
for (final key in a.keys) {
if (!b.containsKey(key) || a[key] != b[key]) return false;
}
return true;
}
@override
int get hashCode {
return Object.hash(
key,
data,
createdAt,
expiresAt,
dataType,
metadata?.hashCode ?? 0,
version,
);
}
}
/// Cache statistics for monitoring cache performance
class CacheStats {
final int totalItems;
final int validItems;
final int expiredItems;
final DateTime oldestItem;
final DateTime newestItem;
final Map<String, int> typeCount;
const CacheStats({
required this.totalItems,
required this.validItems,
required this.expiredItems,
required this.oldestItem,
required this.newestItem,
required this.typeCount,
});
double get hitRate {
if (totalItems == 0) return 0.0;
return validItems / totalItems;
}
double get expirationRate {
if (totalItems == 0) return 0.0;
return expiredItems / totalItems;
}
Map<String, dynamic> toMap() {
return {
'totalItems': totalItems,
'validItems': validItems,
'expiredItems': expiredItems,
'oldestItem': oldestItem.toIso8601String(),
'newestItem': newestItem.toIso8601String(),
'typeCount': typeCount,
'hitRate': hitRate,
'expirationRate': expirationRate,
};
}
@override
String toString() {
return 'CacheStats{total: $totalItems, valid: $validItems, expired: $expiredItems, '
'hitRate: ${(hitRate * 100).toStringAsFixed(1)}%, '
'types: ${typeCount.keys.join(', ')}}';
}
}

View File

@@ -0,0 +1,59 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cache_item.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class CacheItemAdapter extends TypeAdapter<CacheItem> {
@override
final int typeId = 1;
@override
CacheItem read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return CacheItem(
key: fields[0] as String,
data: fields[1] as dynamic,
createdAt: fields[2] as DateTime,
expiresAt: fields[3] as DateTime,
dataType: fields[4] as String,
metadata: (fields[5] as Map?)?.cast<String, dynamic>(),
version: fields[6] as int,
);
}
@override
void write(BinaryWriter writer, CacheItem obj) {
writer
..writeByte(7)
..writeByte(0)
..write(obj.key)
..writeByte(1)
..write(obj.data)
..writeByte(2)
..write(obj.createdAt)
..writeByte(3)
..write(obj.expiresAt)
..writeByte(4)
..write(obj.dataType)
..writeByte(5)
..write(obj.metadata)
..writeByte(6)
..write(obj.version);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CacheItemAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,379 @@
import 'package:hive/hive.dart';
part 'user_preferences.g.dart';
/// User preferences model for storing user-specific settings and data
@HiveType(typeId: 2)
class UserPreferences extends HiveObject {
@HiveField(0)
final String userId;
@HiveField(1)
final String displayName;
@HiveField(2)
final String? email;
@HiveField(3)
final String? avatarUrl;
@HiveField(4)
final Map<String, dynamic> preferences;
@HiveField(5)
final List<String> favoriteItems;
@HiveField(6)
final Map<String, DateTime> lastAccessed;
@HiveField(7)
final DateTime createdAt;
@HiveField(8)
final DateTime lastUpdated;
@HiveField(9)
final int version;
UserPreferences({
required this.userId,
required this.displayName,
this.email,
this.avatarUrl,
this.preferences = const {},
this.favoriteItems = const [],
this.lastAccessed = const {},
required this.createdAt,
required this.lastUpdated,
this.version = 1,
});
/// Create a new user preferences instance
factory UserPreferences.create({
required String userId,
required String displayName,
String? email,
String? avatarUrl,
Map<String, dynamic>? preferences,
List<String>? favoriteItems,
}) {
final now = DateTime.now();
return UserPreferences(
userId: userId,
displayName: displayName,
email: email,
avatarUrl: avatarUrl,
preferences: preferences ?? {},
favoriteItems: favoriteItems ?? [],
lastAccessed: {},
createdAt: now,
lastUpdated: now,
version: 1,
);
}
/// Create default user preferences for anonymous user
factory UserPreferences.anonymous() {
final now = DateTime.now();
return UserPreferences(
userId: 'anonymous',
displayName: 'Anonymous User',
preferences: _getDefaultPreferences(),
favoriteItems: [],
lastAccessed: {},
createdAt: now,
lastUpdated: now,
version: 1,
);
}
/// Get default preferences
static Map<String, dynamic> _getDefaultPreferences() {
return {
// UI Preferences
'compactMode': false,
'showThumbnails': true,
'gridColumns': 2,
'sortBy': 'name',
'sortOrder': 'asc', // 'asc' or 'desc'
// Notification Preferences
'pushNotifications': true,
'emailNotifications': false,
'notificationTypes': ['updates', 'recommendations'],
// Privacy Preferences
'analyticsOptIn': false,
'shareUsageData': false,
'personalizedRecommendations': true,
// Content Preferences
'autoPlay': false,
'highQualityImages': true,
'downloadQuality': 'medium', // 'low', 'medium', 'high'
'cacheSize': 500, // MB
// Accessibility
'textSize': 'normal', // 'small', 'normal', 'large'
'highContrast': false,
'reduceAnimations': false,
};
}
/// Create a copy with modified fields
UserPreferences copyWith({
String? userId,
String? displayName,
String? email,
String? avatarUrl,
Map<String, dynamic>? preferences,
List<String>? favoriteItems,
Map<String, DateTime>? lastAccessed,
DateTime? createdAt,
DateTime? lastUpdated,
int? version,
}) {
return UserPreferences(
userId: userId ?? this.userId,
displayName: displayName ?? this.displayName,
email: email ?? this.email,
avatarUrl: avatarUrl ?? this.avatarUrl,
preferences: preferences ?? this.preferences,
favoriteItems: favoriteItems ?? this.favoriteItems,
lastAccessed: lastAccessed ?? this.lastAccessed,
createdAt: createdAt ?? this.createdAt,
lastUpdated: lastUpdated ?? DateTime.now(),
version: version ?? this.version,
);
}
/// Get a preference value with type safety
T getPreference<T>(String key, T defaultValue) {
final value = preferences[key];
if (value is T) {
return value;
}
return defaultValue;
}
/// Set a preference value
UserPreferences setPreference(String key, dynamic value) {
final updatedPreferences = Map<String, dynamic>.from(preferences);
updatedPreferences[key] = value;
return copyWith(
preferences: updatedPreferences,
lastUpdated: DateTime.now(),
);
}
/// Remove a preference
UserPreferences removePreference(String key) {
final updatedPreferences = Map<String, dynamic>.from(preferences);
updatedPreferences.remove(key);
return copyWith(
preferences: updatedPreferences,
lastUpdated: DateTime.now(),
);
}
/// Add item to favorites
UserPreferences addFavorite(String itemId) {
if (favoriteItems.contains(itemId)) return this;
final updatedFavorites = List<String>.from(favoriteItems);
updatedFavorites.add(itemId);
return copyWith(
favoriteItems: updatedFavorites,
lastUpdated: DateTime.now(),
);
}
/// Remove item from favorites
UserPreferences removeFavorite(String itemId) {
if (!favoriteItems.contains(itemId)) return this;
final updatedFavorites = List<String>.from(favoriteItems);
updatedFavorites.remove(itemId);
return copyWith(
favoriteItems: updatedFavorites,
lastUpdated: DateTime.now(),
);
}
/// Check if item is favorite
bool isFavorite(String itemId) {
return favoriteItems.contains(itemId);
}
/// Update last accessed time for an item
UserPreferences updateLastAccessed(String itemId) {
final updatedLastAccessed = Map<String, DateTime>.from(lastAccessed);
updatedLastAccessed[itemId] = DateTime.now();
return copyWith(
lastAccessed: updatedLastAccessed,
lastUpdated: DateTime.now(),
);
}
/// Get last accessed time for an item
DateTime? getLastAccessed(String itemId) {
return lastAccessed[itemId];
}
/// Get recently accessed items (sorted by most recent)
List<String> getRecentlyAccessed({int limit = 10}) {
final entries = lastAccessed.entries.toList();
entries.sort((a, b) => b.value.compareTo(a.value));
return entries.take(limit).map((e) => e.key).toList();
}
/// Clean old last accessed entries (older than specified days)
UserPreferences cleanOldAccess({int maxAgeDays = 30}) {
final cutoffDate = DateTime.now().subtract(Duration(days: maxAgeDays));
final updatedLastAccessed = Map<String, DateTime>.from(lastAccessed);
updatedLastAccessed.removeWhere((key, value) => value.isBefore(cutoffDate));
return copyWith(
lastAccessed: updatedLastAccessed,
lastUpdated: DateTime.now(),
);
}
/// Get user statistics
Map<String, dynamic> getStats() {
return {
'userId': userId,
'displayName': displayName,
'totalFavorites': favoriteItems.length,
'totalAccessedItems': lastAccessed.length,
'recentlyAccessed': getRecentlyAccessed(limit: 5),
'accountAge': DateTime.now().difference(createdAt).inDays,
'lastActive': lastUpdated,
'preferences': preferences.keys.length,
};
}
/// Convert to Map for JSON serialization
Map<String, dynamic> toMap() {
return {
'userId': userId,
'displayName': displayName,
'email': email,
'avatarUrl': avatarUrl,
'preferences': preferences,
'favoriteItems': favoriteItems,
'lastAccessed': lastAccessed.map((k, v) => MapEntry(k, v.toIso8601String())),
'createdAt': createdAt.toIso8601String(),
'lastUpdated': lastUpdated.toIso8601String(),
'version': version,
};
}
/// Create from Map for JSON deserialization
factory UserPreferences.fromMap(Map<String, dynamic> map) {
return UserPreferences(
userId: map['userId'] as String,
displayName: map['displayName'] as String,
email: map['email'] as String?,
avatarUrl: map['avatarUrl'] as String?,
preferences: Map<String, dynamic>.from(map['preferences'] ?? {}),
favoriteItems: List<String>.from(map['favoriteItems'] ?? []),
lastAccessed: (map['lastAccessed'] as Map<String, dynamic>?)
?.map((k, v) => MapEntry(k, DateTime.parse(v as String))) ?? {},
createdAt: DateTime.parse(map['createdAt'] as String),
lastUpdated: DateTime.parse(map['lastUpdated'] as String),
version: map['version'] as int? ?? 1,
);
}
/// Check if preferences need migration
bool needsMigration() {
const currentVersion = 1;
return version < currentVersion;
}
/// Migrate preferences to current version
UserPreferences migrate() {
if (!needsMigration()) return this;
var migrated = this;
// Add migration logic here as needed
// Example:
// if (version < 2) {
// migrated = _migrateToVersion2(migrated);
// }
return migrated.copyWith(version: 1);
}
@override
String toString() {
return 'UserPreferences{userId: $userId, displayName: $displayName, '
'favorites: ${favoriteItems.length}, preferences: ${preferences.keys.length}, '
'lastUpdated: $lastUpdated}';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UserPreferences &&
other.userId == userId &&
other.displayName == displayName &&
other.email == email &&
other.avatarUrl == avatarUrl &&
_mapEquals(other.preferences, preferences) &&
_listEquals(other.favoriteItems, favoriteItems) &&
_dateMapEquals(other.lastAccessed, lastAccessed) &&
other.createdAt == createdAt &&
other.lastUpdated == lastUpdated &&
other.version == version;
}
bool _mapEquals(Map<String, dynamic> a, Map<String, dynamic> b) {
if (a.length != b.length) return false;
for (final key in a.keys) {
if (!b.containsKey(key) || a[key] != b[key]) return false;
}
return true;
}
bool _listEquals(List<String> a, List<String> b) {
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
bool _dateMapEquals(Map<String, DateTime> a, Map<String, DateTime> b) {
if (a.length != b.length) return false;
for (final key in a.keys) {
if (!b.containsKey(key) || a[key] != b[key]) return false;
}
return true;
}
@override
int get hashCode {
return Object.hash(
userId,
displayName,
email,
avatarUrl,
preferences.hashCode,
favoriteItems.hashCode,
lastAccessed.hashCode,
createdAt,
lastUpdated,
version,
);
}
}

View File

@@ -0,0 +1,68 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_preferences.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class UserPreferencesAdapter extends TypeAdapter<UserPreferences> {
@override
final int typeId = 2;
@override
UserPreferences read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return UserPreferences(
userId: fields[0] as String,
displayName: fields[1] as String,
email: fields[2] as String?,
avatarUrl: fields[3] as String?,
preferences: (fields[4] as Map).cast<String, dynamic>(),
favoriteItems: (fields[5] as List).cast<String>(),
lastAccessed: (fields[6] as Map).cast<String, DateTime>(),
createdAt: fields[7] as DateTime,
lastUpdated: fields[8] as DateTime,
version: fields[9] as int,
);
}
@override
void write(BinaryWriter writer, UserPreferences obj) {
writer
..writeByte(10)
..writeByte(0)
..write(obj.userId)
..writeByte(1)
..write(obj.displayName)
..writeByte(2)
..write(obj.email)
..writeByte(3)
..write(obj.avatarUrl)
..writeByte(4)
..write(obj.preferences)
..writeByte(5)
..write(obj.favoriteItems)
..writeByte(6)
..write(obj.lastAccessed)
..writeByte(7)
..write(obj.createdAt)
..writeByte(8)
..write(obj.lastUpdated)
..writeByte(9)
..write(obj.version);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserPreferencesAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,438 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../repositories/settings_repository.dart';
import '../repositories/cache_repository.dart';
import '../repositories/user_preferences_repository.dart';
import '../models/app_settings.dart';
import '../models/cache_item.dart';
import '../models/user_preferences.dart';
/// Providers for database repositories and services
/// Settings repository provider
final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
return SettingsRepository();
});
/// Cache repository provider
final cacheRepositoryProvider = Provider<CacheRepository>((ref) {
return CacheRepository();
});
/// User preferences repository provider
final userPreferencesRepositoryProvider = Provider<UserPreferencesRepository>((ref) {
return UserPreferencesRepository();
});
/// Current app settings provider
final appSettingsProvider = StateNotifierProvider<AppSettingsNotifier, AppSettings>((ref) {
final repository = ref.watch(settingsRepositoryProvider);
return AppSettingsNotifier(repository);
});
/// App settings notifier
class AppSettingsNotifier extends StateNotifier<AppSettings> {
final SettingsRepository _repository;
AppSettingsNotifier(this._repository) : super(AppSettings.defaultSettings()) {
_loadSettings();
}
void _loadSettings() {
try {
state = _repository.getSettings();
} catch (e) {
// Keep default settings if loading fails
state = AppSettings.defaultSettings();
}
}
/// Update theme mode
Future<void> updateThemeMode(String themeMode) async {
try {
await _repository.updateThemeMode(themeMode);
state = state.copyWith(themeMode: themeMode);
} catch (e) {
// Handle error - maybe show a snackbar
rethrow;
}
}
/// Update locale
Future<void> updateLocale(String locale) async {
try {
await _repository.updateLocale(locale);
state = state.copyWith(locale: locale);
} catch (e) {
rethrow;
}
}
/// Update notifications enabled
Future<void> updateNotificationsEnabled(bool enabled) async {
try {
await _repository.updateNotificationsEnabled(enabled);
state = state.copyWith(notificationsEnabled: enabled);
} catch (e) {
rethrow;
}
}
/// Update analytics enabled
Future<void> updateAnalyticsEnabled(bool enabled) async {
try {
await _repository.updateAnalyticsEnabled(enabled);
state = state.copyWith(analyticsEnabled: enabled);
} catch (e) {
rethrow;
}
}
/// Update cache strategy
Future<void> updateCacheStrategy(String strategy) async {
try {
await _repository.updateCacheStrategy(strategy);
state = state.copyWith(cacheStrategy: strategy);
} catch (e) {
rethrow;
}
}
/// Update cache expiration hours
Future<void> updateCacheExpirationHours(int hours) async {
try {
await _repository.updateCacheExpirationHours(hours);
state = state.copyWith(cacheExpirationHours: hours);
} catch (e) {
rethrow;
}
}
/// Update auto update enabled
Future<void> updateAutoUpdateEnabled(bool enabled) async {
try {
await _repository.updateAutoUpdateEnabled(enabled);
state = state.copyWith(autoUpdateEnabled: enabled);
} catch (e) {
rethrow;
}
}
/// Set custom setting
Future<void> setCustomSetting(String key, dynamic value) async {
try {
await _repository.setCustomSetting(key, value);
state = state.setCustomSetting(key, value);
} catch (e) {
rethrow;
}
}
/// Remove custom setting
Future<void> removeCustomSetting(String key) async {
try {
await _repository.removeCustomSetting(key);
state = state.removeCustomSetting(key);
} catch (e) {
rethrow;
}
}
/// Reset to default settings
Future<void> resetToDefault() async {
try {
await _repository.resetToDefault();
state = AppSettings.defaultSettings();
} catch (e) {
rethrow;
}
}
}
/// Theme mode provider (derived from app settings)
final themeModeProvider = Provider<ThemeMode>((ref) {
final settings = ref.watch(appSettingsProvider);
switch (settings.themeMode) {
case 'light':
return ThemeMode.light;
case 'dark':
return ThemeMode.dark;
case 'system':
default:
return ThemeMode.system;
}
});
/// Current locale provider (derived from app settings)
final localeProvider = Provider<String>((ref) {
final settings = ref.watch(appSettingsProvider);
return settings.locale;
});
/// Notifications enabled provider
final notificationsEnabledProvider = Provider<bool>((ref) {
final settings = ref.watch(appSettingsProvider);
return settings.notificationsEnabled;
});
/// Analytics enabled provider
final analyticsEnabledProvider = Provider<bool>((ref) {
final settings = ref.watch(appSettingsProvider);
return settings.analyticsEnabled;
});
/// Cache strategy provider
final cacheStrategyProvider = Provider<String>((ref) {
final settings = ref.watch(appSettingsProvider);
return settings.cacheStrategy;
});
/// Cache expiration hours provider
final cacheExpirationHoursProvider = Provider<int>((ref) {
final settings = ref.watch(appSettingsProvider);
return settings.cacheExpirationHours;
});
/// Auto update enabled provider
final autoUpdateEnabledProvider = Provider<bool>((ref) {
final settings = ref.watch(appSettingsProvider);
return settings.autoUpdateEnabled;
});
/// Cache statistics provider
final cacheStatsProvider = FutureProvider<CacheStats>((ref) async {
final repository = ref.watch(cacheRepositoryProvider);
return repository.getStats();
});
/// Cache maintenance provider
final cacheMaintenanceProvider = FutureProvider.family<Map<String, dynamic>, bool>(
(ref, performMaintenance) async {
if (!performMaintenance) return {};
final repository = ref.watch(cacheRepositoryProvider);
return repository.performMaintenance();
},
);
/// Cache item provider for a specific key
final cacheItemProvider = Provider.family<CacheItem?, String>((ref, key) {
final repository = ref.watch(cacheRepositoryProvider);
return repository.getCacheItem(key);
});
/// Cache data provider for a specific key with type safety
final cacheDataProvider = Provider.family<dynamic, String>((ref, key) {
final repository = ref.watch(cacheRepositoryProvider);
return repository.get(key);
});
/// Database statistics provider
final databaseStatsProvider = Provider<Map<String, dynamic>>((ref) {
final settingsRepo = ref.watch(settingsRepositoryProvider);
final cacheRepo = ref.watch(cacheRepositoryProvider);
final settingsStats = settingsRepo.getSettingsStats();
final cacheStats = cacheRepo.getStats();
return {
'settings': settingsStats,
'cache': cacheStats.toMap(),
'totalItems': settingsStats['totalSettingsInBox'] + cacheStats.totalItems,
};
});
/// Provider to clear expired cache items
final clearExpiredCacheProvider = FutureProvider<int>((ref) async {
final repository = ref.watch(cacheRepositoryProvider);
return repository.clearExpired();
});
/// Provider to get cache keys by pattern
final cacheKeysByPatternProvider = Provider.family<List<String>, String>((ref, pattern) {
final repository = ref.watch(cacheRepositoryProvider);
return repository.getKeysByPattern(pattern);
});
/// Provider to get cache keys by type
final cacheKeysByTypeProvider = Provider.family<List<String>, String>((ref, type) {
final repository = ref.watch(cacheRepositoryProvider);
return repository.getKeysByType(type);
});
/// Current user preferences provider
final userPreferencesProvider = StateNotifierProvider.family<UserPreferencesNotifier, UserPreferences?, String?>((ref, userId) {
final repository = ref.watch(userPreferencesRepositoryProvider);
return UserPreferencesNotifier(repository, userId);
});
/// User preferences notifier
class UserPreferencesNotifier extends StateNotifier<UserPreferences?> {
final UserPreferencesRepository _repository;
final String? _userId;
UserPreferencesNotifier(this._repository, this._userId) : super(null) {
_loadUserPreferences();
}
void _loadUserPreferences() {
try {
state = _repository.getUserPreferences(_userId);
} catch (e) {
state = null;
}
}
/// Create new user preferences
Future<void> createUserPreferences({
required String userId,
required String displayName,
String? email,
String? avatarUrl,
Map<String, dynamic>? preferences,
List<String>? favoriteItems,
}) async {
try {
final newPreferences = await _repository.createUserPreferences(
userId: userId,
displayName: displayName,
email: email,
avatarUrl: avatarUrl,
preferences: preferences,
favoriteItems: favoriteItems,
);
state = newPreferences;
} catch (e) {
rethrow;
}
}
/// Update profile information
Future<void> updateProfile({
String? displayName,
String? email,
String? avatarUrl,
}) async {
if (state == null) return;
try {
await _repository.updateProfile(
userId: _userId,
displayName: displayName,
email: email,
avatarUrl: avatarUrl,
);
state = state!.copyWith(
displayName: displayName ?? state!.displayName,
email: email ?? state!.email,
avatarUrl: avatarUrl ?? state!.avatarUrl,
);
} catch (e) {
rethrow;
}
}
/// Set preference
Future<void> setPreference(String key, dynamic value) async {
if (state == null) return;
try {
await _repository.setPreference(key, value, _userId);
state = state!.setPreference(key, value);
} catch (e) {
rethrow;
}
}
/// Remove preference
Future<void> removePreference(String key) async {
if (state == null) return;
try {
await _repository.removePreference(key, _userId);
state = state!.removePreference(key);
} catch (e) {
rethrow;
}
}
/// Add favorite
Future<void> addFavorite(String itemId) async {
if (state == null) return;
try {
await _repository.addFavorite(itemId, _userId);
state = state!.addFavorite(itemId);
} catch (e) {
rethrow;
}
}
/// Remove favorite
Future<void> removeFavorite(String itemId) async {
if (state == null) return;
try {
await _repository.removeFavorite(itemId, _userId);
state = state!.removeFavorite(itemId);
} catch (e) {
rethrow;
}
}
/// Update last accessed
Future<void> updateLastAccessed(String itemId) async {
if (state == null) return;
try {
await _repository.updateLastAccessed(itemId, _userId);
state = state!.updateLastAccessed(itemId);
} catch (e) {
rethrow;
}
}
/// Clear user preferences
Future<void> clearPreferences() async {
try {
await _repository.clearUserPreferences(_userId);
state = null;
} catch (e) {
rethrow;
}
}
}
/// User preference provider for specific key
final userPreferenceProvider = Provider.family.autoDispose<dynamic, (String, dynamic, String?)>((ref, params) {
final (key, defaultValue, userId) = params;
final repository = ref.watch(userPreferencesRepositoryProvider);
return repository.getPreference(key, defaultValue, userId);
});
/// User favorites provider
final userFavoritesProvider = Provider.family<List<String>, String?>((ref, userId) {
final repository = ref.watch(userPreferencesRepositoryProvider);
return repository.getFavorites(userId);
});
/// Recently accessed provider
final recentlyAccessedProvider = Provider.family<List<String>, (int, String?)>((ref, params) {
final (limit, userId) = params;
final repository = ref.watch(userPreferencesRepositoryProvider);
return repository.getRecentlyAccessed(limit: limit, userId: userId);
});
/// Is favorite provider
final isFavoriteProvider = Provider.family<bool, (String, String?)>((ref, params) {
final (itemId, userId) = params;
final repository = ref.watch(userPreferencesRepositoryProvider);
return repository.isFavorite(itemId, userId);
});
/// User stats provider
final userStatsProvider = Provider.family<Map<String, dynamic>, String?>((ref, userId) {
final repository = ref.watch(userPreferencesRepositoryProvider);
return repository.getUserStats(userId);
});

View File

@@ -0,0 +1,480 @@
import 'package:flutter/foundation.dart';
import '../hive_service.dart';
import '../models/cache_item.dart';
/// Repository for managing cached data using Hive
class CacheRepository {
/// Store data in cache with expiration
Future<void> put<T>({
required String key,
required T data,
required Duration expirationDuration,
Map<String, dynamic>? metadata,
}) async {
try {
final box = HiveService.cacheBox;
final cacheItem = CacheItem.create(
key: key,
data: data,
expirationDuration: expirationDuration,
metadata: metadata,
);
await box.put(key, cacheItem);
debugPrint('✅ Cache item stored: $key');
} catch (e, stackTrace) {
debugPrint('❌ Error storing cache item $key: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
/// Store permanent data in cache (never expires)
Future<void> putPermanent<T>({
required String key,
required T data,
Map<String, dynamic>? metadata,
}) async {
try {
final box = HiveService.cacheBox;
final cacheItem = CacheItem.permanent(
key: key,
data: data,
metadata: metadata,
);
await box.put(key, cacheItem);
debugPrint('✅ Permanent cache item stored: $key');
} catch (e, stackTrace) {
debugPrint('❌ Error storing permanent cache item $key: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
/// Get data from cache
T? get<T>(String key) {
try {
final box = HiveService.cacheBox;
final cacheItem = box.get(key);
if (cacheItem == null) {
debugPrint('📭 Cache miss: $key');
return null;
}
if (cacheItem.isExpired) {
debugPrint('⏰ Cache expired: $key');
// Optionally remove expired item
delete(key);
return null;
}
debugPrint('✅ Cache hit: $key');
return cacheItem.data as T?;
} catch (e) {
debugPrint('❌ Error getting cache item $key: $e');
return null;
}
}
/// Get cache item with full metadata
CacheItem? getCacheItem(String key) {
try {
final box = HiveService.cacheBox;
final cacheItem = box.get(key);
if (cacheItem == null) {
return null;
}
if (cacheItem.isExpired) {
// Optionally remove expired item
delete(key);
return null;
}
return cacheItem;
} catch (e) {
debugPrint('❌ Error getting cache item $key: $e');
return null;
}
}
/// Check if key exists and is valid (not expired)
bool contains(String key) {
try {
final box = HiveService.cacheBox;
final cacheItem = box.get(key);
if (cacheItem == null) return false;
if (cacheItem.isExpired) {
delete(key);
return false;
}
return true;
} catch (e) {
debugPrint('❌ Error checking cache item $key: $e');
return false;
}
}
/// Check if key exists regardless of expiration
bool containsKey(String key) {
try {
final box = HiveService.cacheBox;
return box.containsKey(key);
} catch (e) {
debugPrint('❌ Error checking key $key: $e');
return false;
}
}
/// Delete specific cache item
Future<void> delete(String key) async {
try {
final box = HiveService.cacheBox;
await box.delete(key);
debugPrint('🗑️ Cache item deleted: $key');
} catch (e) {
debugPrint('❌ Error deleting cache item $key: $e');
}
}
/// Delete multiple cache items
Future<void> deleteMultiple(List<String> keys) async {
try {
final box = HiveService.cacheBox;
for (final key in keys) {
await box.delete(key);
}
debugPrint('🗑️ Multiple cache items deleted: ${keys.length} items');
} catch (e) {
debugPrint('❌ Error deleting multiple cache items: $e');
}
}
/// Clear all expired items
Future<int> cleanExpiredItems() async {
return await clearExpired();
}
/// Clear all expired items
Future<int> clearExpired() async {
try {
final box = HiveService.cacheBox;
final expiredKeys = <String>[];
final now = DateTime.now();
for (final key in box.keys) {
final cacheItem = box.get(key);
if (cacheItem != null && now.isAfter(cacheItem.expiresAt)) {
expiredKeys.add(key as String);
}
}
for (final key in expiredKeys) {
await box.delete(key);
}
debugPrint('🧹 Cleared ${expiredKeys.length} expired cache items');
return expiredKeys.length;
} catch (e) {
debugPrint('❌ Error clearing expired items: $e');
return 0;
}
}
/// Clear all cache items
Future<void> clearAll() async {
try {
final box = HiveService.cacheBox;
final count = box.length;
await box.clear();
debugPrint('🧹 Cleared all cache items: $count items');
} catch (e) {
debugPrint('❌ Error clearing all cache items: $e');
rethrow;
}
}
/// Clear cache items by pattern
Future<int> clearByPattern(Pattern pattern) async {
try {
final box = HiveService.cacheBox;
final keysToDelete = <String>[];
for (final key in box.keys) {
if (key is String && key.contains(pattern)) {
keysToDelete.add(key);
}
}
for (final key in keysToDelete) {
await box.delete(key);
}
debugPrint('🧹 Cleared ${keysToDelete.length} cache items matching pattern: $pattern');
return keysToDelete.length;
} catch (e) {
debugPrint('❌ Error clearing cache items by pattern: $e');
return 0;
}
}
/// Clear cache items by type
Future<int> clearByType(String dataType) async {
try {
final box = HiveService.cacheBox;
final keysToDelete = <String>[];
for (final key in box.keys) {
final cacheItem = box.get(key);
if (cacheItem != null && cacheItem.dataType == dataType) {
keysToDelete.add(key as String);
}
}
for (final key in keysToDelete) {
await box.delete(key);
}
debugPrint('🧹 Cleared ${keysToDelete.length} cache items of type: $dataType');
return keysToDelete.length;
} catch (e) {
debugPrint('❌ Error clearing cache items by type: $e');
return 0;
}
}
/// Refresh cache item with new expiration
Future<bool> refresh(String key, Duration newExpirationDuration) async {
try {
final box = HiveService.cacheBox;
final cacheItem = box.get(key);
if (cacheItem == null) return false;
final refreshedItem = cacheItem.refresh(newExpirationDuration);
await box.put(key, refreshedItem);
debugPrint('🔄 Cache item refreshed: $key');
return true;
} catch (e) {
debugPrint('❌ Error refreshing cache item $key: $e');
return false;
}
}
/// Update cache item data
Future<bool> update<T>(String key, T newData, {Duration? newExpirationDuration}) async {
try {
final box = HiveService.cacheBox;
final cacheItem = box.get(key);
if (cacheItem == null) return false;
final updatedItem = cacheItem.updateData(newData, newExpirationDuration: newExpirationDuration);
await box.put(key, updatedItem);
debugPrint('📝 Cache item updated: $key');
return true;
} catch (e) {
debugPrint('❌ Error updating cache item $key: $e');
return false;
}
}
/// Get all keys in cache
List<String> getAllKeys() {
try {
final box = HiveService.cacheBox;
return box.keys.cast<String>().toList();
} catch (e) {
debugPrint('❌ Error getting all keys: $e');
return [];
}
}
/// Get keys by pattern
List<String> getKeysByPattern(Pattern pattern) {
try {
final box = HiveService.cacheBox;
return box.keys
.cast<String>()
.where((key) => key.contains(pattern))
.toList();
} catch (e) {
debugPrint('❌ Error getting keys by pattern: $e');
return [];
}
}
/// Get keys by data type
List<String> getKeysByType(String dataType) {
try {
final box = HiveService.cacheBox;
final keys = <String>[];
for (final key in box.keys) {
final cacheItem = box.get(key);
if (cacheItem != null && cacheItem.dataType == dataType) {
keys.add(key as String);
}
}
return keys;
} catch (e) {
debugPrint('❌ Error getting keys by type: $e');
return [];
}
}
/// Get cache statistics
CacheStats getStats() {
try {
final box = HiveService.cacheBox;
final now = DateTime.now();
var validItems = 0;
var expiredItems = 0;
DateTime? oldestItem;
DateTime? newestItem;
final typeCount = <String, int>{};
for (final key in box.keys) {
final cacheItem = box.get(key);
if (cacheItem == null) continue;
// Count by expiration status
if (now.isAfter(cacheItem.expiresAt)) {
expiredItems++;
} else {
validItems++;
}
// Track oldest and newest items
if (oldestItem == null || cacheItem.createdAt.isBefore(oldestItem)) {
oldestItem = cacheItem.createdAt;
}
if (newestItem == null || cacheItem.createdAt.isAfter(newestItem)) {
newestItem = cacheItem.createdAt;
}
// Count by type
typeCount[cacheItem.dataType] = (typeCount[cacheItem.dataType] ?? 0) + 1;
}
return CacheStats(
totalItems: box.length,
validItems: validItems,
expiredItems: expiredItems,
oldestItem: oldestItem ?? DateTime.now(),
newestItem: newestItem ?? DateTime.now(),
typeCount: typeCount,
);
} catch (e) {
debugPrint('❌ Error getting cache stats: $e');
return CacheStats(
totalItems: 0,
validItems: 0,
expiredItems: 0,
oldestItem: DateTime.now(),
newestItem: DateTime.now(),
typeCount: const {},
);
}
}
/// Get cache size in bytes (approximate)
int getApproximateSize() {
try {
final box = HiveService.cacheBox;
// This is an approximation as Hive doesn't provide exact size
return box.length * 1024; // Assume average 1KB per item
} catch (e) {
debugPrint('❌ Error getting cache size: $e');
return 0;
}
}
/// Compact cache storage
Future<void> compact() async {
try {
final box = HiveService.cacheBox;
await box.compact();
debugPrint('✅ Cache storage compacted');
} catch (e) {
debugPrint('❌ Error compacting cache: $e');
}
}
/// Export cache data (for debugging or backup)
Map<String, dynamic> exportCache({bool includeExpired = false}) {
try {
final box = HiveService.cacheBox;
final now = DateTime.now();
final exportData = <String, dynamic>{};
for (final key in box.keys) {
final cacheItem = box.get(key);
if (cacheItem == null) continue;
if (!includeExpired && now.isAfter(cacheItem.expiresAt)) {
continue;
}
exportData[key as String] = cacheItem.toMap();
}
return exportData;
} catch (e) {
debugPrint('❌ Error exporting cache: $e');
return {};
}
}
/// Watch cache changes for a specific key
Stream<CacheItem?> watch(String key) {
try {
final box = HiveService.cacheBox;
return box.watch(key: key).map((event) => event.value as CacheItem?);
} catch (e) {
debugPrint('❌ Error watching cache key $key: $e');
return Stream.value(null);
}
}
/// Perform cache maintenance (cleanup expired items, compact storage)
Future<Map<String, dynamic>> performMaintenance() async {
try {
final startTime = DateTime.now();
// Get stats before maintenance
final statsBefore = getStats();
// Clear expired items
final expiredCount = await clearExpired();
// Compact storage
await compact();
// Get stats after maintenance
final statsAfter = getStats();
final maintenanceTime = DateTime.now().difference(startTime);
final result = {
'expiredItemsRemoved': expiredCount,
'itemsBefore': statsBefore.totalItems,
'itemsAfter': statsAfter.totalItems,
'maintenanceTimeMs': maintenanceTime.inMilliseconds,
'completedAt': DateTime.now().toIso8601String(),
};
debugPrint('🔧 Cache maintenance completed: $result');
return result;
} catch (e) {
debugPrint('❌ Error during cache maintenance: $e');
return {'error': e.toString()};
}
}
}

View File

@@ -0,0 +1,249 @@
import 'package:flutter/foundation.dart';
import '../hive_service.dart';
import '../models/app_settings.dart';
/// Repository for managing application settings using Hive
class SettingsRepository {
static const String _defaultKey = 'app_settings';
/// Get the current app settings
AppSettings getSettings() {
try {
final box = HiveService.appSettingsBox;
final settings = box.get(_defaultKey);
if (settings == null) {
// Return default settings if none exist
final defaultSettings = AppSettings.defaultSettings();
saveSettings(defaultSettings);
return defaultSettings;
}
// Check if settings need migration
if (settings.version < 1) {
final migratedSettings = _migrateSettings(settings);
saveSettings(migratedSettings);
return migratedSettings;
}
return settings;
} catch (e, stackTrace) {
debugPrint('Error getting settings: $e');
debugPrint('Stack trace: $stackTrace');
// Return default settings on error
return AppSettings.defaultSettings();
}
}
/// Save app settings
Future<void> saveSettings(AppSettings settings) async {
try {
final box = HiveService.appSettingsBox;
final updatedSettings = settings.copyWith(lastUpdated: DateTime.now());
await box.put(_defaultKey, updatedSettings);
debugPrint('✅ Settings saved successfully');
} catch (e, stackTrace) {
debugPrint('❌ Error saving settings: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
/// Update theme mode
Future<void> updateThemeMode(String themeMode) async {
final currentSettings = getSettings();
final updatedSettings = currentSettings.copyWith(themeMode: themeMode);
await saveSettings(updatedSettings);
}
/// Update locale
Future<void> updateLocale(String locale) async {
final currentSettings = getSettings();
final updatedSettings = currentSettings.copyWith(locale: locale);
await saveSettings(updatedSettings);
}
/// Update notifications enabled
Future<void> updateNotificationsEnabled(bool enabled) async {
final currentSettings = getSettings();
final updatedSettings = currentSettings.copyWith(notificationsEnabled: enabled);
await saveSettings(updatedSettings);
}
/// Update analytics enabled
Future<void> updateAnalyticsEnabled(bool enabled) async {
final currentSettings = getSettings();
final updatedSettings = currentSettings.copyWith(analyticsEnabled: enabled);
await saveSettings(updatedSettings);
}
/// Update cache strategy
Future<void> updateCacheStrategy(String strategy) async {
final currentSettings = getSettings();
final updatedSettings = currentSettings.copyWith(cacheStrategy: strategy);
await saveSettings(updatedSettings);
}
/// Update cache expiration hours
Future<void> updateCacheExpirationHours(int hours) async {
final currentSettings = getSettings();
final updatedSettings = currentSettings.copyWith(cacheExpirationHours: hours);
await saveSettings(updatedSettings);
}
/// Update auto update enabled
Future<void> updateAutoUpdateEnabled(bool enabled) async {
final currentSettings = getSettings();
final updatedSettings = currentSettings.copyWith(autoUpdateEnabled: enabled);
await saveSettings(updatedSettings);
}
/// Set custom setting
Future<void> setCustomSetting(String key, dynamic value) async {
final currentSettings = getSettings();
final updatedSettings = currentSettings.setCustomSetting(key, value);
await saveSettings(updatedSettings);
}
/// Get custom setting
T? getCustomSetting<T>(String key) {
final settings = getSettings();
return settings.getCustomSetting<T>(key);
}
/// Remove custom setting
Future<void> removeCustomSetting(String key) async {
final currentSettings = getSettings();
final updatedSettings = currentSettings.removeCustomSetting(key);
await saveSettings(updatedSettings);
}
/// Reset to default settings
Future<void> resetToDefault() async {
final defaultSettings = AppSettings.defaultSettings();
await saveSettings(defaultSettings);
debugPrint('✅ Settings reset to default');
}
/// Export settings to Map (for backup)
Map<String, dynamic> exportSettings() {
try {
final settings = getSettings();
return settings.toMap();
} catch (e) {
debugPrint('❌ Error exporting settings: $e');
return {};
}
}
/// Import settings from Map (for restore)
Future<bool> importSettings(Map<String, dynamic> settingsMap) async {
try {
final settings = AppSettings.fromMap(settingsMap);
await saveSettings(settings);
debugPrint('✅ Settings imported successfully');
return true;
} catch (e) {
debugPrint('❌ Error importing settings: $e');
return false;
}
}
/// Check if settings exist
bool hasSettings() {
try {
final box = HiveService.appSettingsBox;
return box.containsKey(_defaultKey);
} catch (e) {
debugPrint('❌ Error checking settings existence: $e');
return false;
}
}
/// Clear all settings (use with caution)
Future<void> clearSettings() async {
try {
final box = HiveService.appSettingsBox;
await box.delete(_defaultKey);
debugPrint('✅ Settings cleared');
} catch (e) {
debugPrint('❌ Error clearing settings: $e');
rethrow;
}
}
/// Get settings statistics
Map<String, dynamic> getSettingsStats() {
try {
final settings = getSettings();
final box = HiveService.appSettingsBox;
return {
'hasCustomSettings': settings.customSettings?.isNotEmpty ?? false,
'customSettingsCount': settings.customSettings?.length ?? 0,
'lastUpdated': settings.lastUpdated.toIso8601String(),
'version': settings.version,
'settingsAge': DateTime.now().difference(settings.lastUpdated).inDays,
'isExpired': settings.isExpired(),
'totalSettingsInBox': box.length,
};
} catch (e) {
debugPrint('❌ Error getting settings stats: $e');
return {};
}
}
/// Migrate settings from older version
AppSettings _migrateSettings(AppSettings oldSettings) {
debugPrint('🔄 Migrating settings from version ${oldSettings.version} to 1');
// Perform any necessary migrations here
// For now, just update the version and timestamp
return oldSettings.copyWith(
version: 1,
lastUpdated: DateTime.now(),
);
}
/// Validate settings
bool validateSettings(AppSettings settings) {
try {
// Basic validation
if (settings.version < 1) return false;
if (settings.cacheExpirationHours < 1) return false;
if (!['light', 'dark', 'system'].contains(settings.themeMode)) return false;
if (!['aggressive', 'normal', 'minimal'].contains(settings.cacheStrategy)) return false;
return true;
} catch (e) {
debugPrint('❌ Error validating settings: $e');
return false;
}
}
/// Watch settings changes
Stream<AppSettings> watchSettings() {
try {
final box = HiveService.appSettingsBox;
return box.watch(key: _defaultKey).map((event) {
final settings = event.value as AppSettings?;
return settings ?? AppSettings.defaultSettings();
});
} catch (e) {
debugPrint('❌ Error watching settings: $e');
return Stream.value(AppSettings.defaultSettings());
}
}
/// Compact settings storage
Future<void> compact() async {
try {
final box = HiveService.appSettingsBox;
await box.compact();
debugPrint('✅ Settings storage compacted');
} catch (e) {
debugPrint('❌ Error compacting settings: $e');
}
}
}

View File

@@ -0,0 +1,329 @@
import 'package:flutter/foundation.dart';
import '../hive_service.dart';
import '../models/user_preferences.dart';
/// Repository for managing user preferences using Hive
class UserPreferencesRepository {
static const String _defaultKey = 'current_user_preferences';
/// Get the current user preferences (alias for getUserPreferences)
UserPreferences? getPreferences([String? userId]) {
return getUserPreferences(userId);
}
/// Get the current user preferences
UserPreferences? getUserPreferences([String? userId]) {
try {
final box = HiveService.userDataBox;
final key = userId ?? _defaultKey;
final preferences = box.get(key);
if (preferences == null) return null;
// Check if preferences need migration
if (preferences.needsMigration()) {
final migratedPreferences = preferences.migrate();
saveUserPreferences(migratedPreferences, userId);
return migratedPreferences;
}
return preferences;
} catch (e, stackTrace) {
debugPrint('Error getting user preferences: $e');
debugPrint('Stack trace: $stackTrace');
return null;
}
}
/// Save user preferences
Future<void> saveUserPreferences(UserPreferences preferences, [String? userId]) async {
try {
final box = HiveService.userDataBox;
final key = userId ?? _defaultKey;
final updatedPreferences = preferences.copyWith(lastUpdated: DateTime.now());
await box.put(key, updatedPreferences);
debugPrint('✅ User preferences saved for key: $key');
} catch (e, stackTrace) {
debugPrint('❌ Error saving user preferences: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
/// Create new user preferences
Future<UserPreferences> createUserPreferences({
required String userId,
required String displayName,
String? email,
String? avatarUrl,
Map<String, dynamic>? preferences,
List<String>? favoriteItems,
}) async {
final userPreferences = UserPreferences.create(
userId: userId,
displayName: displayName,
email: email,
avatarUrl: avatarUrl,
preferences: preferences,
favoriteItems: favoriteItems,
);
await saveUserPreferences(userPreferences, userId);
return userPreferences;
}
/// Update user profile information
Future<void> updateProfile({
String? userId,
String? displayName,
String? email,
String? avatarUrl,
}) async {
final currentPreferences = getUserPreferences(userId);
if (currentPreferences == null) return;
final updatedPreferences = currentPreferences.copyWith(
displayName: displayName ?? currentPreferences.displayName,
email: email ?? currentPreferences.email,
avatarUrl: avatarUrl ?? currentPreferences.avatarUrl,
);
await saveUserPreferences(updatedPreferences, userId);
}
/// Set a user preference
Future<void> setPreference(String key, dynamic value, [String? userId]) async {
final currentPreferences = getUserPreferences(userId);
if (currentPreferences == null) return;
final updatedPreferences = currentPreferences.setPreference(key, value);
await saveUserPreferences(updatedPreferences, userId);
}
/// Get a user preference with type safety
T getPreference<T>(String key, T defaultValue, [String? userId]) {
final preferences = getUserPreferences(userId);
if (preferences == null) return defaultValue;
return preferences.getPreference<T>(key, defaultValue);
}
/// Remove a user preference
Future<void> removePreference(String key, [String? userId]) async {
final currentPreferences = getUserPreferences(userId);
if (currentPreferences == null) return;
final updatedPreferences = currentPreferences.removePreference(key);
await saveUserPreferences(updatedPreferences, userId);
}
/// Add item to favorites
Future<void> addFavorite(String itemId, [String? userId]) async {
final currentPreferences = getUserPreferences(userId);
if (currentPreferences == null) return;
final updatedPreferences = currentPreferences.addFavorite(itemId);
await saveUserPreferences(updatedPreferences, userId);
}
/// Remove item from favorites
Future<void> removeFavorite(String itemId, [String? userId]) async {
final currentPreferences = getUserPreferences(userId);
if (currentPreferences == null) return;
final updatedPreferences = currentPreferences.removeFavorite(itemId);
await saveUserPreferences(updatedPreferences, userId);
}
/// Check if item is favorite
bool isFavorite(String itemId, [String? userId]) {
final preferences = getUserPreferences(userId);
return preferences?.isFavorite(itemId) ?? false;
}
/// Get all favorite items
List<String> getFavorites([String? userId]) {
final preferences = getUserPreferences(userId);
return preferences?.favoriteItems ?? [];
}
/// Update last accessed time for an item
Future<void> updateLastAccessed(String itemId, [String? userId]) async {
final currentPreferences = getUserPreferences(userId);
if (currentPreferences == null) return;
final updatedPreferences = currentPreferences.updateLastAccessed(itemId);
await saveUserPreferences(updatedPreferences, userId);
}
/// Get last accessed time for an item
DateTime? getLastAccessed(String itemId, [String? userId]) {
final preferences = getUserPreferences(userId);
return preferences?.getLastAccessed(itemId);
}
/// Get recently accessed items
List<String> getRecentlyAccessed({int limit = 10, String? userId}) {
final preferences = getUserPreferences(userId);
return preferences?.getRecentlyAccessed(limit: limit) ?? [];
}
/// Clean old access records
Future<void> cleanOldAccess({int maxAgeDays = 30, String? userId}) async {
final currentPreferences = getUserPreferences(userId);
if (currentPreferences == null) return;
final updatedPreferences = currentPreferences.cleanOldAccess(maxAgeDays: maxAgeDays);
await saveUserPreferences(updatedPreferences, userId);
}
/// Get user statistics
Map<String, dynamic> getUserStats([String? userId]) {
final preferences = getUserPreferences(userId);
return preferences?.getStats() ?? {};
}
/// Export user preferences to Map (for backup)
Map<String, dynamic> exportUserPreferences([String? userId]) {
try {
final preferences = getUserPreferences(userId);
return preferences?.toMap() ?? {};
} catch (e) {
debugPrint('❌ Error exporting user preferences: $e');
return {};
}
}
/// Import user preferences from Map (for restore)
Future<bool> importUserPreferences(Map<String, dynamic> preferencesMap, [String? userId]) async {
try {
final preferences = UserPreferences.fromMap(preferencesMap);
await saveUserPreferences(preferences, userId);
debugPrint('✅ User preferences imported successfully');
return true;
} catch (e) {
debugPrint('❌ Error importing user preferences: $e');
return false;
}
}
/// Check if user preferences exist
bool hasUserPreferences([String? userId]) {
try {
final box = HiveService.userDataBox;
final key = userId ?? _defaultKey;
return box.containsKey(key);
} catch (e) {
debugPrint('❌ Error checking user preferences existence: $e');
return false;
}
}
/// Clear user preferences (use with caution)
Future<void> clearUserPreferences([String? userId]) async {
try {
final box = HiveService.userDataBox;
final key = userId ?? _defaultKey;
await box.delete(key);
debugPrint('✅ User preferences cleared for key: $key');
} catch (e) {
debugPrint('❌ Error clearing user preferences: $e');
rethrow;
}
}
/// Get all user IDs that have preferences stored
List<String> getAllUserIds() {
try {
final box = HiveService.userDataBox;
return box.keys.cast<String>().where((key) => key != _defaultKey).toList();
} catch (e) {
debugPrint('❌ Error getting all user IDs: $e');
return [];
}
}
/// Delete preferences for a specific user
Future<void> deleteUserPreferences(String userId) async {
try {
final box = HiveService.userDataBox;
await box.delete(userId);
debugPrint('✅ User preferences deleted for user: $userId');
} catch (e) {
debugPrint('❌ Error deleting user preferences: $e');
rethrow;
}
}
/// Get multiple users' preferences
Map<String, UserPreferences> getMultipleUserPreferences(List<String> userIds) {
final result = <String, UserPreferences>{};
for (final userId in userIds) {
final preferences = getUserPreferences(userId);
if (preferences != null) {
result[userId] = preferences;
}
}
return result;
}
/// Validate user preferences
bool validateUserPreferences(UserPreferences preferences) {
try {
// Basic validation
if (preferences.userId.isEmpty) return false;
if (preferences.displayName.isEmpty) return false;
if (preferences.version < 1) return false;
return true;
} catch (e) {
debugPrint('❌ Error validating user preferences: $e');
return false;
}
}
/// Watch user preferences changes
Stream<UserPreferences?> watchUserPreferences([String? userId]) {
try {
final box = HiveService.userDataBox;
final key = userId ?? _defaultKey;
return box.watch(key: key).map((event) => event.value as UserPreferences?);
} catch (e) {
debugPrint('❌ Error watching user preferences: $e');
return Stream.value(null);
}
}
/// Compact user preferences storage
Future<void> compact() async {
try {
final box = HiveService.userDataBox;
await box.compact();
debugPrint('✅ User preferences storage compacted');
} catch (e) {
debugPrint('❌ Error compacting user preferences: $e');
}
}
/// Get storage statistics
Map<String, dynamic> getStorageStats() {
try {
final box = HiveService.userDataBox;
final allUserIds = getAllUserIds();
return {
'totalUsers': allUserIds.length,
'hasDefaultUser': hasUserPreferences(),
'totalEntries': box.length,
'userIds': allUserIds,
};
} catch (e) {
debugPrint('❌ Error getting storage stats: $e');
return {};
}
}
}

View File

@@ -0,0 +1,3 @@
// Barrel export file for errors
export 'exceptions.dart';
export 'failures.dart';

View File

@@ -0,0 +1,79 @@
/// Base exception class for the application
abstract class AppException implements Exception {
final String message;
final String? code;
final dynamic originalError;
const AppException(
this.message, {
this.code,
this.originalError,
});
@override
String toString() {
return 'AppException: $message${code != null ? ' (Code: $code)' : ''}';
}
}
/// Network-related exceptions
class NetworkException extends AppException {
const NetworkException(
super.message, {
super.code,
super.originalError,
});
}
/// Cache-related exceptions
class CacheException extends AppException {
const CacheException(
super.message, {
super.code,
super.originalError,
});
}
/// Validation-related exceptions
class ValidationException extends AppException {
const ValidationException(
super.message, {
super.code,
super.originalError,
});
}
/// Authentication-related exceptions
class AuthException extends AppException {
const AuthException(
super.message, {
super.code,
super.originalError,
});
}
/// Server-related exceptions
class ServerException extends AppException {
final int? statusCode;
const ServerException(
super.message, {
this.statusCode,
super.code,
super.originalError,
});
@override
String toString() {
return 'ServerException: $message${statusCode != null ? ' (Status: $statusCode)' : ''}${code != null ? ' (Code: $code)' : ''}';
}
}
/// Local storage exceptions
class StorageException extends AppException {
const StorageException(
super.message, {
super.code,
super.originalError,
});
}

View File

@@ -0,0 +1,59 @@
import 'package:equatable/equatable.dart';
/// Base failure class for error handling in the application
abstract class Failure extends Equatable {
final String message;
final String? code;
const Failure(this.message, {this.code});
@override
List<Object?> get props => [message, code];
@override
String toString() => 'Failure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Network failure
class NetworkFailure extends Failure {
const NetworkFailure(super.message, {super.code});
}
/// Server failure
class ServerFailure extends Failure {
final int? statusCode;
const ServerFailure(
super.message, {
this.statusCode,
super.code,
});
@override
List<Object?> get props => [message, code, statusCode];
}
/// Cache failure
class CacheFailure extends Failure {
const CacheFailure(super.message, {super.code});
}
/// Validation failure
class ValidationFailure extends Failure {
const ValidationFailure(super.message, {super.code});
}
/// Authentication failure
class AuthFailure extends Failure {
const AuthFailure(super.message, {super.code});
}
/// Storage failure
class StorageFailure extends Failure {
const StorageFailure(super.message, {super.code});
}
/// Unknown failure
class UnknownFailure extends Failure {
const UnknownFailure(super.message, {super.code});
}

501
lib/core/network/README.md Normal file
View File

@@ -0,0 +1,501 @@
# Network Layer Documentation
This network layer provides a comprehensive HTTP client implementation using Dio with advanced features like authentication, retry logic, error handling, and connectivity monitoring.
## Features
-**Configured Dio client** with timeouts and base URL management
-**Authentication interceptor** with automatic token refresh
-**Comprehensive error handling** with domain-specific exceptions
-**Request/response logging** for debugging
-**Automatic retry logic** for failed requests
-**Network connectivity monitoring**
-**Certificate pinning setup** (configurable)
-**File upload/download support**
-**Standardized API response models**
## Architecture
```
lib/core/network/
├── dio_client.dart # Main HTTP client wrapper
├── api_constants.dart # API configuration and endpoints
├── network_info.dart # Connectivity monitoring
├── interceptors/
│ ├── auth_interceptor.dart # Token management and refresh
│ ├── logging_interceptor.dart # Request/response logging
│ └── error_interceptor.dart # Error handling and mapping
├── models/
│ └── api_response.dart # Standardized response models
└── README.md # This documentation
```
## Quick Start
### 1. Setup Providers
```dart
// In your app, use the pre-configured providers
final dioClient = ref.watch(dioClientProvider);
// Or manually create
final dioClient = DioClient(
networkInfo: NetworkInfoImpl(Connectivity()),
secureStorage: FlutterSecureStorage(),
);
```
### 2. Basic HTTP Requests
```dart
// GET request
final response = await dioClient.get('/users/123');
// POST request
final response = await dioClient.post('/posts', data: {
'title': 'My Post',
'content': 'Post content'
});
// PUT request
final response = await dioClient.put('/users/123', data: userData);
// DELETE request
final response = await dioClient.delete('/posts/456');
```
### 3. File Operations
```dart
// Upload file
final response = await dioClient.uploadFile(
'/upload',
File('/path/to/file.jpg'),
filename: 'avatar.jpg',
);
// Download file
await dioClient.downloadFile(
'/files/document.pdf',
'/local/path/document.pdf',
);
```
### 4. Authentication
```dart
// Store tokens after login
await dioClient.authInterceptor.storeTokens(
accessToken: 'your-access-token',
refreshToken: 'your-refresh-token',
expiresIn: 3600, // 1 hour
);
// Check authentication status
final isAuth = await dioClient.authInterceptor.isAuthenticated();
// Logout (clears tokens)
await dioClient.authInterceptor.logout();
```
### 5. Network Connectivity
```dart
// Check current connectivity
final isConnected = await dioClient.isConnected;
// Listen to connectivity changes
dioClient.connectionStream.listen((isConnected) {
if (isConnected) {
print('Connected to internet');
} else {
print('No internet connection');
}
});
```
## Configuration
### API Constants (`api_constants.dart`)
```dart
class ApiConstants {
// Environment URLs
static const String baseUrlDev = 'https://api-dev.example.com';
static const String baseUrlProd = 'https://api.example.com';
// Timeouts
static const int connectTimeout = 30000;
static const int receiveTimeout = 30000;
// Retry configuration
static const int maxRetries = 3;
static const Duration retryDelay = Duration(seconds: 1);
// Endpoints
static const String loginEndpoint = '/auth/login';
static const String userEndpoint = '/user';
}
```
### Environment Switching
```dart
// Update base URL at runtime
dioClient.updateBaseUrl('https://api-staging.example.com');
// Add custom headers
dioClient.addHeader('X-Custom-Header', 'value');
// Remove headers
dioClient.removeHeader('X-Custom-Header');
```
## Error Handling
The network layer provides comprehensive error handling with domain-specific exceptions:
```dart
try {
final response = await dioClient.get('/api/data');
// Handle success
} on DioException catch (e) {
final failure = e.networkFailure;
failure.when(
serverError: (statusCode, message, errors) {
// Handle server errors (5xx)
},
networkError: (message) {
// Handle network connectivity issues
},
timeoutError: (message) {
// Handle timeout errors
},
unauthorizedError: (message) {
// Handle authentication errors (401)
},
forbiddenError: (message) {
// Handle authorization errors (403)
},
notFoundError: (message) {
// Handle not found errors (404)
},
validationError: (message, errors) {
// Handle validation errors (422)
},
unknownError: (message) {
// Handle unknown errors
},
);
}
```
### Error Types
- **ServerError**: HTTP 5xx errors from the server
- **NetworkConnectionError**: Network connectivity issues
- **TimeoutError**: Request/response timeouts
- **UnauthorizedError**: HTTP 401 authentication failures
- **ForbiddenError**: HTTP 403 authorization failures
- **NotFoundError**: HTTP 404 resource not found
- **ValidationError**: HTTP 422 validation failures with field details
- **UnknownError**: Any other unexpected errors
## Authentication Flow
The authentication interceptor automatically handles:
1. **Adding tokens** to requests (Authorization header)
2. **Token refresh** when access token expires
3. **Retry failed requests** after token refresh
4. **Token storage** in secure storage
5. **Automatic logout** when refresh fails
### Token Storage
Tokens are securely stored using `flutter_secure_storage`:
- `access_token`: Current access token
- `refresh_token`: Refresh token for getting new access tokens
- `token_expiry`: Token expiration timestamp
### Automatic Refresh
When a request fails with 401 Unauthorized:
1. Interceptor checks if refresh token exists
2. Makes refresh request to `/auth/refresh`
3. Stores new tokens if successful
4. Retries original request with new token
5. If refresh fails, clears all tokens
## Retry Logic
Requests are automatically retried for:
- **Connection timeouts**
- **Server errors (5xx)**
- **Network connectivity issues**
Configuration:
- Maximum retries: 3
- Delay between retries: Progressive (1s, 2s, 3s)
- Only retries on recoverable errors
## Logging
Request/response logging is automatically handled by the logging interceptor:
### Log Output Example
```
🚀 REQUEST: GET https://api.example.com/api/v1/users/123
📋 Headers: {"Authorization": "***HIDDEN***", "Content-Type": "application/json"}
✅ RESPONSE: GET https://api.example.com/api/v1/users/123 [200] (245ms)
📥 Response Body: {"id": 123, "name": "John Doe"}
```
### Log Features
- **Request/response timing**
- **Sensitive header sanitization** (Authorization, API keys, etc.)
- **Body truncation** for large responses
- **Error stack traces** in debug mode
- **Configurable log levels**
### Controlling Logging
```dart
// Disable logging
dioClient.setLoggingEnabled(false);
// Create client with custom logging
final loggingInterceptor = LoggingInterceptor(
enabled: true,
logRequestBody: true,
logResponseBody: false, // Disable response body logging
maxBodyLength: 1000, // Limit body length
);
```
## Network Monitoring
The network info service provides detailed connectivity information:
```dart
final networkInfo = NetworkInfoImpl(Connectivity());
// Simple connectivity check
final isConnected = await networkInfo.isConnected;
// Detailed connection info
final details = await networkInfo.getConnectionDetails();
print(details.connectionDescription); // "Connected via WiFi"
// Connection type checks
final isWiFi = await networkInfo.isConnectedToWiFi;
final isMobile = await networkInfo.isConnectedToMobile;
```
## API Response Models
### Basic API Response
```dart
class ApiResponse<T> {
final bool success;
final String message;
final T? data;
final List<String>? errors;
final Map<String, dynamic>? meta;
// Factory constructors
factory ApiResponse.success({required T data});
factory ApiResponse.error({required String message});
}
```
### Usage with Services
```dart
class UserService {
Future<User> getUser(String id) async {
final response = await dioClient.get('/users/$id');
return handleApiResponse(
response,
(data) => User.fromJson(data),
);
}
T handleApiResponse<T>(Response response, T Function(dynamic) fromJson) {
if (response.statusCode == 200) {
return fromJson(response.data);
}
throw Exception('Request failed');
}
}
```
## Best Practices
### 1. Use Providers for Dependency Injection
```dart
final userServiceProvider = Provider((ref) {
final dioClient = ref.watch(dioClientProvider);
return UserService(dioClient);
});
```
### 2. Create Service Classes
```dart
class UserService extends BaseApiService {
UserService(super.dioClient);
Future<User> getUser(String id) => executeRequest(
() => dioClient.get('/users/$id'),
User.fromJson,
);
}
```
### 3. Handle Errors Gracefully
```dart
try {
final user = await userService.getUser('123');
// Handle success
} catch (e) {
// Show user-friendly error message
showErrorSnackBar(context, e.toString());
}
```
### 4. Use Network Status
```dart
Widget build(BuildContext context) {
final networkStatus = ref.watch(networkConnectivityProvider);
return networkStatus.when(
data: (isConnected) => isConnected
? MainContent()
: OfflineWidget(),
loading: () => LoadingWidget(),
error: (_, __) => ErrorWidget(),
);
}
```
### 5. Configure for Different Environments
```dart
class ApiEnvironment {
static String get baseUrl {
if (kDebugMode) return ApiConstants.baseUrlDev;
if (kProfileMode) return ApiConstants.baseUrlStaging;
return ApiConstants.baseUrlProd;
}
}
```
## Testing
### Mock Network Responses
```dart
class MockDioClient extends DioClient {
@override
Future<Response<T>> get<T>(String path, {options, cancelToken, queryParameters}) async {
// Return mock response
return Response(
data: {'id': 1, 'name': 'Test User'},
statusCode: 200,
requestOptions: RequestOptions(path: path),
);
}
}
```
### Test Network Info
```dart
class MockNetworkInfo implements NetworkInfo {
final bool _isConnected;
MockNetworkInfo({required bool isConnected}) : _isConnected = isConnected;
@override
Future<bool> get isConnected => Future.value(_isConnected);
}
```
## Security Considerations
### 1. Certificate Pinning
```dart
// Enable in production
class ApiConstants {
static const bool enableCertificatePinning = true;
static const List<String> certificateHashes = [
'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
];
}
```
### 2. Secure Token Storage
Tokens are automatically stored in secure storage with platform-specific encryption.
### 3. Request Sanitization
Sensitive headers are automatically sanitized in logs to prevent token leakage.
## Troubleshooting
### Common Issues
1. **Connection Timeouts**
- Increase timeout values in `ApiConstants`
- Check network connectivity
- Verify server availability
2. **Authentication Failures**
- Ensure tokens are correctly stored
- Verify refresh endpoint configuration
- Check token expiration handling
3. **Certificate Errors**
- Disable certificate pinning in development
- Add proper certificate hashes for production
- Check server SSL configuration
### Debug Mode
Enable detailed logging to troubleshoot issues:
```dart
final dioClient = DioClient(
networkInfo: networkInfo,
secureStorage: secureStorage,
);
// Enable detailed logging
dioClient.setLoggingEnabled(true);
```
## Migration Guide
When migrating from basic Dio to this network layer:
1. Replace `Dio()` instances with `DioClient`
2. Update error handling to use `NetworkFailure` types
3. Use providers for dependency injection
4. Migrate to service classes extending `BaseApiService`
5. Update authentication flow to use interceptor methods
This network layer provides a solid foundation for any Flutter app requiring robust HTTP communication with proper error handling, authentication, and network monitoring.

View File

@@ -0,0 +1,78 @@
/// API constants for network configuration
class ApiConstants {
// Private constructor to prevent instantiation
const ApiConstants._();
// Base URLs for different environments
static const String baseUrlDev = 'https://api-dev.example.com';
static const String baseUrlStaging = 'https://api-staging.example.com';
static const String baseUrlProd = 'https://api.example.com';
// Current environment base URL
// In a real app, this would be determined by build configuration
static const String baseUrl = baseUrlDev;
// API versioning
static const String apiVersion = 'v1';
static const String apiPath = '/api/$apiVersion';
// Timeout configurations (in milliseconds)
static const int connectTimeout = 30000; // 30 seconds
static const int receiveTimeout = 30000; // 30 seconds
static const int sendTimeout = 30000; // 30 seconds
// Retry configurations
static const int maxRetries = 3;
static const Duration retryDelay = Duration(seconds: 1);
// Headers
static const String contentType = 'application/json';
static const String accept = 'application/json';
static const String userAgent = 'BaseFlutter/1.0.0';
// Authentication
static const String authHeaderKey = 'Authorization';
static const String bearerPrefix = 'Bearer';
static const String apiKeyHeaderKey = 'X-API-Key';
// Common API endpoints
static const String authEndpoint = '/auth';
static const String loginEndpoint = '$authEndpoint/login';
static const String refreshEndpoint = '$authEndpoint/refresh';
static const String logoutEndpoint = '$authEndpoint/logout';
static const String userEndpoint = '/user';
static const String profileEndpoint = '$userEndpoint/profile';
// Example service endpoints (for demonstration)
static const String todosEndpoint = '/todos';
static const String postsEndpoint = '/posts';
static const String usersEndpoint = '/users';
// Cache configurations
static const Duration cacheMaxAge = Duration(minutes: 5);
static const String cacheControlHeader = 'Cache-Control';
static const String etagHeader = 'ETag';
static const String ifNoneMatchHeader = 'If-None-Match';
// Error codes
static const int unauthorizedCode = 401;
static const int forbiddenCode = 403;
static const int notFoundCode = 404;
static const int internalServerErrorCode = 500;
static const int badGatewayCode = 502;
static const int serviceUnavailableCode = 503;
// Certificate pinning (for production)
static const List<String> certificateHashes = [
// Add SHA256 hashes of your server certificates here
// Example: 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
];
// Development flags
static const bool enableLogging = true;
static const bool enableCertificatePinning = false; // Disabled for development
// API rate limiting
static const int maxRequestsPerMinute = 100;
static const Duration rateLimitWindow = Duration(minutes: 1);
}

View File

@@ -0,0 +1,362 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'api_constants.dart';
import 'interceptors/auth_interceptor.dart';
import 'interceptors/error_interceptor.dart';
import 'interceptors/logging_interceptor.dart';
import 'network_info.dart';
/// Dio HTTP client wrapper with comprehensive configuration
class DioClient {
late final Dio _dio;
final NetworkInfo _networkInfo;
late final AuthInterceptor _authInterceptor;
final LoggingInterceptor _loggingInterceptor;
final ErrorInterceptor _errorInterceptor;
DioClient({
required NetworkInfo networkInfo,
required FlutterSecureStorage secureStorage,
String? baseUrl,
}) : _networkInfo = networkInfo,
_loggingInterceptor = LoggingInterceptor(),
_errorInterceptor = ErrorInterceptor() {
_dio = _createDio(baseUrl ?? ApiConstants.baseUrl);
_authInterceptor = AuthInterceptor(
secureStorage: secureStorage,
dio: _dio,
);
_setupInterceptors();
_configureHttpClient();
}
/// Getter for the underlying Dio instance
Dio get dio => _dio;
/// Create and configure Dio instance
Dio _createDio(String baseUrl) {
final dio = Dio(BaseOptions(
baseUrl: baseUrl + ApiConstants.apiPath,
connectTimeout: const Duration(milliseconds: ApiConstants.connectTimeout),
receiveTimeout: const Duration(milliseconds: ApiConstants.receiveTimeout),
sendTimeout: const Duration(milliseconds: ApiConstants.sendTimeout),
headers: {
'Content-Type': ApiConstants.contentType,
'Accept': ApiConstants.accept,
'User-Agent': ApiConstants.userAgent,
},
responseType: ResponseType.json,
followRedirects: true,
validateStatus: (status) {
// Consider all status codes as valid to handle them in interceptors
return status != null && status < 500;
},
));
return dio;
}
/// Setup interceptors in the correct order
void _setupInterceptors() {
// Add interceptors in order:
// 1. Request logging and preparation
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// Add request start time for duration calculation
options.extra['start_time'] = DateTime.now().millisecondsSinceEpoch;
handler.next(options);
},
),
);
// 2. Authentication (adds tokens to requests)
_dio.interceptors.add(_authInterceptor);
// 3. Retry interceptor for network failures
_dio.interceptors.add(_createRetryInterceptor());
// 4. Logging (logs requests and responses)
_dio.interceptors.add(_loggingInterceptor);
// 5. Error handling (last to catch all errors)
_dio.interceptors.add(_errorInterceptor);
}
/// Configure HTTP client for certificate pinning and other security features
void _configureHttpClient() {
if (_dio.httpClientAdapter is IOHttpClientAdapter) {
final adapter = _dio.httpClientAdapter as IOHttpClientAdapter;
adapter.createHttpClient = () {
final client = HttpClient();
// Configure certificate pinning in production
if (ApiConstants.enableCertificatePinning) {
client.badCertificateCallback = (cert, host, port) {
// Implement certificate pinning logic here
// For now, return false to reject invalid certificates
return false;
};
}
// Configure timeouts
client.connectionTimeout = const Duration(
milliseconds: ApiConstants.connectTimeout,
);
return client;
};
}
}
/// Create retry interceptor for handling network failures
InterceptorsWrapper _createRetryInterceptor() {
return InterceptorsWrapper(
onError: (error, handler) async {
// Only retry on network errors, not server errors
if (_shouldRetry(error)) {
final retryCount = error.requestOptions.extra['retry_count'] as int? ?? 0;
if (retryCount < ApiConstants.maxRetries) {
error.requestOptions.extra['retry_count'] = retryCount + 1;
// Wait before retrying
await Future.delayed(
ApiConstants.retryDelay * (retryCount + 1),
);
// Check network connectivity before retry
final isConnected = await _networkInfo.isConnected;
if (!isConnected) {
handler.next(error);
return;
}
try {
final response = await _dio.fetch(error.requestOptions);
handler.resolve(response);
return;
} catch (e) {
// If retry fails, continue with original error
}
}
}
handler.next(error);
},
);
}
/// Determine if an error should trigger a retry
bool _shouldRetry(DioException error) {
// Retry on network connectivity issues
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.connectionError) {
return true;
}
// Retry on server errors (5xx)
if (error.response?.statusCode != null) {
final statusCode = error.response!.statusCode!;
return statusCode >= 500 && statusCode < 600;
}
return false;
}
// HTTP Methods
/// GET request
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// POST request
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// PUT request
Future<Response<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.put<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// PATCH request
Future<Response<T>> patch<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.patch<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// DELETE request
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.delete<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// Upload file
Future<Response<T>> uploadFile<T>(
String path,
File file, {
String? field,
String? filename,
Map<String, dynamic>? data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
}) async {
final formData = FormData();
// Add file
formData.files.add(MapEntry(
field ?? 'file',
await MultipartFile.fromFile(
file.path,
filename: filename ?? file.path.split('/').last,
),
));
// Add other form fields
if (data != null) {
data.forEach((key, value) {
formData.fields.add(MapEntry(key, value.toString()));
});
}
return await _dio.post<T>(
path,
data: formData,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
);
}
/// Download file
Future<Response> downloadFile(
String urlPath,
String savePath, {
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) async {
return await _dio.download(
urlPath,
savePath,
queryParameters: queryParameters,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
}
// Utility Methods
/// Check network connectivity
Future<bool> get isConnected => _networkInfo.isConnected;
/// Get network connection stream
Stream<bool> get connectionStream => _networkInfo.connectionStream;
/// Update base URL (useful for environment switching)
void updateBaseUrl(String baseUrl) {
_dio.options.baseUrl = baseUrl + ApiConstants.apiPath;
}
/// Add custom header
void addHeader(String key, String value) {
_dio.options.headers[key] = value;
}
/// Remove header
void removeHeader(String key) {
_dio.options.headers.remove(key);
}
/// Clear all custom headers (keeps default headers)
void clearHeaders() {
_dio.options.headers.clear();
_dio.options.headers.addAll({
'Content-Type': ApiConstants.contentType,
'Accept': ApiConstants.accept,
'User-Agent': ApiConstants.userAgent,
});
}
/// Get auth interceptor for token management
AuthInterceptor get authInterceptor => _authInterceptor;
/// Enable/disable logging
void setLoggingEnabled(bool enabled) {
_loggingInterceptor.enabled = enabled;
}
/// Create a CancelToken for request cancellation
CancelToken createCancelToken() => CancelToken();
/// Close the client and clean up resources
void close({bool force = false}) {
_dio.close(force: force);
}
}

View File

@@ -0,0 +1,279 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../api_constants.dart';
/// Interceptor that handles authentication tokens and automatic token refresh
class AuthInterceptor extends Interceptor {
final FlutterSecureStorage _secureStorage;
final Dio _dio;
// Token storage keys
static const String _accessTokenKey = 'access_token';
static const String _refreshTokenKey = 'refresh_token';
static const String _tokenExpiryKey = 'token_expiry';
// Track if we're currently refreshing to prevent multiple refresh attempts
bool _isRefreshing = false;
final List<Completer<void>> _refreshCompleters = [];
AuthInterceptor({
required FlutterSecureStorage secureStorage,
required Dio dio,
}) : _secureStorage = secureStorage,
_dio = dio;
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
try {
// Skip auth for certain endpoints
if (_shouldSkipAuth(options.path)) {
handler.next(options);
return;
}
// Add access token to request
final accessToken = await _getAccessToken();
if (accessToken != null && accessToken.isNotEmpty) {
options.headers[ApiConstants.authHeaderKey] =
'${ApiConstants.bearerPrefix} $accessToken';
}
handler.next(options);
} catch (e) {
handler.reject(
DioException(
requestOptions: options,
error: 'Failed to add authentication token: $e',
type: DioExceptionType.unknown,
),
);
}
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// Only handle 401 unauthorized errors
if (err.response?.statusCode != ApiConstants.unauthorizedCode) {
handler.next(err);
return;
}
// Skip refresh for certain endpoints
if (_shouldSkipAuth(err.requestOptions.path)) {
handler.next(err);
return;
}
try {
// Attempt to refresh token
final refreshed = await _refreshToken();
if (refreshed) {
// Retry the original request with new token
final response = await _retryRequest(err.requestOptions);
handler.resolve(response);
} else {
// Refresh failed, clear tokens and propagate error
await _clearTokens();
handler.next(err);
}
} catch (e) {
// If refresh fails, clear tokens and propagate original error
await _clearTokens();
handler.next(err);
}
}
/// Check if the endpoint should skip authentication
bool _shouldSkipAuth(String path) {
final skipAuthEndpoints = [
ApiConstants.loginEndpoint,
ApiConstants.refreshEndpoint,
// Add other public endpoints here
];
return skipAuthEndpoints.any((endpoint) => path.contains(endpoint));
}
/// Get the stored access token
Future<String?> _getAccessToken() async {
try {
return await _secureStorage.read(key: _accessTokenKey);
} catch (e) {
return null;
}
}
/// Get the stored refresh token
Future<String?> _getRefreshToken() async {
try {
return await _secureStorage.read(key: _refreshTokenKey);
} catch (e) {
return null;
}
}
/// Check if the token is expired
Future<bool> _isTokenExpired() async {
try {
final expiryString = await _secureStorage.read(key: _tokenExpiryKey);
if (expiryString == null) return true;
final expiry = DateTime.parse(expiryString);
return DateTime.now().isAfter(expiry);
} catch (e) {
return true;
}
}
/// Refresh the access token using the refresh token
Future<bool> _refreshToken() async {
// If already refreshing, wait for it to complete
if (_isRefreshing) {
final completer = Completer<void>();
_refreshCompleters.add(completer);
await completer.future;
return await _getAccessToken() != null;
}
_isRefreshing = true;
try {
final refreshToken = await _getRefreshToken();
if (refreshToken == null || refreshToken.isEmpty) {
return false;
}
// Make refresh request
final response = await _dio.post(
ApiConstants.refreshEndpoint,
data: {'refresh_token': refreshToken},
options: Options(
headers: {
ApiConstants.contentType: ApiConstants.contentType,
},
),
);
if (response.statusCode == 200 && response.data != null) {
final data = response.data as Map<String, dynamic>;
// Store new tokens
await _storeTokens(
accessToken: data['access_token'] as String,
refreshToken: data['refresh_token'] as String?,
expiresIn: data['expires_in'] as int?,
);
return true;
}
return false;
} catch (e) {
return false;
} finally {
_isRefreshing = false;
// Complete all waiting requests
for (final completer in _refreshCompleters) {
if (!completer.isCompleted) {
completer.complete();
}
}
_refreshCompleters.clear();
}
}
/// Retry the original request with the new token
Future<Response> _retryRequest(RequestOptions requestOptions) async {
// Add the new access token
final accessToken = await _getAccessToken();
if (accessToken != null) {
requestOptions.headers[ApiConstants.authHeaderKey] =
'${ApiConstants.bearerPrefix} $accessToken';
}
// Retry the request
return await _dio.fetch(requestOptions);
}
/// Store authentication tokens securely
Future<void> storeTokens({
required String accessToken,
String? refreshToken,
int? expiresIn,
}) async {
await _storeTokens(
accessToken: accessToken,
refreshToken: refreshToken,
expiresIn: expiresIn,
);
}
Future<void> _storeTokens({
required String accessToken,
String? refreshToken,
int? expiresIn,
}) async {
try {
// Store access token
await _secureStorage.write(key: _accessTokenKey, value: accessToken);
// Store refresh token if provided
if (refreshToken != null) {
await _secureStorage.write(key: _refreshTokenKey, value: refreshToken);
}
// Calculate and store expiry time
if (expiresIn != null) {
final expiry = DateTime.now().add(Duration(seconds: expiresIn));
await _secureStorage.write(
key: _tokenExpiryKey,
value: expiry.toIso8601String(),
);
}
} catch (e) {
throw Exception('Failed to store tokens: $e');
}
}
/// Clear all stored tokens
Future<void> _clearTokens() async {
try {
await _secureStorage.delete(key: _accessTokenKey);
await _secureStorage.delete(key: _refreshTokenKey);
await _secureStorage.delete(key: _tokenExpiryKey);
} catch (e) {
// Log error but don't throw
}
}
/// Check if user is authenticated
Future<bool> isAuthenticated() async {
final accessToken = await _getAccessToken();
if (accessToken == null || accessToken.isEmpty) {
return false;
}
// Check if token is expired
return !(await _isTokenExpired());
}
/// Logout by clearing all tokens
Future<void> logout() async {
await _clearTokens();
}
/// Get current access token (for debugging or manual API calls)
Future<String?> getCurrentAccessToken() async {
return await _getAccessToken();
}
/// Get current refresh token (for debugging)
Future<String?> getCurrentRefreshToken() async {
return await _getRefreshToken();
}
}

View File

@@ -0,0 +1,348 @@
import 'dart:io';
import 'package:dio/dio.dart';
import '../api_constants.dart';
import '../models/api_response.dart';
/// Interceptor that handles and transforms network errors into domain-specific exceptions
class ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final networkFailure = _mapDioExceptionToNetworkFailure(err);
// Create a new DioException with our custom error
final customError = DioException(
requestOptions: err.requestOptions,
response: err.response,
error: networkFailure,
type: err.type,
message: networkFailure.message,
stackTrace: err.stackTrace,
);
handler.next(customError);
}
NetworkFailure _mapDioExceptionToNetworkFailure(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return TimeoutError(
message: _getTimeoutMessage(error.type),
);
case DioExceptionType.badResponse:
return _handleBadResponse(error);
case DioExceptionType.cancel:
return const NetworkConnectionError(
message: 'Request was cancelled',
);
case DioExceptionType.connectionError:
return _handleConnectionError(error);
case DioExceptionType.badCertificate:
return const NetworkConnectionError(
message: 'Certificate verification failed. Please check your connection security.',
);
case DioExceptionType.unknown:
return _handleUnknownError(error);
}
}
NetworkFailure _handleBadResponse(DioException error) {
final statusCode = error.response?.statusCode;
final responseData = error.response?.data;
switch (statusCode) {
case ApiConstants.unauthorizedCode:
return UnauthorizedError(
message: _extractErrorMessage(responseData) ??
'Authentication failed. Please log in again.',
);
case ApiConstants.forbiddenCode:
return ForbiddenError(
message: _extractErrorMessage(responseData) ??
'Access denied. You don\'t have permission to access this resource.',
);
case ApiConstants.notFoundCode:
return NotFoundError(
message: _extractErrorMessage(responseData) ??
'The requested resource was not found.',
);
case 422: // Validation error
final errors = _extractValidationErrors(responseData);
return ValidationError(
message: _extractErrorMessage(responseData) ??
'Validation failed. Please check your input.',
errors: errors,
);
case ApiConstants.internalServerErrorCode:
case ApiConstants.badGatewayCode:
case ApiConstants.serviceUnavailableCode:
return ServerError(
statusCode: statusCode!,
message: _extractErrorMessage(responseData) ??
'Server error occurred. Please try again later.',
);
default:
return ServerError(
statusCode: statusCode ?? 0,
message: _extractErrorMessage(responseData) ??
'An unexpected server error occurred.',
);
}
}
NetworkFailure _handleConnectionError(DioException error) {
// Check for specific connection error types
final originalError = error.error;
if (originalError is SocketException) {
return _handleSocketException(originalError);
}
if (originalError is HttpException) {
return NetworkConnectionError(
message: 'HTTP error: ${originalError.message}',
);
}
return const NetworkConnectionError(
message: 'Connection failed. Please check your internet connection and try again.',
);
}
NetworkFailure _handleSocketException(SocketException socketException) {
final message = socketException.message.toLowerCase();
if (message.contains('network is unreachable') ||
message.contains('no route to host')) {
return const NetworkConnectionError(
message: 'Network is unreachable. Please check your internet connection.',
);
}
if (message.contains('connection refused') ||
message.contains('connection failed')) {
return const NetworkConnectionError(
message: 'Unable to connect to server. Please try again later.',
);
}
if (message.contains('host lookup failed') ||
message.contains('nodename nor servname provided')) {
return const NetworkConnectionError(
message: 'Server not found. Please check your connection and try again.',
);
}
return NetworkConnectionError(
message: 'Connection error: ${socketException.message}',
);
}
NetworkFailure _handleUnknownError(DioException error) {
final originalError = error.error;
if (originalError is FormatException) {
return const UnknownError(
message: 'Invalid response format received from server.',
);
}
if (originalError is TypeError) {
return const UnknownError(
message: 'Data parsing error occurred.',
);
}
return UnknownError(
message: originalError?.toString() ?? 'An unexpected error occurred.',
);
}
String _getTimeoutMessage(DioExceptionType type) {
switch (type) {
case DioExceptionType.connectionTimeout:
return 'Connection timeout. Please check your internet connection and try again.';
case DioExceptionType.sendTimeout:
return 'Send timeout. Request took too long to send.';
case DioExceptionType.receiveTimeout:
return 'Receive timeout. Server took too long to respond.';
default:
return 'Request timeout. Please try again.';
}
}
String? _extractErrorMessage(dynamic responseData) {
if (responseData == null) return null;
try {
// Handle different response formats
if (responseData is Map<String, dynamic>) {
// Try common error message fields
final messageFields = ['message', 'error', 'detail', 'error_description'];
for (final field in messageFields) {
if (responseData.containsKey(field) && responseData[field] != null) {
return responseData[field].toString();
}
}
// Try to extract from nested error object
if (responseData.containsKey('error') && responseData['error'] is Map) {
final errorObj = responseData['error'] as Map<String, dynamic>;
for (final field in messageFields) {
if (errorObj.containsKey(field) && errorObj[field] != null) {
return errorObj[field].toString();
}
}
}
// Try to extract from errors array
if (responseData.containsKey('errors') && responseData['errors'] is List) {
final errors = responseData['errors'] as List;
if (errors.isNotEmpty) {
final firstError = errors.first;
if (firstError is Map<String, dynamic> && firstError.containsKey('message')) {
return firstError['message'].toString();
} else if (firstError is String) {
return firstError;
}
}
}
}
// If it's a string, return it directly
if (responseData is String) {
return responseData;
}
return null;
} catch (e) {
return null;
}
}
List<ApiError> _extractValidationErrors(dynamic responseData) {
final errors = <ApiError>[];
if (responseData == null) return errors;
try {
if (responseData is Map<String, dynamic>) {
// Handle Laravel-style validation errors
if (responseData.containsKey('errors') && responseData['errors'] is Map) {
final errorsMap = responseData['errors'] as Map<String, dynamic>;
errorsMap.forEach((field, messages) {
if (messages is List) {
for (final message in messages) {
errors.add(ApiError(
code: 'validation_error',
message: message.toString(),
field: field,
));
}
} else if (messages is String) {
errors.add(ApiError(
code: 'validation_error',
message: messages,
field: field,
));
}
});
}
// Handle array of error objects
if (responseData.containsKey('errors') && responseData['errors'] is List) {
final errorsList = responseData['errors'] as List;
for (final error in errorsList) {
if (error is Map<String, dynamic>) {
errors.add(ApiError(
code: error['code']?.toString() ?? 'validation_error',
message: error['message']?.toString() ?? 'Validation error',
field: error['field']?.toString(),
details: error['details'] as Map<String, dynamic>?,
));
} else if (error is String) {
errors.add(ApiError(
code: 'validation_error',
message: error,
));
}
}
}
}
} catch (e) {
// If parsing fails, add a generic validation error
errors.add(const ApiError(
code: 'validation_error',
message: 'Validation failed',
));
}
return errors;
}
}
/// Extension to get NetworkFailure from DioException
extension DioExceptionExtension on DioException {
NetworkFailure get networkFailure {
if (error is NetworkFailure) {
return error as NetworkFailure;
}
// Fallback mapping if not processed by interceptor
return const UnknownError(
message: 'An unexpected error occurred',
);
}
}
/// Helper extension to check error types
extension NetworkFailureExtension on NetworkFailure {
bool get isNetworkError => when(
serverError: (_, __, ___) => false,
networkError: (_) => true,
timeoutError: (_) => true,
unauthorizedError: (_) => false,
forbiddenError: (_) => false,
notFoundError: (_) => false,
validationError: (_, __) => false,
unknownError: (_) => false,
);
bool get isServerError => when(
serverError: (_, __, ___) => true,
networkError: (_) => false,
timeoutError: (_) => false,
unauthorizedError: (_) => false,
forbiddenError: (_) => false,
notFoundError: (_) => false,
validationError: (_, __) => false,
unknownError: (_) => false,
);
bool get isAuthError => when(
serverError: (_, __, ___) => false,
networkError: (_) => false,
timeoutError: (_) => false,
unauthorizedError: (_) => true,
forbiddenError: (_) => true,
notFoundError: (_) => false,
validationError: (_, __) => false,
unknownError: (_) => false,
);
}

View File

@@ -0,0 +1,281 @@
import 'dart:convert';
import 'dart:developer' as developer;
import 'package:dio/dio.dart';
import '../api_constants.dart';
/// Custom logging interceptor for detailed request/response logging
class LoggingInterceptor extends Interceptor {
bool enabled;
final bool logRequestBody;
final bool logResponseBody;
final bool logHeaders;
final int maxBodyLength;
LoggingInterceptor({
this.enabled = ApiConstants.enableLogging,
this.logRequestBody = true,
this.logResponseBody = true,
this.logHeaders = true,
this.maxBodyLength = 2000,
});
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (enabled) {
_logRequest(options);
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (enabled) {
_logResponse(response);
}
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (enabled) {
_logError(err);
}
handler.next(err);
}
void _logRequest(RequestOptions options) {
final uri = options.uri;
final method = options.method.toUpperCase();
developer.log(
'🚀 REQUEST: $method $uri',
name: 'HTTP_REQUEST',
);
// Log headers
if (logHeaders && options.headers.isNotEmpty) {
final headers = _sanitizeHeaders(options.headers);
developer.log(
'📋 Headers: ${_formatJson(headers)}',
name: 'HTTP_REQUEST',
);
}
// Log query parameters
if (options.queryParameters.isNotEmpty) {
developer.log(
'🔍 Query Parameters: ${_formatJson(options.queryParameters)}',
name: 'HTTP_REQUEST',
);
}
// Log request body
if (logRequestBody && options.data != null) {
final body = _formatRequestBody(options.data);
if (body.isNotEmpty) {
developer.log(
'📝 Request Body: $body',
name: 'HTTP_REQUEST',
);
}
}
}
void _logResponse(Response response) {
final statusCode = response.statusCode;
final method = response.requestOptions.method.toUpperCase();
final uri = response.requestOptions.uri;
final duration = DateTime.now().millisecondsSinceEpoch -
(response.requestOptions.extra['start_time'] as int? ?? 0);
// Status icon based on response code
String statusIcon;
if (statusCode != null && statusCode != 0) {
if (statusCode >= 200 && statusCode < 300) {
statusIcon = '';
} else if (statusCode >= 300 && statusCode < 400) {
statusIcon = '↩️';
} else if (statusCode >= 400 && statusCode < 500) {
statusIcon = '';
} else {
statusIcon = '💥';
}
} else {
statusIcon = '';
}
developer.log(
'$statusIcon RESPONSE: $method $uri [$statusCode] (${duration}ms)',
name: 'HTTP_RESPONSE',
);
// Log response headers
if (logHeaders && response.headers.map.isNotEmpty) {
final headers = _sanitizeHeaders(response.headers.map);
developer.log(
'📋 Response Headers: ${_formatJson(headers)}',
name: 'HTTP_RESPONSE',
);
}
// Log response body
if (logResponseBody && response.data != null) {
final body = _formatResponseBody(response.data);
if (body.isNotEmpty) {
developer.log(
'📥 Response Body: $body',
name: 'HTTP_RESPONSE',
);
}
}
}
void _logError(DioException error) {
final method = error.requestOptions.method.toUpperCase();
final uri = error.requestOptions.uri;
final statusCode = error.response?.statusCode;
developer.log(
'💥 ERROR: $method $uri [${statusCode ?? 'NO_STATUS'}] - ${error.type.name}',
name: 'HTTP_ERROR',
error: error,
);
// Log error message
if (error.message != null) {
developer.log(
'❗ Error Message: ${error.message}',
name: 'HTTP_ERROR',
);
}
// Log error response if available
if (error.response?.data != null) {
final errorBody = _formatResponseBody(error.response!.data);
if (errorBody.isNotEmpty) {
developer.log(
'📥 Error Response: $errorBody',
name: 'HTTP_ERROR',
);
}
}
// Log stack trace in debug mode
if (error.stackTrace.toString().isNotEmpty) {
developer.log(
'🔍 Stack Trace: ${error.stackTrace}',
name: 'HTTP_ERROR',
);
}
}
String _formatRequestBody(dynamic data) {
if (data == null) return '';
try {
String body;
if (data is Map || data is List) {
body = _formatJson(data);
} else if (data is FormData) {
body = _formatFormData(data);
} else {
body = data.toString();
}
return _truncateIfNeeded(body);
} catch (e) {
return 'Failed to format request body: $e';
}
}
String _formatResponseBody(dynamic data) {
if (data == null) return '';
try {
String body;
if (data is Map || data is List) {
body = _formatJson(data);
} else {
body = data.toString();
}
return _truncateIfNeeded(body);
} catch (e) {
return 'Failed to format response body: $e';
}
}
String _formatJson(dynamic data) {
try {
const encoder = JsonEncoder.withIndent(' ');
return encoder.convert(data);
} catch (e) {
return data.toString();
}
}
String _formatFormData(FormData formData) {
final buffer = StringBuffer('FormData{\n');
for (final field in formData.fields) {
buffer.writeln(' ${field.key}: ${field.value}');
}
for (final file in formData.files) {
buffer.writeln(' ${file.key}: ${file.value.filename} (${file.value.length} bytes)');
}
buffer.write('}');
return buffer.toString();
}
Map<String, dynamic> _sanitizeHeaders(Map<String, dynamic> headers) {
final sanitized = <String, dynamic>{};
headers.forEach((key, value) {
final lowerKey = key.toLowerCase();
// Sanitize sensitive headers
if (_isSensitiveHeader(lowerKey)) {
sanitized[key] = '***HIDDEN***';
} else {
sanitized[key] = value;
}
});
return sanitized;
}
bool _isSensitiveHeader(String headerName) {
const sensitiveHeaders = [
'authorization',
'cookie',
'set-cookie',
'x-api-key',
'x-auth-token',
'x-access-token',
'x-refresh-token',
];
return sensitiveHeaders.contains(headerName);
}
String _truncateIfNeeded(String text) {
if (text.length <= maxBodyLength) {
return text;
}
return '${text.substring(0, maxBodyLength)}... (truncated ${text.length - maxBodyLength} characters)';
}
}
/// Extension to add start time to request options for duration calculation
extension RequestOptionsExtension on RequestOptions {
void markStartTime() {
extra['start_time'] = DateTime.now().millisecondsSinceEpoch;
}
}

View File

@@ -0,0 +1,333 @@
/// Simple API response wrapper that standardizes all API responses
class ApiResponse<T> {
final bool success;
final String message;
final T? data;
final List<String>? errors;
final Map<String, dynamic>? meta;
final int? statusCode;
final String? timestamp;
const ApiResponse({
required this.success,
required this.message,
this.data,
this.errors,
this.meta,
this.statusCode,
this.timestamp,
});
/// Factory constructor for successful responses
factory ApiResponse.success({
required T data,
String message = 'Success',
Map<String, dynamic>? meta,
}) {
return ApiResponse(
success: true,
message: message,
data: data,
meta: meta,
statusCode: 200,
timestamp: DateTime.now().toIso8601String(),
);
}
/// Factory constructor for error responses
factory ApiResponse.error({
required String message,
List<String>? errors,
int? statusCode,
Map<String, dynamic>? meta,
}) {
return ApiResponse(
success: false,
message: message,
errors: errors,
statusCode: statusCode,
meta: meta,
timestamp: DateTime.now().toIso8601String(),
);
}
/// Create from JSON
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic)? fromJsonT,
) {
return ApiResponse(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: json['data'] != null && fromJsonT != null ? fromJsonT(json['data']) : null,
errors: (json['errors'] as List<dynamic>?)?.cast<String>(),
meta: json['meta'] as Map<String, dynamic>?,
statusCode: json['status_code'] as int?,
timestamp: json['timestamp'] as String?,
);
}
/// Convert to JSON
Map<String, dynamic> toJson([dynamic Function(T)? toJsonT]) {
return {
'success': success,
'message': message,
'data': data != null && toJsonT != null ? toJsonT(data as T) : data,
if (errors != null) 'errors': errors,
if (meta != null) 'meta': meta,
if (statusCode != null) 'status_code': statusCode,
if (timestamp != null) 'timestamp': timestamp,
};
}
@override
String toString() {
return 'ApiResponse(success: $success, message: $message, data: $data)';
}
}
/// Pagination metadata for paginated API responses
class PaginationMeta {
final int currentPage;
final int perPage;
final int total;
final int totalPages;
final bool hasNextPage;
final bool hasPreviousPage;
const PaginationMeta({
required this.currentPage,
required this.perPage,
required this.total,
required this.totalPages,
required this.hasNextPage,
required this.hasPreviousPage,
});
factory PaginationMeta.fromJson(Map<String, dynamic> json) {
return PaginationMeta(
currentPage: json['current_page'] ?? 0,
perPage: json['per_page'] ?? 0,
total: json['total'] ?? 0,
totalPages: json['total_pages'] ?? 0,
hasNextPage: json['has_next_page'] ?? false,
hasPreviousPage: json['has_previous_page'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'current_page': currentPage,
'per_page': perPage,
'total': total,
'total_pages': totalPages,
'has_next_page': hasNextPage,
'has_previous_page': hasPreviousPage,
};
}
}
/// Paginated API response wrapper
class PaginatedApiResponse<T> {
final bool success;
final String message;
final List<T> data;
final PaginationMeta pagination;
final List<String>? errors;
final int? statusCode;
final String? timestamp;
const PaginatedApiResponse({
required this.success,
required this.message,
required this.data,
required this.pagination,
this.errors,
this.statusCode,
this.timestamp,
});
factory PaginatedApiResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic) fromJsonT,
) {
return PaginatedApiResponse(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: (json['data'] as List<dynamic>?)?.map(fromJsonT).toList() ?? [],
pagination: PaginationMeta.fromJson(json['pagination'] ?? {}),
errors: (json['errors'] as List<dynamic>?)?.cast<String>(),
statusCode: json['status_code'] as int?,
timestamp: json['timestamp'] as String?,
);
}
}
/// API error details for more specific error handling
class ApiError {
final String code;
final String message;
final String? field;
final Map<String, dynamic>? details;
const ApiError({
required this.code,
required this.message,
this.field,
this.details,
});
factory ApiError.fromJson(Map<String, dynamic> json) {
return ApiError(
code: json['code'] ?? '',
message: json['message'] ?? '',
field: json['field'] as String?,
details: json['details'] as Map<String, dynamic>?,
);
}
Map<String, dynamic> toJson() {
return {
'code': code,
'message': message,
if (field != null) 'field': field,
if (details != null) 'details': details,
};
}
@override
String toString() => 'ApiError(code: $code, message: $message, field: $field)';
}
/// Network response wrapper that includes both success and error cases
abstract class NetworkResponse<T> {
const NetworkResponse();
}
class NetworkSuccess<T> extends NetworkResponse<T> {
final T data;
const NetworkSuccess(this.data);
@override
String toString() => 'NetworkSuccess(data: $data)';
}
class NetworkError<T> extends NetworkResponse<T> {
final NetworkFailure failure;
const NetworkError(this.failure);
@override
String toString() => 'NetworkError(failure: $failure)';
}
/// Network failure types
abstract class NetworkFailure {
final String message;
const NetworkFailure({required this.message});
/// Pattern matching helper
T when<T>({
required T Function(int statusCode, String message, List<ApiError>? errors) serverError,
required T Function(String message) networkError,
required T Function(String message) timeoutError,
required T Function(String message) unauthorizedError,
required T Function(String message) forbiddenError,
required T Function(String message) notFoundError,
required T Function(String message, List<ApiError> errors) validationError,
required T Function(String message) unknownError,
}) {
if (this is ServerError) {
final error = this as ServerError;
return serverError(error.statusCode, error.message, error.errors);
} else if (this is NetworkConnectionError) {
return networkError(message);
} else if (this is TimeoutError) {
return timeoutError(message);
} else if (this is UnauthorizedError) {
return unauthorizedError(message);
} else if (this is ForbiddenError) {
return forbiddenError(message);
} else if (this is NotFoundError) {
return notFoundError(message);
} else if (this is ValidationError) {
final error = this as ValidationError;
return validationError(error.message, error.errors);
} else {
return unknownError(message);
}
}
@override
String toString() => 'NetworkFailure(message: $message)';
}
class ServerError extends NetworkFailure {
final int statusCode;
final List<ApiError>? errors;
const ServerError({
required this.statusCode,
required String message,
this.errors,
}) : super(message: message);
@override
String toString() => 'ServerError(statusCode: $statusCode, message: $message)';
}
class NetworkConnectionError extends NetworkFailure {
const NetworkConnectionError({required String message}) : super(message: message);
@override
String toString() => 'NetworkConnectionError(message: $message)';
}
class TimeoutError extends NetworkFailure {
const TimeoutError({required String message}) : super(message: message);
@override
String toString() => 'TimeoutError(message: $message)';
}
class UnauthorizedError extends NetworkFailure {
const UnauthorizedError({required String message}) : super(message: message);
@override
String toString() => 'UnauthorizedError(message: $message)';
}
class ForbiddenError extends NetworkFailure {
const ForbiddenError({required String message}) : super(message: message);
@override
String toString() => 'ForbiddenError(message: $message)';
}
class NotFoundError extends NetworkFailure {
const NotFoundError({required String message}) : super(message: message);
@override
String toString() => 'NotFoundError(message: $message)';
}
class ValidationError extends NetworkFailure {
final List<ApiError> errors;
const ValidationError({
required String message,
required this.errors,
}) : super(message: message);
@override
String toString() => 'ValidationError(message: $message, errors: $errors)';
}
class UnknownError extends NetworkFailure {
const UnknownError({required String message}) : super(message: message);
@override
String toString() => 'UnknownError(message: $message)';
}

View File

@@ -0,0 +1,233 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
/// Abstract class defining network information interface
abstract class NetworkInfo {
Future<bool> get isConnected;
Stream<bool> get connectionStream;
Future<ConnectivityResult> get connectionStatus;
Future<bool> hasInternetConnection();
}
/// Implementation of NetworkInfo using connectivity_plus package
class NetworkInfoImpl implements NetworkInfo {
final Connectivity _connectivity;
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
final StreamController<bool> _connectionController = StreamController<bool>.broadcast();
NetworkInfoImpl(this._connectivity) {
_initializeConnectivityStream();
}
void _initializeConnectivityStream() {
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(
(List<ConnectivityResult> results) {
_updateConnectionStatus(results);
},
);
// Check initial connectivity status
_checkInitialConnectivity();
}
Future<void> _checkInitialConnectivity() async {
try {
final results = await _connectivity.checkConnectivity();
_updateConnectionStatus(results);
} catch (e) {
_connectionController.add(false);
}
}
void _updateConnectionStatus(List<ConnectivityResult> results) async {
final hasConnection = _hasConnectionFromResults(results);
// Double-check with internet connectivity test for reliability
if (hasConnection) {
final hasInternet = await hasInternetConnection();
_connectionController.add(hasInternet);
} else {
_connectionController.add(false);
}
}
bool _hasConnectionFromResults(List<ConnectivityResult> results) {
return results.any((result) =>
result != ConnectivityResult.none);
}
@override
Future<bool> get isConnected async {
try {
final results = await _connectivity.checkConnectivity();
if (!_hasConnectionFromResults(results)) {
return false;
}
return await hasInternetConnection();
} catch (e) {
return false;
}
}
@override
Stream<bool> get connectionStream => _connectionController.stream;
@override
Future<ConnectivityResult> get connectionStatus async {
try {
final results = await _connectivity.checkConnectivity();
// Return the first non-none result, or none if all are none
return results.firstWhere(
(result) => result != ConnectivityResult.none,
orElse: () => ConnectivityResult.none,
);
} catch (e) {
return ConnectivityResult.none;
}
}
@override
Future<bool> hasInternetConnection() async {
try {
// Try to connect to multiple reliable hosts for better reliability
final hosts = [
'google.com',
'cloudflare.com',
'8.8.8.8', // Google DNS
];
for (final host in hosts) {
try {
final result = await InternetAddress.lookup(host).timeout(
const Duration(seconds: 5),
);
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
return true;
}
} catch (e) {
// Continue to next host if this one fails
continue;
}
}
return false;
} catch (e) {
return false;
}
}
/// Get detailed connectivity information
Future<NetworkConnectionDetails> getConnectionDetails() async {
try {
final results = await _connectivity.checkConnectivity();
final hasInternet = await hasInternetConnection();
return NetworkConnectionDetails(
connectivityResults: results,
hasInternetConnection: hasInternet,
timestamp: DateTime.now(),
);
} catch (e) {
return NetworkConnectionDetails(
connectivityResults: [ConnectivityResult.none],
hasInternetConnection: false,
timestamp: DateTime.now(),
error: e.toString(),
);
}
}
/// Check if connected to WiFi
Future<bool> get isConnectedToWiFi async {
final results = await _connectivity.checkConnectivity();
return results.contains(ConnectivityResult.wifi);
}
/// Check if connected to mobile data
Future<bool> get isConnectedToMobile async {
final results = await _connectivity.checkConnectivity();
return results.contains(ConnectivityResult.mobile);
}
/// Check if connected to ethernet (mainly for desktop/web)
Future<bool> get isConnectedToEthernet async {
final results = await _connectivity.checkConnectivity();
return results.contains(ConnectivityResult.ethernet);
}
/// Dispose of resources
void dispose() {
_connectivitySubscription?.cancel();
_connectionController.close();
}
}
/// Detailed network connection information
class NetworkConnectionDetails {
final List<ConnectivityResult> connectivityResults;
final bool hasInternetConnection;
final DateTime timestamp;
final String? error;
const NetworkConnectionDetails({
required this.connectivityResults,
required this.hasInternetConnection,
required this.timestamp,
this.error,
});
/// Check if any connection is available
bool get hasConnection =>
connectivityResults.any((result) => result != ConnectivityResult.none);
/// Get primary connection type
ConnectivityResult get primaryConnection {
return connectivityResults.firstWhere(
(result) => result != ConnectivityResult.none,
orElse: () => ConnectivityResult.none,
);
}
/// Check if connected via WiFi
bool get isWiFi => connectivityResults.contains(ConnectivityResult.wifi);
/// Check if connected via mobile
bool get isMobile => connectivityResults.contains(ConnectivityResult.mobile);
/// Check if connected via ethernet
bool get isEthernet => connectivityResults.contains(ConnectivityResult.ethernet);
/// Get human-readable connection description
String get connectionDescription {
if (error != null) {
return 'Connection error: $error';
}
if (!hasConnection) {
return 'No connection';
}
if (!hasInternetConnection) {
return 'Connected but no internet access';
}
final types = <String>[];
if (isWiFi) types.add('WiFi');
if (isMobile) types.add('Mobile');
if (isEthernet) types.add('Ethernet');
return types.isEmpty ? 'Connected' : 'Connected via ${types.join(', ')}';
}
@override
String toString() {
return 'NetworkConnectionDetails('
'results: $connectivityResults, '
'hasInternet: $hasInternetConnection, '
'timestamp: $timestamp, '
'error: $error)';
}
}

View File

@@ -0,0 +1,360 @@
# Riverpod State Management Setup
This directory contains a comprehensive Riverpod state management setup following Riverpod 2.x best practices with modern provider patterns.
## 📁 Directory Structure
```
lib/core/providers/
├── providers.dart # Barrel file - single import point
├── app_providers.dart # Global app state and initialization
├── theme_providers.dart # Theme and UI state management
├── storage_providers.dart # Secure storage and Hive management
├── network_providers.dart # HTTP clients and API providers
├── api_providers.dart # API-specific providers
└── provider_usage_example.dart # Usage examples and patterns
lib/shared/presentation/providers/
└── connectivity_providers.dart # Network connectivity monitoring
```
## 🚀 Key Features
### Modern Riverpod 2.x Patterns
- **AsyncNotifierProvider**: For async mutable state management
- **NotifierProvider**: For synchronous mutable state management
- **StreamProvider**: For reactive data streams
- **Provider**: For dependency injection and immutable values
- **Code Generation**: Using `@riverpod` annotation for type safety
### Comprehensive State Management
- **App Initialization**: Multi-stage app startup with error handling
- **Theme Management**: Dark/light mode with system preference support
- **Storage Integration**: Hive + Secure Storage with health monitoring
- **Network Connectivity**: Real-time connection status and history
- **Feature Flags**: Dynamic feature toggling
- **Error Tracking**: Centralized error logging and monitoring
### Performance Optimized
- **State Persistence**: Automatic state saving and restoration
- **Efficient Rebuilds**: Minimal widget rebuilds with proper selectors
- **Resource Management**: Automatic disposal and cleanup
- **Caching Strategy**: Intelligent data caching with expiration
## 📋 Provider Categories
### 🏗️ Core Application (`app_providers.dart`)
```dart
// App initialization with multi-stage loading
final initData = ref.watch(appInitializationProvider);
// Global app state management
final globalState = ref.watch(globalAppStateProvider);
// Feature flags for conditional features
final featureFlags = ref.watch(featureFlagsProvider);
```
### 🎨 Theme & UI (`theme_providers.dart`)
```dart
// Theme mode management with persistence
final themeMode = ref.watch(currentThemeModeProvider);
await ref.read(appSettingsNotifierProvider.notifier)
.updateThemeMode(AppThemeMode.dark);
// Reactive theme changes
final isDark = ref.watch(isDarkModeProvider);
```
### 💾 Storage Management (`storage_providers.dart`)
```dart
// Secure storage operations
final secureNotifier = ref.read(secureStorageNotifierProvider.notifier);
await secureNotifier.store('token', 'secure_value');
// Hive storage with health monitoring
final storageHealth = ref.watch(storageHealthMonitorProvider);
```
### 🌐 Network Connectivity (`connectivity_providers.dart`)
```dart
// Real-time connectivity monitoring
final isConnected = ref.watch(isConnectedProvider);
final connectionType = ref.watch(connectionTypeProvider);
// Network history and statistics
final networkHistory = ref.watch(networkHistoryNotifierProvider);
```
## 🔧 Usage Patterns
### 1. Basic Provider Consumption
```dart
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final settingsAsync = ref.watch(appSettingsNotifierProvider);
return settingsAsync.when(
data: (settings) => Text('Theme: ${settings.themeMode}'),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
);
}
}
```
### 2. State Mutation
```dart
// Theme change
await ref.read(appSettingsNotifierProvider.notifier)
.updateThemeMode(AppThemeMode.dark);
// Feature flag toggle
ref.read(featureFlagsProvider.notifier)
.toggleFeature('darkMode');
// Storage operations
await ref.read(secureStorageNotifierProvider.notifier)
.store('api_key', newApiKey);
```
### 3. Provider Listening
```dart
// Listen for state changes
ref.listen(networkStatusNotifierProvider, (previous, next) {
if (next.isConnected && !previous?.isConnected) {
// Connection restored
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Connection restored')),
);
}
});
```
### 4. Combining Providers
```dart
@riverpod
String userStatus(UserStatusRef ref) {
final isConnected = ref.watch(isConnectedProvider);
final settings = ref.watch(appSettingsNotifierProvider);
return settings.when(
data: (data) => isConnected
? 'Online - ${data.locale}'
: 'Offline - ${data.locale}',
loading: () => 'Loading...',
error: (_, __) => 'Error',
);
}
```
## 📱 App Integration
### 1. Provider Setup
```dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive before app starts
await HiveService.init();
runApp(
ProviderScope(
child: const MyApp(),
),
);
}
```
### 2. App Initialization
```dart
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final initAsync = ref.watch(appInitializationProvider);
return initAsync.when(
data: (data) => data.state == AppInitializationState.initialized
? const MainApp()
: const SplashScreen(),
loading: () => const SplashScreen(),
error: (error, stack) => ErrorApp(error: error),
);
}
}
```
### 3. Theme Integration
```dart
class MainApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(effectiveThemeModeProvider);
return MaterialApp(
themeMode: themeMode,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: const HomeScreen(),
);
}
}
```
## 🔄 State Persistence
### Automatic Persistence
- **Theme Mode**: Automatically saved to Hive
- **User Preferences**: Persisted across app restarts
- **Feature Flags**: Maintained in local storage
- **Network History**: Cached for analytics
### Manual Persistence
```dart
// Save custom app state
await ref.read(appSettingsNotifierProvider.notifier)
.setCustomSetting('user_preference', value);
// Restore state on app start
final customValue = settings.getCustomSetting<String>('user_preference');
```
## 🚨 Error Handling
### Provider-Level Error Handling
```dart
@riverpod
class DataNotifier extends _$DataNotifier {
@override
Future<Data> build() async {
try {
return await loadData();
} catch (error, stackTrace) {
// Log error for debugging
ref.read(errorTrackerProvider.notifier)
.logError(error, stackTrace);
rethrow;
}
}
}
```
### Global Error Tracking
```dart
// Access error history
final recentErrors = ref.read(errorTrackerProvider.notifier)
.getRecentErrors(count: 5);
// Clear error history
ref.read(errorTrackerProvider.notifier).clearErrors();
```
## 🔍 Debugging & Monitoring
### Provider Inspector
```dart
// Check provider state in debug mode
if (kDebugMode) {
final globalState = ref.read(globalAppStateProvider);
debugPrint('App State: $globalState');
}
```
### Storage Health Monitoring
```dart
// Perform health check
await ref.read(storageHealthMonitorProvider.notifier)
.performHealthCheck();
// Check storage statistics
final stats = ref.read(hiveStorageNotifierProvider.notifier)
.getStorageStats();
```
## 🛠️ Best Practices
1. **Import**: Use the barrel file `import '../core/providers/providers.dart'`
2. **Error Handling**: Always handle AsyncValue error states
3. **Disposal**: Providers auto-dispose, but manual cleanup when needed
4. **Performance**: Use `select()` for specific state slices
5. **Testing**: Mock providers using `ProviderContainer`
6. **State Size**: Keep provider state minimal and focused
7. **Provider Names**: Use descriptive names indicating purpose
8. **Documentation**: Document complex provider logic
## 📚 Code Generation
The providers use Riverpod's code generation. Run this command after making changes:
```bash
dart run build_runner build
```
For continuous generation during development:
```bash
dart run build_runner watch
```
## 🧪 Testing
### Provider Testing
```dart
test('theme provider updates correctly', () async {
final container = ProviderContainer();
final notifier = container.read(appSettingsNotifierProvider.notifier);
await notifier.updateThemeMode(AppThemeMode.dark);
final settings = container.read(appSettingsNotifierProvider);
expect(settings.value?.themeMode, 'dark');
container.dispose();
});
```
### Widget Testing with Providers
```dart
testWidgets('widget shows correct theme', (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: MyWidget(),
),
),
);
expect(find.text('Theme: system'), findsOneWidget);
});
```
## 📄 Dependencies
This setup requires the following dependencies in `pubspec.yaml`:
```yaml
dependencies:
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5
connectivity_plus: ^6.0.5
flutter_secure_storage: ^9.2.2
hive: ^2.2.3
hive_flutter: ^1.1.0
dev_dependencies:
riverpod_generator: ^2.4.0
build_runner: ^2.4.7
```
## 🔄 Migration from Provider/Bloc
If migrating from other state management solutions:
1. Replace Provider with Riverpod providers
2. Convert Bloc to AsyncNotifierProvider
3. Update UI to use ConsumerWidget
4. Migrate state persistence logic
5. Update testing to use ProviderContainer
This setup provides a solid foundation for scalable Flutter applications with proper state management, error handling, and performance optimization.

View File

@@ -0,0 +1,34 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/api_service.dart';
import 'network_providers.dart';
/// Provider for ExampleApiService
final exampleApiServiceProvider = Provider<ExampleApiService>((ref) {
final dioClient = ref.watch(dioClientProvider);
return ExampleApiService(dioClient);
});
/// Provider for AuthApiService
final authApiServiceProvider = Provider<AuthApiService>((ref) {
final dioClient = ref.watch(dioClientProvider);
return AuthApiService(dioClient);
});
/// Provider to check authentication status
final isAuthenticatedProvider = FutureProvider<bool>((ref) async {
final authService = ref.watch(authApiServiceProvider);
return await authService.isAuthenticated();
});
/// Example provider for user profile data
final userProfileProvider = FutureProvider.family<Map<String, dynamic>, String>((ref, userId) async {
final apiService = ref.watch(exampleApiServiceProvider);
return await apiService.getUserProfile(userId);
});
/// Example provider for posts list with pagination
final postsProvider = FutureProvider.family<List<Map<String, dynamic>>, ({int page, int limit})>((ref, params) async {
final apiService = ref.watch(exampleApiServiceProvider);
return await apiService.getPosts(page: params.page, limit: params.limit);
});

View File

@@ -0,0 +1,348 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter/foundation.dart';
import '../database/hive_service.dart';
import '../database/models/app_settings.dart';
import '../database/providers/database_providers.dart';
import '../database/repositories/settings_repository.dart';
import '../database/repositories/cache_repository.dart';
import '../database/repositories/user_preferences_repository.dart';
part 'app_providers.g.dart';
/// App initialization state
enum AppInitializationState {
uninitialized,
initializing,
initialized,
error,
}
/// App initialization data
class AppInitializationData {
final AppInitializationState state;
final String? error;
final DateTime? initializedAt;
const AppInitializationData({
required this.state,
this.error,
this.initializedAt,
});
AppInitializationData copyWith({
AppInitializationState? state,
String? error,
DateTime? initializedAt,
}) {
return AppInitializationData(
state: state ?? this.state,
error: error ?? this.error,
initializedAt: initializedAt ?? this.initializedAt,
);
}
}
/// Repository providers
@riverpod
CacheRepository cacheRepository(CacheRepositoryRef ref) {
return CacheRepository();
}
@riverpod
UserPreferencesRepository userPreferencesRepository(UserPreferencesRepositoryRef ref) {
return UserPreferencesRepository();
}
/// App initialization provider
@riverpod
class AppInitialization extends _$AppInitialization {
@override
Future<AppInitializationData> build() async {
return _initializeApp();
}
Future<AppInitializationData> _initializeApp() async {
try {
debugPrint('🚀 Starting app initialization...');
// Initialize Hive
debugPrint('📦 Initializing Hive database...');
await HiveService.initialize();
// Initialize repositories
debugPrint('🗂️ Initializing repositories...');
final settingsRepo = ref.read(settingsRepositoryProvider);
final cacheRepo = ref.read(cacheRepositoryProvider);
final userPrefsRepo = ref.read(userPreferencesRepositoryProvider);
// Load initial settings
debugPrint('⚙️ Loading app settings...');
final settings = settingsRepo.getSettings();
// Initialize user preferences if needed
debugPrint('👤 Initializing user preferences...');
userPrefsRepo.getPreferences();
// Perform any necessary migrations
debugPrint('🔄 Checking for migrations...');
await _performMigrations(settingsRepo, settings);
// Clean up expired cache entries
debugPrint('🧹 Cleaning up expired cache...');
await cacheRepo.cleanExpiredItems();
debugPrint('✅ App initialization completed successfully');
return AppInitializationData(
state: AppInitializationState.initialized,
initializedAt: DateTime.now(),
);
} catch (error, stackTrace) {
debugPrint('❌ App initialization failed: $error');
debugPrint('Stack trace: $stackTrace');
return AppInitializationData(
state: AppInitializationState.error,
error: error.toString(),
);
}
}
Future<void> _performMigrations(SettingsRepository repo, AppSettings settings) async {
// Add any migration logic here
if (settings.version < 1) {
debugPrint('🔄 Migrating settings to version 1...');
// Migration logic would go here
}
}
/// Retry initialization
Future<void> retry() async {
state = const AsyncValue.loading();
state = AsyncValue.data(await _initializeApp());
}
/// Force re-initialization
Future<void> reinitialize() async {
state = const AsyncValue.loading();
try {
// Close all Hive boxes
await HiveService.closeAll();
// Re-initialize everything
state = AsyncValue.data(await _initializeApp());
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
}
/// App version provider
@riverpod
String appVersion(AppVersionRef ref) {
// This would typically come from package_info_plus
return '1.0.0+1';
}
/// App build mode provider
@riverpod
String appBuildMode(AppBuildModeRef ref) {
if (kDebugMode) return 'debug';
if (kProfileMode) return 'profile';
return 'release';
}
/// App ready state provider
@riverpod
bool isAppReady(IsAppReadyRef ref) {
final initData = ref.watch(appInitializationProvider);
return initData.when(
data: (data) => data.state == AppInitializationState.initialized,
loading: () => false,
error: (_, __) => false,
);
}
/// Global app state notifier
@riverpod
class GlobalAppState extends _$GlobalAppState {
@override
Map<String, dynamic> build() {
final initAsync = ref.watch(appInitializationProvider);
final appVersion = ref.watch(appVersionProvider);
final buildMode = ref.watch(appBuildModeProvider);
return initAsync.when(
data: (initData) => {
'isInitialized': initData.state == AppInitializationState.initialized,
'initializationState': initData.state,
'initializationError': initData.error,
'initializedAt': initData.initializedAt?.toIso8601String(),
'appVersion': appVersion,
'buildMode': buildMode,
'isReady': initData.state == AppInitializationState.initialized,
},
loading: () => {
'isInitialized': false,
'initializationState': AppInitializationState.initializing,
'initializationError': null,
'initializedAt': null,
'appVersion': appVersion,
'buildMode': buildMode,
'isReady': false,
},
error: (error, _) => {
'isInitialized': false,
'initializationState': AppInitializationState.error,
'initializationError': error.toString(),
'initializedAt': null,
'appVersion': appVersion,
'buildMode': buildMode,
'isReady': false,
},
);
}
/// Update global state
void updateState(String key, dynamic value) {
final currentState = Map<String, dynamic>.from(state);
currentState[key] = value;
state = currentState;
}
/// Reset global state
void reset() {
ref.invalidate(appInitializationProvider);
}
}
/// Feature flags provider
@riverpod
class FeatureFlags extends _$FeatureFlags {
@override
Map<String, bool> build() {
final buildMode = ref.watch(appBuildModeProvider);
return {
'enableDebugMode': buildMode == 'debug',
'enableAnalytics': buildMode == 'release',
'enableCrashReporting': buildMode != 'debug',
'enablePerformanceMonitoring': true,
'enableOfflineMode': true,
'enableDarkMode': true,
'enableNotifications': true,
};
}
/// Check if feature is enabled
bool isEnabled(String feature) {
return state[feature] ?? false;
}
/// Enable feature
void enableFeature(String feature) {
state = {...state, feature: true};
}
/// Disable feature
void disableFeature(String feature) {
state = {...state, feature: false};
}
/// Toggle feature
void toggleFeature(String feature) {
final currentValue = state[feature] ?? false;
state = {...state, feature: !currentValue};
}
}
/// App configuration provider
@riverpod
class AppConfiguration extends _$AppConfiguration {
@override
Map<String, dynamic> build() {
final buildMode = ref.watch(appBuildModeProvider);
return {
'apiTimeout': 30000, // 30 seconds
'cacheTimeout': 3600000, // 1 hour in milliseconds
'maxRetries': 3,
'retryDelay': 1000, // 1 second
'enableLogging': buildMode == 'debug',
'logLevel': buildMode == 'debug' ? 'verbose' : 'error',
'maxCacheSize': 100 * 1024 * 1024, // 100MB
'imageQuality': 85,
'compressionEnabled': true,
};
}
/// Get configuration value
T? getValue<T>(String key) {
return state[key] as T?;
}
/// Update configuration
void updateConfiguration(String key, dynamic value) {
state = {...state, key: value};
}
/// Update multiple configurations
void updateConfigurations(Map<String, dynamic> updates) {
state = {...state, ...updates};
}
}
/// App lifecycle state provider
@riverpod
class AppLifecycleNotifier extends _$AppLifecycleNotifier {
@override
String build() {
return 'resumed'; // Default state
}
void updateState(String newState) {
state = newState;
debugPrint('📱 App lifecycle state changed to: $newState');
}
}
/// Error tracking provider
@riverpod
class ErrorTracker extends _$ErrorTracker {
@override
List<Map<String, dynamic>> build() {
return [];
}
/// Log error
void logError(dynamic error, StackTrace? stackTrace, {Map<String, dynamic>? context}) {
final errorEntry = {
'error': error.toString(),
'stackTrace': stackTrace?.toString(),
'timestamp': DateTime.now().toIso8601String(),
'context': context,
};
state = [...state, errorEntry];
// Keep only last 100 errors to prevent memory issues
if (state.length > 100) {
state = state.sublist(state.length - 100);
}
debugPrint('🐛 Error logged: $error');
}
/// Clear errors
void clearErrors() {
state = [];
}
/// Get recent errors
List<Map<String, dynamic>> getRecentErrors({int count = 10}) {
return state.reversed.take(count).toList();
}
}

View File

@@ -0,0 +1,200 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$cacheRepositoryHash() => r'0137087454bd51e0465886de0eab7acdc124ecb9';
/// Repository providers
///
/// Copied from [cacheRepository].
@ProviderFor(cacheRepository)
final cacheRepositoryProvider = AutoDisposeProvider<CacheRepository>.internal(
cacheRepository,
name: r'cacheRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$cacheRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef CacheRepositoryRef = AutoDisposeProviderRef<CacheRepository>;
String _$userPreferencesRepositoryHash() =>
r'0244be191fd7576cbfc90468fe491306ed06d537';
/// See also [userPreferencesRepository].
@ProviderFor(userPreferencesRepository)
final userPreferencesRepositoryProvider =
AutoDisposeProvider<UserPreferencesRepository>.internal(
userPreferencesRepository,
name: r'userPreferencesRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$userPreferencesRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef UserPreferencesRepositoryRef
= AutoDisposeProviderRef<UserPreferencesRepository>;
String _$appVersionHash() => r'2605d9c0fd6d6a24e56caceadbe25b8370fedc4f';
/// App version provider
///
/// Copied from [appVersion].
@ProviderFor(appVersion)
final appVersionProvider = AutoDisposeProvider<String>.internal(
appVersion,
name: r'appVersionProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$appVersionHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef AppVersionRef = AutoDisposeProviderRef<String>;
String _$appBuildModeHash() => r'fa100842dc5c894edb352036f8d887d97618f696';
/// App build mode provider
///
/// Copied from [appBuildMode].
@ProviderFor(appBuildMode)
final appBuildModeProvider = AutoDisposeProvider<String>.internal(
appBuildMode,
name: r'appBuildModeProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$appBuildModeHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef AppBuildModeRef = AutoDisposeProviderRef<String>;
String _$isAppReadyHash() => r'b23a0450aa7bb2c9e3ea07630429118f239e610a';
/// App ready state provider
///
/// Copied from [isAppReady].
@ProviderFor(isAppReady)
final isAppReadyProvider = AutoDisposeProvider<bool>.internal(
isAppReady,
name: r'isAppReadyProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$isAppReadyHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef IsAppReadyRef = AutoDisposeProviderRef<bool>;
String _$appInitializationHash() => r'eb87040a5ee3d20a172bef9221c2c56d7e07fe77';
/// App initialization provider
///
/// Copied from [AppInitialization].
@ProviderFor(AppInitialization)
final appInitializationProvider = AutoDisposeAsyncNotifierProvider<
AppInitialization, AppInitializationData>.internal(
AppInitialization.new,
name: r'appInitializationProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$appInitializationHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppInitialization = AutoDisposeAsyncNotifier<AppInitializationData>;
String _$globalAppStateHash() => r'fd0daa69a2a1dc4aaa3af95a1b148ba1e6de0e3f';
/// Global app state notifier
///
/// Copied from [GlobalAppState].
@ProviderFor(GlobalAppState)
final globalAppStateProvider =
AutoDisposeNotifierProvider<GlobalAppState, Map<String, dynamic>>.internal(
GlobalAppState.new,
name: r'globalAppStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$globalAppStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$GlobalAppState = AutoDisposeNotifier<Map<String, dynamic>>;
String _$featureFlagsHash() => r'747e9d64c73eed5b374f37a8f28eb4b7fc94e53d';
/// Feature flags provider
///
/// Copied from [FeatureFlags].
@ProviderFor(FeatureFlags)
final featureFlagsProvider =
AutoDisposeNotifierProvider<FeatureFlags, Map<String, bool>>.internal(
FeatureFlags.new,
name: r'featureFlagsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$featureFlagsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$FeatureFlags = AutoDisposeNotifier<Map<String, bool>>;
String _$appConfigurationHash() => r'115fff1ac67a37ff620bbd15ea142a7211e9dc9c';
/// App configuration provider
///
/// Copied from [AppConfiguration].
@ProviderFor(AppConfiguration)
final appConfigurationProvider = AutoDisposeNotifierProvider<AppConfiguration,
Map<String, dynamic>>.internal(
AppConfiguration.new,
name: r'appConfigurationProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$appConfigurationHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppConfiguration = AutoDisposeNotifier<Map<String, dynamic>>;
String _$appLifecycleNotifierHash() =>
r'344a33715910c38bccc596ac0b543e59cb5752a0';
/// App lifecycle state provider
///
/// Copied from [AppLifecycleNotifier].
@ProviderFor(AppLifecycleNotifier)
final appLifecycleNotifierProvider =
AutoDisposeNotifierProvider<AppLifecycleNotifier, String>.internal(
AppLifecycleNotifier.new,
name: r'appLifecycleNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$appLifecycleNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppLifecycleNotifier = AutoDisposeNotifier<String>;
String _$errorTrackerHash() => r'c286897f0ac33b2b619be30d3fd8d18331635b88';
/// Error tracking provider
///
/// Copied from [ErrorTracker].
@ProviderFor(ErrorTracker)
final errorTrackerProvider = AutoDisposeNotifierProvider<ErrorTracker,
List<Map<String, dynamic>>>.internal(
ErrorTracker.new,
name: r'errorTrackerProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$errorTrackerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ErrorTracker = AutoDisposeNotifier<List<Map<String, dynamic>>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -0,0 +1,51 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../network/dio_client.dart';
import '../network/network_info.dart';
/// Provider for Connectivity instance
final connectivityProvider = Provider<Connectivity>((ref) {
return Connectivity();
});
/// Provider for FlutterSecureStorage instance
final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
return const FlutterSecureStorage();
});
/// Provider for NetworkInfo implementation
final networkInfoProvider = Provider<NetworkInfo>((ref) {
final connectivity = ref.watch(connectivityProvider);
return NetworkInfoImpl(connectivity);
});
/// Provider for DioClient - the main HTTP client
final dioClientProvider = Provider<DioClient>((ref) {
final networkInfo = ref.watch(networkInfoProvider);
final secureStorage = ref.watch(secureStorageProvider);
return DioClient(
networkInfo: networkInfo,
secureStorage: secureStorage,
);
});
/// Provider for network connectivity stream
final networkConnectivityProvider = StreamProvider<bool>((ref) {
final networkInfo = ref.watch(networkInfoProvider);
return networkInfo.connectionStream;
});
/// Provider for current network status
final isConnectedProvider = FutureProvider<bool>((ref) async {
final networkInfo = ref.watch(networkInfoProvider);
return await networkInfo.isConnected;
});
/// Utility provider to get detailed network connection information
final networkConnectionDetailsProvider = FutureProvider((ref) async {
final networkInfo = ref.watch(networkInfoProvider) as NetworkInfoImpl;
return await networkInfo.getConnectionDetails();
});

View File

@@ -0,0 +1,381 @@
/// Example usage of the Riverpod state management system
///
/// This file demonstrates how to properly use the various providers
/// created in this comprehensive Riverpod setup following 2.x best practices.
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers.dart'; // Import the barrel file
/// Example widget showing theme provider usage
class ThemeExampleWidget extends ConsumerWidget {
const ThemeExampleWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch theme-related providers
final settingsAsync = ref.watch(appSettingsNotifierProvider);
final currentTheme = ref.watch(currentThemeModeProvider);
final isDark = ref.watch(isDarkModeProvider);
return settingsAsync.when(
data: (settings) => Card(
child: Column(
children: [
ListTile(
title: const Text('Current Theme'),
subtitle: Text(currentTheme.value),
trailing: Switch(
value: isDark,
onChanged: (value) {
// Update theme mode
final notifier = ref.read(appSettingsNotifierProvider.notifier);
notifier.updateThemeMode(
value ? AppThemeMode.dark : AppThemeMode.light,
);
},
),
),
ListTile(
title: const Text('Notifications'),
trailing: Switch(
value: settings.notificationsEnabled,
onChanged: (value) {
final notifier = ref.read(appSettingsNotifierProvider.notifier);
notifier.updateNotificationsEnabled(value);
},
),
),
],
),
),
loading: () => const Card(
child: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => Card(
child: ListTile(
title: const Text('Error loading settings'),
subtitle: Text(error.toString()),
leading: const Icon(Icons.error),
),
),
);
}
}
/// Example widget showing connectivity provider usage
class ConnectivityExampleWidget extends ConsumerWidget {
const ConnectivityExampleWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch connectivity providers
final isConnected = ref.watch(isConnectedProvider);
final connectionType = ref.watch(connectionTypeProvider);
final networkQuality = ref.watch(networkQualityProvider);
final isWifi = ref.watch(isWifiConnectedProvider);
return Card(
color: isConnected == true ? Colors.green.shade50 : Colors.red.shade50,
child: Column(
children: [
ListTile(
leading: Icon(
isConnected == true ? Icons.wifi : Icons.wifi_off,
color: isConnected == true ? Colors.green : Colors.red,
),
title: Text(isConnected == true ? 'Connected' : 'Disconnected'),
subtitle: Text('Connection: ${connectionType.toString().split('.').last}'),
),
if (isConnected == true) ...[
ListTile(
title: const Text('Network Quality'),
subtitle: Text(networkQuality),
),
if (isWifi == true)
const ListTile(
leading: Icon(Icons.wifi, color: Colors.blue),
title: Text('Using Wi-Fi'),
),
],
ListTile(
title: const Text('Refresh Network Status'),
trailing: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.read(networkStatusNotifierProvider.notifier).refresh();
},
),
),
],
),
);
}
}
/// Example widget showing app initialization and global state
class AppStateExampleWidget extends ConsumerWidget {
const AppStateExampleWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch app state providers
final initAsync = ref.watch(appInitializationProvider);
final globalState = ref.watch(globalAppStateProvider);
final isReady = ref.watch(isAppReadyProvider);
return Card(
child: Column(
children: [
ListTile(
title: const Text('App Status'),
subtitle: Text(isReady == true ? 'Ready' : 'Initializing...'),
leading: Icon(
isReady == true ? Icons.check_circle : Icons.hourglass_empty,
color: isReady == true ? Colors.green : Colors.orange,
),
),
ListTile(
title: const Text('App Version'),
subtitle: Text(globalState['appVersion'] ?? 'Unknown'),
),
ListTile(
title: const Text('Build Mode'),
subtitle: Text(globalState['buildMode'] ?? 'Unknown'),
),
initAsync.when(
data: (data) => ListTile(
title: const Text('Initialization Status'),
subtitle: Text(data.state.toString().split('.').last),
trailing: data.state == AppInitializationState.error
? IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.read(appInitializationProvider.notifier).retry();
},
)
: null,
),
loading: () => const ListTile(
title: Text('Initialization Status'),
subtitle: Text('Loading...'),
trailing: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
error: (error, stack) => ListTile(
title: const Text('Initialization Error'),
subtitle: Text(error.toString()),
leading: const Icon(Icons.error, color: Colors.red),
),
),
],
),
);
}
}
/// Example widget showing storage provider usage
class StorageExampleWidget extends ConsumerWidget {
const StorageExampleWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch storage providers
final storageManager = ref.watch(storageManagerProvider);
final hiveStats = storageManager['hive'] as Map<String, dynamic>? ?? {};
final secureStorageInfo = storageManager['secureStorage'] as Map<String, dynamic>? ?? {};
final healthInfo = storageManager['health'] as Map<String, dynamic>? ?? {};
return Card(
child: Column(
children: [
ListTile(
title: const Text('Storage Health'),
subtitle: Text(healthInfo['isHealthy'] == true ? 'Healthy' : 'Issues detected'),
leading: Icon(
healthInfo['isHealthy'] == true ? Icons.check_circle : Icons.warning,
color: healthInfo['isHealthy'] == true ? Colors.green : Colors.orange,
),
),
ListTile(
title: const Text('Hive Storage'),
subtitle: Text('${hiveStats['appSettingsCount'] ?? 0} settings, ${hiveStats['cacheItemsCount'] ?? 0} cache items'),
),
ListTile(
title: const Text('Secure Storage'),
subtitle: Text('${secureStorageInfo['keyCount'] ?? 0} secure keys stored'),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () {
ref.read(storageManagerProvider.notifier).performMaintenance();
},
child: const Text('Maintenance'),
),
TextButton(
onPressed: () {
ref.read(storageHealthMonitorProvider.notifier).performHealthCheck();
},
child: const Text('Health Check'),
),
],
),
],
),
);
}
}
/// Example widget showing feature flags usage
class FeatureFlagsExampleWidget extends ConsumerWidget {
const FeatureFlagsExampleWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final featureFlags = ref.watch(featureFlagsProvider);
final flagsNotifier = ref.watch(featureFlagsProvider.notifier);
return Card(
child: Column(
children: [
const ListTile(
title: Text('Feature Flags'),
leading: Icon(Icons.flag),
),
...featureFlags.entries.map((entry) => ListTile(
title: Text(entry.key),
trailing: Switch(
value: entry.value,
onChanged: (value) {
if (value) {
flagsNotifier.enableFeature(entry.key);
} else {
flagsNotifier.disableFeature(entry.key);
}
},
),
)),
],
),
);
}
}
/// Example screen demonstrating all provider usage
class ProviderDemoScreen extends ConsumerWidget {
const ProviderDemoScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('Provider Demo'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
// Refresh all providers
ref.invalidate(appSettingsNotifierProvider);
ref.invalidate(networkStatusNotifierProvider);
ref.read(globalAppStateProvider.notifier).reset();
},
),
],
),
body: const SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
children: [
AppStateExampleWidget(),
SizedBox(height: 16),
ThemeExampleWidget(),
SizedBox(height: 16),
ConnectivityExampleWidget(),
SizedBox(height: 16),
StorageExampleWidget(),
SizedBox(height: 16),
FeatureFlagsExampleWidget(),
],
),
),
);
}
}
/// Example of using providers in a lifecycle-aware way
class ProviderLifecycleExample extends ConsumerStatefulWidget {
const ProviderLifecycleExample({super.key});
@override
ConsumerState<ProviderLifecycleExample> createState() => _ProviderLifecycleExampleState();
}
class _ProviderLifecycleExampleState extends ConsumerState<ProviderLifecycleExample>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// Listen to app initialization
ref.listenManual(appInitializationProvider, (previous, next) {
next.when(
data: (data) {
if (data.state == AppInitializationState.initialized) {
debugPrint('✅ App initialized successfully');
}
},
loading: () => debugPrint('🔄 App initializing...'),
error: (error, stack) => debugPrint('❌ App initialization failed: $error'),
);
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
// Update app lifecycle state provider
ref.read(appLifecycleNotifierProvider.notifier).updateState(state.name);
// Handle different lifecycle states
switch (state) {
case AppLifecycleState.paused:
// App is paused, save important state
ref.read(storageManagerProvider.notifier).performMaintenance();
break;
case AppLifecycleState.resumed:
// App is resumed, refresh network status
ref.read(networkStatusNotifierProvider.notifier).refresh();
break;
case AppLifecycleState.detached:
// App is detached, cleanup if needed
break;
case AppLifecycleState.inactive:
case AppLifecycleState.hidden:
// Handle inactive/hidden states
break;
}
}
@override
Widget build(BuildContext context) {
// This would be your main app content
return const ProviderDemoScreen();
}
}

View File

@@ -0,0 +1,267 @@
/// Barrel file for all providers - central export point for dependency injection
///
/// This file exports all provider files to provide a single import point
/// for accessing all application providers following clean architecture principles.
///
/// Usage:
/// ```dart
/// import '../core/providers/providers.dart';
///
/// // Access any provider from the exported modules
/// final theme = ref.watch(currentThemeModeProvider);
/// final isConnected = ref.watch(isConnectedProvider);
/// ```
// Core application providers
export 'app_providers.dart';
// Network and API providers
export 'network_providers.dart';
export 'api_providers.dart';
// Theme and UI state providers
export 'theme_providers.dart';
// Storage and persistence providers
export 'storage_providers.dart' hide secureStorageProvider;
// Shared connectivity providers
export '../../../shared/presentation/providers/connectivity_providers.dart' hide connectivityProvider, isConnectedProvider;
/// Provider initialization helper
///
/// This class provides utilities for initializing and managing providers
/// across the application lifecycle.
class ProviderInitializer {
const ProviderInitializer._();
/// List of providers that need to be initialized early in the app lifecycle
/// These providers will be warmed up during app initialization to ensure
/// they're ready when needed.
static const List<String> criticalProviders = [
'appInitializationProvider',
'appSettingsNotifierProvider',
'networkStatusNotifierProvider',
'secureStorageNotifierProvider',
];
/// List of providers that can be initialized lazily when first accessed
static const List<String> lazyProviders = [
'featureFlagsProvider',
'appConfigurationProvider',
'networkHistoryNotifierProvider',
'errorTrackerProvider',
];
/// Provider categories for better organization and management
static const Map<String, List<String>> providerCategories = {
'core': [
'appInitializationProvider',
'globalAppStateProvider',
'appVersionProvider',
'appBuildModeProvider',
],
'theme': [
'appSettingsNotifierProvider',
'currentThemeModeProvider',
'effectiveThemeModeProvider',
'isDarkModeProvider',
'currentLocaleProvider',
],
'storage': [
'secureStorageNotifierProvider',
'hiveStorageNotifierProvider',
'storageHealthMonitorProvider',
'storageManagerProvider',
],
'network': [
'networkStatusNotifierProvider',
'networkConnectivityStreamProvider',
'isConnectedProvider',
'connectionTypeProvider',
'networkQualityProvider',
],
'utilities': [
'featureFlagsProvider',
'appConfigurationProvider',
'errorTrackerProvider',
'appLifecycleNotifierProvider',
],
};
/// Get providers by category
static List<String> getProvidersByCategory(String category) {
return providerCategories[category] ?? [];
}
/// Get all provider names
static List<String> getAllProviders() {
return providerCategories.values.expand((providers) => providers).toList();
}
/// Check if provider is critical (needs early initialization)
static bool isCriticalProvider(String providerName) {
return criticalProviders.contains(providerName);
}
/// Check if provider can be loaded lazily
static bool isLazyProvider(String providerName) {
return lazyProviders.contains(providerName);
}
}
/// Provider documentation and usage guidelines
///
/// This class contains documentation for proper provider usage patterns
/// following Riverpod 2.x best practices.
class ProviderDocumentation {
const ProviderDocumentation._();
/// Usage examples for different provider types
static const Map<String, String> usageExamples = {
'AsyncNotifierProvider': '''
// For async mutable state management
final notifier = ref.watch(appSettingsNotifierProvider.notifier);
await notifier.updateThemeMode(AppThemeMode.dark);
// Watch for state changes
ref.listen(appSettingsNotifierProvider, (previous, next) {
next.when(
data: (settings) => print('Settings updated'),
loading: () => print('Loading...'),
error: (error, stack) => print('Error: \$error'),
);
});
''',
'NotifierProvider': '''
// For synchronous mutable state
final flags = ref.watch(featureFlagsProvider);
final notifier = ref.watch(featureFlagsProvider.notifier);
// Update state
notifier.enableFeature('darkMode');
notifier.toggleFeature('analytics');
''',
'StreamProvider': '''
// For reactive data streams
final connectivityStream = ref.watch(networkConnectivityStreamProvider);
connectivityStream.when(
data: (status) => Text('Connected: \${status.isConnected}'),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: \$error'),
);
''',
'Provider': '''
// For dependency injection and immutable values
final storage = ref.watch(secureStorageProvider);
final connectivity = ref.watch(connectivityProvider);
// Use in other providers
@riverpod
MyService myService(MyServiceRef ref) {
final storage = ref.watch(secureStorageProvider);
return MyService(storage);
}
''',
'FutureProvider': '''
// For async operations (read-only)
final initData = ref.watch(appInitializationProvider);
initData.when(
data: (data) => data.state == AppInitializationState.initialized
? const HomeScreen()
: const LoadingScreen(),
loading: () => const SplashScreen(),
error: (error, stack) => ErrorScreen(error: error),
);
''',
};
/// Best practices for provider usage
static const List<String> bestPractices = [
'1. Use @riverpod annotation for new providers (Riverpod 2.x)',
'2. Prefer AsyncNotifierProvider for mutable async state',
'3. Use NotifierProvider for mutable synchronous state',
'4. Use StreamProvider for reactive data streams',
'5. Use Provider for dependency injection and immutable values',
'6. Always handle error states in AsyncValue.when()',
'7. Use ref.invalidate() to refresh provider state',
'8. Implement proper disposal in ref.onDispose()',
'9. Keep providers focused on a single responsibility',
'10. Use meaningful provider names that indicate their purpose',
];
/// Common patterns and solutions
static const Map<String, String> commonPatterns = {
'Combining Providers': '''
@riverpod
String userDisplayName(UserDisplayNameRef ref) {
final user = ref.watch(currentUserProvider);
final settings = ref.watch(userSettingsProvider);
return settings.showFullName
? '\${user.firstName} \${user.lastName}'
: user.username;
}
''',
'Error Handling': '''
@riverpod
class DataNotifier extends _\$DataNotifier {
@override
Future<Data> build() async {
return await _loadData();
}
Future<void> refresh() async {
state = const AsyncValue.loading();
try {
final data = await _loadData();
state = AsyncValue.data(data);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
// Log error for debugging
ref.read(errorTrackerProvider.notifier).logError(error, stackTrace);
}
}
}
''',
'State Persistence': '''
@riverpod
class PersistentCounter extends _\$PersistentCounter {
@override
int build() {
// Load from storage on build
final storage = ref.watch(storageProvider);
return storage.getInt('counter') ?? 0;
}
void increment() {
state = state + 1;
// Persist state changes
final storage = ref.read(storageProvider);
storage.setInt('counter', state);
}
}
''',
};
/// Performance tips
static const List<String> performanceTips = [
'1. Use select() to watch specific parts of complex state',
'2. Avoid creating providers in build methods',
'3. Use autoDispose for temporary providers',
'4. Keep provider state minimal and focused',
'5. Use family providers for parameterized providers',
'6. Implement proper caching for expensive operations',
'7. Dispose of resources in ref.onDispose()',
'8. Use keepAlive() for providers that should survive rebuilds',
];
}

View File

@@ -0,0 +1,373 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter/foundation.dart';
import '../database/hive_service.dart';
import '../database/models/app_settings.dart';
import '../database/models/cache_item.dart';
import '../database/models/user_preferences.dart';
part 'storage_providers.g.dart';
/// Secure storage configuration
const _secureStorageOptions = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
/// Secure storage provider
@riverpod
FlutterSecureStorage secureStorage(SecureStorageRef ref) {
return _secureStorageOptions;
}
/// Secure storage notifier for managing secure data
@riverpod
class SecureStorageNotifier extends _$SecureStorageNotifier {
late FlutterSecureStorage _storage;
@override
Future<Map<String, String>> build() async {
_storage = ref.read(secureStorageProvider);
return await _loadAllSecureData();
}
Future<Map<String, String>> _loadAllSecureData() async {
try {
final allData = await _storage.readAll();
return allData;
} catch (e) {
debugPrint('❌ Error loading secure storage data: $e');
return {};
}
}
/// Store secure value
Future<void> store(String key, String value) async {
try {
await _storage.write(key: key, value: value);
state = AsyncValue.data({...state.value ?? {}, key: value});
debugPrint('🔐 Securely stored: $key');
} catch (error, stackTrace) {
debugPrint('❌ Error storing secure value: $error');
state = AsyncValue.error(error, stackTrace);
}
}
/// Retrieve secure value
Future<String?> retrieve(String key) async {
try {
return await _storage.read(key: key);
} catch (e) {
debugPrint('❌ Error retrieving secure value: $e');
return null;
}
}
/// Delete secure value
Future<void> delete(String key) async {
try {
await _storage.delete(key: key);
final currentData = Map<String, String>.from(state.value ?? {});
currentData.remove(key);
state = AsyncValue.data(currentData);
debugPrint('🗑️ Deleted secure key: $key');
} catch (error, stackTrace) {
debugPrint('❌ Error deleting secure value: $error');
state = AsyncValue.error(error, stackTrace);
}
}
/// Clear all secure storage
Future<void> clearAll() async {
try {
await _storage.deleteAll();
state = const AsyncValue.data({});
debugPrint('🧹 Cleared all secure storage');
} catch (error, stackTrace) {
debugPrint('❌ Error clearing secure storage: $error');
state = AsyncValue.error(error, stackTrace);
}
}
/// Check if key exists
Future<bool> containsKey(String key) async {
try {
return await _storage.containsKey(key: key);
} catch (e) {
debugPrint('❌ Error checking key existence: $e');
return false;
}
}
/// Refresh storage data
Future<void> refresh() async {
state = const AsyncValue.loading();
state = AsyncValue.data(await _loadAllSecureData());
}
}
/// Hive storage providers
@riverpod
Box<AppSettings> appSettingsBox(AppSettingsBoxRef ref) {
return HiveService.appSettingsBox;
}
@riverpod
Box<CacheItem> cacheBox(CacheBoxRef ref) {
return HiveService.cacheBox;
}
@riverpod
Box<UserPreferences> userPreferencesBox(UserPreferencesBoxRef ref) {
return HiveService.userDataBox;
}
/// Hive storage notifier for managing Hive data
@riverpod
class HiveStorageNotifier extends _$HiveStorageNotifier {
@override
Map<String, dynamic> build() {
final appSettingsBox = ref.watch(appSettingsBoxProvider);
final cacheBox = ref.watch(cacheBoxProvider);
final userPreferencesBox = ref.watch(userPreferencesBoxProvider);
return {
'appSettingsCount': appSettingsBox.length,
'cacheItemsCount': cacheBox.length,
'userPreferencesCount': userPreferencesBox.length,
'totalSize': _calculateTotalSize(),
'lastUpdated': DateTime.now().toIso8601String(),
};
}
int _calculateTotalSize() {
try {
final appSettingsBox = ref.read(appSettingsBoxProvider);
final cacheBox = ref.read(cacheBoxProvider);
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
// Rough estimation of storage size
return appSettingsBox.length + cacheBox.length + userPreferencesBox.length;
} catch (e) {
debugPrint('❌ Error calculating storage size: $e');
return 0;
}
}
/// Compact all Hive boxes
Future<void> compactAll() async {
try {
final appSettingsBox = ref.read(appSettingsBoxProvider);
final cacheBox = ref.read(cacheBoxProvider);
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
await Future.wait([
appSettingsBox.compact(),
cacheBox.compact(),
userPreferencesBox.compact(),
]);
_updateStats();
debugPrint('🗜️ Compacted all Hive storage');
} catch (e) {
debugPrint('❌ Error compacting storage: $e');
}
}
/// Clear all cache data
Future<void> clearCache() async {
try {
final cacheBox = ref.read(cacheBoxProvider);
await cacheBox.clear();
_updateStats();
debugPrint('🧹 Cleared all cache data');
} catch (e) {
debugPrint('❌ Error clearing cache: $e');
}
}
/// Get storage statistics
Map<String, dynamic> getStorageStats() {
try {
final appSettingsBox = ref.read(appSettingsBoxProvider);
final cacheBox = ref.read(cacheBoxProvider);
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
return {
'appSettings': {
'count': appSettingsBox.length,
'keys': appSettingsBox.keys.toList(),
'isEmpty': appSettingsBox.isEmpty,
},
'cache': {
'count': cacheBox.length,
'keys': cacheBox.keys.take(10).toList(), // Show only first 10 keys
'isEmpty': cacheBox.isEmpty,
},
'userPreferences': {
'count': userPreferencesBox.length,
'keys': userPreferencesBox.keys.toList(),
'isEmpty': userPreferencesBox.isEmpty,
},
'total': {
'items': appSettingsBox.length + cacheBox.length + userPreferencesBox.length,
'estimatedSize': _calculateTotalSize(),
},
};
} catch (e) {
debugPrint('❌ Error getting storage stats: $e');
return {};
}
}
void _updateStats() {
state = {
...state,
'appSettingsCount': ref.read(appSettingsBoxProvider).length,
'cacheItemsCount': ref.read(cacheBoxProvider).length,
'userPreferencesCount': ref.read(userPreferencesBoxProvider).length,
'totalSize': _calculateTotalSize(),
'lastUpdated': DateTime.now().toIso8601String(),
};
}
}
/// Storage health monitor
@riverpod
class StorageHealthMonitor extends _$StorageHealthMonitor {
@override
Map<String, dynamic> build() {
return {
'isHealthy': true,
'lastCheck': DateTime.now().toIso8601String(),
'errors': <String>[],
'warnings': <String>[],
};
}
/// Perform storage health check
Future<void> performHealthCheck() async {
final errors = <String>[];
final warnings = <String>[];
try {
// Check secure storage
final secureStorage = ref.read(secureStorageProvider);
try {
await secureStorage.containsKey(key: 'health_check');
} catch (e) {
errors.add('Secure storage error: $e');
}
// Check Hive boxes
try {
final appSettingsBox = ref.read(appSettingsBoxProvider);
final cacheBox = ref.read(cacheBoxProvider);
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
if (!appSettingsBox.isOpen) errors.add('App settings box is not open');
if (!cacheBox.isOpen) errors.add('Cache box is not open');
if (!userPreferencesBox.isOpen) errors.add('User preferences box is not open');
// Check for large cache
if (cacheBox.length > 1000) {
warnings.add('Cache has more than 1000 items, consider cleanup');
}
} catch (e) {
errors.add('Hive storage error: $e');
}
state = {
'isHealthy': errors.isEmpty,
'lastCheck': DateTime.now().toIso8601String(),
'errors': errors,
'warnings': warnings,
};
debugPrint('🏥 Storage health check completed: ${errors.isEmpty ? '✅ Healthy' : '❌ Issues found'}');
} catch (e) {
state = {
'isHealthy': false,
'lastCheck': DateTime.now().toIso8601String(),
'errors': ['Health check failed: $e'],
'warnings': warnings,
};
}
}
/// Get health status
bool get isHealthy => state['isHealthy'] ?? false;
/// Get errors
List<String> get errors => List<String>.from(state['errors'] ?? []);
/// Get warnings
List<String> get warnings => List<String>.from(state['warnings'] ?? []);
}
/// Unified storage manager
@riverpod
class StorageManager extends _$StorageManager {
@override
Map<String, dynamic> build() {
final hiveStats = ref.watch(hiveStorageNotifierProvider);
final secureStorageAsync = ref.watch(secureStorageNotifierProvider);
final healthStatus = ref.watch(storageHealthMonitorProvider);
return {
'hive': hiveStats,
'secureStorage': secureStorageAsync.when(
data: (data) => {
'keyCount': data.length,
'isAvailable': true,
},
loading: () => {'isAvailable': false, 'isLoading': true},
error: (_, __) => {'isAvailable': false, 'hasError': true},
),
'health': healthStatus,
'lastUpdated': DateTime.now().toIso8601String(),
};
}
/// Clear all storage
Future<void> clearAllStorage() async {
try {
// Clear secure storage
await ref.read(secureStorageNotifierProvider.notifier).clearAll();
// Clear Hive storage
await ref.read(hiveStorageNotifierProvider.notifier).clearCache();
debugPrint('🧹 Cleared all storage');
} catch (e) {
debugPrint('❌ Error clearing all storage: $e');
}
}
/// Perform maintenance
Future<void> performMaintenance() async {
try {
// Compact storage
await ref.read(hiveStorageNotifierProvider.notifier).compactAll();
// Perform health check
await ref.read(storageHealthMonitorProvider.notifier).performHealthCheck();
debugPrint('🔧 Storage maintenance completed');
} catch (e) {
debugPrint('❌ Error during maintenance: $e');
}
}
/// Get storage overview
Map<String, dynamic> getStorageOverview() {
return state;
}
}

View File

@@ -0,0 +1,151 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'storage_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$secureStorageHash() => r'9cd02a4033a37568df4c1778f34709abb5853782';
/// Secure storage provider
///
/// Copied from [secureStorage].
@ProviderFor(secureStorage)
final secureStorageProvider =
AutoDisposeProvider<FlutterSecureStorage>.internal(
secureStorage,
name: r'secureStorageProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$secureStorageHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef SecureStorageRef = AutoDisposeProviderRef<FlutterSecureStorage>;
String _$appSettingsBoxHash() => r'9e348c0084f7f23850f09adb2e6496fdbf8f2bdf';
/// Hive storage providers
///
/// Copied from [appSettingsBox].
@ProviderFor(appSettingsBox)
final appSettingsBoxProvider = AutoDisposeProvider<Box<AppSettings>>.internal(
appSettingsBox,
name: r'appSettingsBoxProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$appSettingsBoxHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef AppSettingsBoxRef = AutoDisposeProviderRef<Box<AppSettings>>;
String _$cacheBoxHash() => r'949b55a2b7423b7fa7182b8e45adf02367ab8c7c';
/// See also [cacheBox].
@ProviderFor(cacheBox)
final cacheBoxProvider = AutoDisposeProvider<Box<CacheItem>>.internal(
cacheBox,
name: r'cacheBoxProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$cacheBoxHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef CacheBoxRef = AutoDisposeProviderRef<Box<CacheItem>>;
String _$userPreferencesBoxHash() =>
r'38e2eab12afb00cca5ad2f48bf1f9ec76cc962c8';
/// See also [userPreferencesBox].
@ProviderFor(userPreferencesBox)
final userPreferencesBoxProvider =
AutoDisposeProvider<Box<UserPreferences>>.internal(
userPreferencesBox,
name: r'userPreferencesBoxProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$userPreferencesBoxHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef UserPreferencesBoxRef = AutoDisposeProviderRef<Box<UserPreferences>>;
String _$secureStorageNotifierHash() =>
r'08d6cb392865d7483027fde37192c07cb944c45f';
/// Secure storage notifier for managing secure data
///
/// Copied from [SecureStorageNotifier].
@ProviderFor(SecureStorageNotifier)
final secureStorageNotifierProvider = AutoDisposeAsyncNotifierProvider<
SecureStorageNotifier, Map<String, String>>.internal(
SecureStorageNotifier.new,
name: r'secureStorageNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$secureStorageNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SecureStorageNotifier = AutoDisposeAsyncNotifier<Map<String, String>>;
String _$hiveStorageNotifierHash() =>
r'5d91bf162282fcfbef13aa7296255bb87640af51';
/// Hive storage notifier for managing Hive data
///
/// Copied from [HiveStorageNotifier].
@ProviderFor(HiveStorageNotifier)
final hiveStorageNotifierProvider = AutoDisposeNotifierProvider<
HiveStorageNotifier, Map<String, dynamic>>.internal(
HiveStorageNotifier.new,
name: r'hiveStorageNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$hiveStorageNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$HiveStorageNotifier = AutoDisposeNotifier<Map<String, dynamic>>;
String _$storageHealthMonitorHash() =>
r'1d52e331a84bd59a36055f5e8963eaa996f9c235';
/// Storage health monitor
///
/// Copied from [StorageHealthMonitor].
@ProviderFor(StorageHealthMonitor)
final storageHealthMonitorProvider = AutoDisposeNotifierProvider<
StorageHealthMonitor, Map<String, dynamic>>.internal(
StorageHealthMonitor.new,
name: r'storageHealthMonitorProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$storageHealthMonitorHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$StorageHealthMonitor = AutoDisposeNotifier<Map<String, dynamic>>;
String _$storageManagerHash() => r'8e017d34c8c574dd2777d6478af3cd921448b080';
/// Unified storage manager
///
/// Copied from [StorageManager].
@ProviderFor(StorageManager)
final storageManagerProvider =
AutoDisposeNotifierProvider<StorageManager, Map<String, dynamic>>.internal(
StorageManager.new,
name: r'storageManagerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$storageManagerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$StorageManager = AutoDisposeNotifier<Map<String, dynamic>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../database/models/app_settings.dart';
import '../database/repositories/settings_repository.dart';
part 'theme_providers.g.dart';
/// Theme mode enumeration
enum AppThemeMode {
light('light'),
dark('dark'),
system('system');
const AppThemeMode(this.value);
final String value;
static AppThemeMode fromString(String value) {
return AppThemeMode.values.firstWhere(
(mode) => mode.value == value,
orElse: () => AppThemeMode.system,
);
}
}
/// Settings repository provider
@riverpod
SettingsRepository settingsRepository(SettingsRepositoryRef ref) {
return SettingsRepository();
}
/// Current app settings provider
@riverpod
class AppSettingsNotifier extends _$AppSettingsNotifier {
late SettingsRepository _repository;
@override
Future<AppSettings> build() async {
_repository = ref.read(settingsRepositoryProvider);
return _repository.getSettings();
}
/// Update theme mode
Future<void> updateThemeMode(AppThemeMode mode) async {
state = const AsyncValue.loading();
try {
await _repository.updateThemeMode(mode.value);
final updatedSettings = _repository.getSettings();
state = AsyncValue.data(updatedSettings);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
/// Update locale
Future<void> updateLocale(String locale) async {
state = const AsyncValue.loading();
try {
await _repository.updateLocale(locale);
final updatedSettings = _repository.getSettings();
state = AsyncValue.data(updatedSettings);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
/// Update notifications enabled
Future<void> updateNotificationsEnabled(bool enabled) async {
state = const AsyncValue.loading();
try {
await _repository.updateNotificationsEnabled(enabled);
final updatedSettings = _repository.getSettings();
state = AsyncValue.data(updatedSettings);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
/// Update analytics enabled
Future<void> updateAnalyticsEnabled(bool enabled) async {
state = const AsyncValue.loading();
try {
await _repository.updateAnalyticsEnabled(enabled);
final updatedSettings = _repository.getSettings();
state = AsyncValue.data(updatedSettings);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
/// Set custom setting
Future<void> setCustomSetting(String key, dynamic value) async {
state = const AsyncValue.loading();
try {
await _repository.setCustomSetting(key, value);
final updatedSettings = _repository.getSettings();
state = AsyncValue.data(updatedSettings);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
/// Reset to default settings
Future<void> resetToDefault() async {
state = const AsyncValue.loading();
try {
await _repository.resetToDefault();
final updatedSettings = _repository.getSettings();
state = AsyncValue.data(updatedSettings);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
/// Refresh settings from storage
Future<void> refresh() async {
state = const AsyncValue.loading();
try {
final settings = _repository.getSettings();
state = AsyncValue.data(settings);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
}
/// Current theme mode provider
@riverpod
AppThemeMode currentThemeMode(CurrentThemeModeRef ref) {
final settingsAsync = ref.watch(appSettingsNotifierProvider);
return settingsAsync.when(
data: (settings) => AppThemeMode.fromString(settings.themeMode),
loading: () => AppThemeMode.system,
error: (_, __) => AppThemeMode.system,
);
}
/// Effective theme mode provider (resolves system theme)
@riverpod
ThemeMode effectiveThemeMode(EffectiveThemeModeRef ref) {
final themeMode = ref.watch(currentThemeModeProvider);
switch (themeMode) {
case AppThemeMode.light:
return ThemeMode.light;
case AppThemeMode.dark:
return ThemeMode.dark;
case AppThemeMode.system:
return ThemeMode.system;
}
}
/// Is dark mode active provider
@riverpod
bool isDarkMode(IsDarkModeRef ref) {
final themeMode = ref.watch(currentThemeModeProvider);
switch (themeMode) {
case AppThemeMode.light:
return false;
case AppThemeMode.dark:
return true;
case AppThemeMode.system:
// Get platform brightness from MediaQuery
// This will be handled at the widget level
return false; // Default fallback
}
}
/// Current locale provider
@riverpod
Locale currentLocale(CurrentLocaleRef ref) {
final settingsAsync = ref.watch(appSettingsNotifierProvider);
return settingsAsync.when(
data: (settings) => Locale(settings.locale),
loading: () => const Locale('en'),
error: (_, __) => const Locale('en'),
);
}
/// Settings stream provider for reactive updates
@riverpod
class SettingsStreamNotifier extends _$SettingsStreamNotifier {
@override
Stream<AppSettings> build() {
final repository = ref.read(settingsRepositoryProvider);
return repository.watchSettings();
}
}
/// Theme preferences provider for quick access
@riverpod
class ThemePreferences extends _$ThemePreferences {
@override
Map<String, dynamic> build() {
final settingsAsync = ref.watch(appSettingsNotifierProvider);
return settingsAsync.when(
data: (settings) => {
'themeMode': settings.themeMode,
'isDarkMode': AppThemeMode.fromString(settings.themeMode) == AppThemeMode.dark,
'locale': settings.locale,
'analyticsEnabled': settings.analyticsEnabled,
'notificationsEnabled': settings.notificationsEnabled,
},
loading: () => {
'themeMode': 'system',
'isDarkMode': false,
'locale': 'en',
'analyticsEnabled': false,
'notificationsEnabled': true,
},
error: (_, __) => {
'themeMode': 'system',
'isDarkMode': false,
'locale': 'en',
'analyticsEnabled': false,
'notificationsEnabled': true,
},
);
}
}

View File

@@ -0,0 +1,153 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'theme_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$settingsRepositoryHash() =>
r'0203e31bb994214ce864bf95a7afa14a8a14b812';
/// Settings repository provider
///
/// Copied from [settingsRepository].
@ProviderFor(settingsRepository)
final settingsRepositoryProvider =
AutoDisposeProvider<SettingsRepository>.internal(
settingsRepository,
name: r'settingsRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$settingsRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef SettingsRepositoryRef = AutoDisposeProviderRef<SettingsRepository>;
String _$currentThemeModeHash() => r'6cd4101e1d0f6cbd7851f117872cd49253fe0564';
/// Current theme mode provider
///
/// Copied from [currentThemeMode].
@ProviderFor(currentThemeMode)
final currentThemeModeProvider = AutoDisposeProvider<AppThemeMode>.internal(
currentThemeMode,
name: r'currentThemeModeProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$currentThemeModeHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef CurrentThemeModeRef = AutoDisposeProviderRef<AppThemeMode>;
String _$effectiveThemeModeHash() =>
r'd747fdd8489857c595ae766ee6a9497c4ad360c0';
/// Effective theme mode provider (resolves system theme)
///
/// Copied from [effectiveThemeMode].
@ProviderFor(effectiveThemeMode)
final effectiveThemeModeProvider = AutoDisposeProvider<ThemeMode>.internal(
effectiveThemeMode,
name: r'effectiveThemeModeProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$effectiveThemeModeHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef EffectiveThemeModeRef = AutoDisposeProviderRef<ThemeMode>;
String _$isDarkModeHash() => r'e76c5818694a33e63bd0a8ba0b7494d7ee12cff5';
/// Is dark mode active provider
///
/// Copied from [isDarkMode].
@ProviderFor(isDarkMode)
final isDarkModeProvider = AutoDisposeProvider<bool>.internal(
isDarkMode,
name: r'isDarkModeProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$isDarkModeHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef IsDarkModeRef = AutoDisposeProviderRef<bool>;
String _$currentLocaleHash() => r'c3cb4000a5eefa748ca41e50818b27323e61605a';
/// Current locale provider
///
/// Copied from [currentLocale].
@ProviderFor(currentLocale)
final currentLocaleProvider = AutoDisposeProvider<Locale>.internal(
currentLocale,
name: r'currentLocaleProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$currentLocaleHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef CurrentLocaleRef = AutoDisposeProviderRef<Locale>;
String _$appSettingsNotifierHash() =>
r'3a66de82c9b8f75bf34ffc7755b145a6d1e9c21e';
/// Current app settings provider
///
/// Copied from [AppSettingsNotifier].
@ProviderFor(AppSettingsNotifier)
final appSettingsNotifierProvider =
AutoDisposeAsyncNotifierProvider<AppSettingsNotifier, AppSettings>.internal(
AppSettingsNotifier.new,
name: r'appSettingsNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$appSettingsNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppSettingsNotifier = AutoDisposeAsyncNotifier<AppSettings>;
String _$settingsStreamNotifierHash() =>
r'1c1e31439ee63edc3217a20c0198bbb2aff6e033';
/// Settings stream provider for reactive updates
///
/// Copied from [SettingsStreamNotifier].
@ProviderFor(SettingsStreamNotifier)
final settingsStreamNotifierProvider = AutoDisposeStreamNotifierProvider<
SettingsStreamNotifier, AppSettings>.internal(
SettingsStreamNotifier.new,
name: r'settingsStreamNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$settingsStreamNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SettingsStreamNotifier = AutoDisposeStreamNotifier<AppSettings>;
String _$themePreferencesHash() => r'71778e4afc614e1566d4a15131e2ab5d2302e57b';
/// Theme preferences provider for quick access
///
/// Copied from [ThemePreferences].
@ProviderFor(ThemePreferences)
final themePreferencesProvider = AutoDisposeNotifierProvider<ThemePreferences,
Map<String, dynamic>>.internal(
ThemePreferences.new,
name: r'themePreferencesProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$themePreferencesHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ThemePreferences = AutoDisposeNotifier<Map<String, dynamic>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

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

View File

@@ -0,0 +1,237 @@
import 'package:dio/dio.dart';
import '../network/dio_client.dart';
import '../network/models/api_response.dart';
/// Base API service class that provides common functionality for all API services
abstract class BaseApiService {
final DioClient _dioClient;
BaseApiService(this._dioClient);
/// Handle API response and extract data
T handleApiResponse<T>(
Response response,
T Function(dynamic) fromJson,
) {
if (response.statusCode != null && response.statusCode! >= 200 && response.statusCode! < 300) {
try {
// If the response data is already the expected type
if (response.data is T) {
return response.data as T;
}
// If the response data is a Map, try to parse it
if (response.data is Map<String, dynamic>) {
return fromJson(response.data);
}
// If the response data is a List, try to parse each item
if (response.data is List && T.toString().contains('List')) {
return response.data as T;
}
return fromJson(response.data);
} catch (e) {
throw Exception('Failed to parse response: $e');
}
} else {
throw Exception('API request failed with status: ${response.statusCode}');
}
}
/// Handle API response with ApiResponse wrapper
T handleWrappedApiResponse<T>(
Response response,
T Function(dynamic) fromJson,
) {
try {
if (response.statusCode != null && response.statusCode! >= 200 && response.statusCode! < 300) {
if (response.data is Map<String, dynamic>) {
final apiResponse = ApiResponse<T>.fromJson(
response.data as Map<String, dynamic>,
fromJson,
);
if (apiResponse.success && apiResponse.data != null) {
return apiResponse.data!;
} else {
throw Exception(apiResponse.message);
}
}
return fromJson(response.data);
} else {
throw Exception('API request failed with status: ${response.statusCode}');
}
} catch (e) {
throw Exception('Failed to handle API response: $e');
}
}
/// Execute a request with error handling
Future<T> executeRequest<T>(
Future<Response> Function() request,
T Function(dynamic) fromJson, {
bool useWrapper = false,
}) async {
try {
final response = await request();
if (useWrapper) {
return handleWrappedApiResponse<T>(response, fromJson);
} else {
return handleApiResponse<T>(response, fromJson);
}
} on DioException catch (e) {
// The error interceptor will have already processed this
if (e.error is NetworkFailure) {
final failure = e.error as NetworkFailure;
throw Exception(failure.message);
} else {
throw Exception(e.message ?? 'Network error occurred');
}
} catch (e) {
throw Exception('Unexpected error: $e');
}
}
/// Get the underlying dio client for advanced usage
DioClient get dioClient => _dioClient;
}
/// Example API service for demonstration
class ExampleApiService extends BaseApiService {
ExampleApiService(super.dioClient);
/// Example: Get user profile
Future<Map<String, dynamic>> getUserProfile(String userId) async {
return executeRequest(
() => dioClient.get('/users/$userId'),
(data) => data as Map<String, dynamic>,
);
}
/// Example: Create a new post
Future<Map<String, dynamic>> createPost(Map<String, dynamic> postData) async {
return executeRequest(
() => dioClient.post('/posts', data: postData),
(data) => data as Map<String, dynamic>,
);
}
/// Example: Get posts with pagination
Future<List<Map<String, dynamic>>> getPosts({
int page = 1,
int limit = 10,
}) async {
return executeRequest(
() => dioClient.get(
'/posts',
queryParameters: {
'page': page,
'limit': limit,
},
),
(data) => (data as List).cast<Map<String, dynamic>>(),
);
}
/// Example: Upload file
Future<Map<String, dynamic>> uploadFile(
String filePath,
String filename,
) async {
try {
// Note: This is a placeholder. In real implementation, you would use:
// final response = await dioClient.uploadFile(
// '/upload',
// File(filePath),
// filename: filename,
// );
final response = await dioClient.post('/upload', data: {
'filename': filename,
'path': filePath,
});
return handleApiResponse(
response,
(data) => data as Map<String, dynamic>,
);
} catch (e) {
throw Exception('File upload failed: $e');
}
}
/// Example: Download file
Future<void> downloadFile(String url, String savePath) async {
try {
await dioClient.downloadFile(url, savePath);
} catch (e) {
throw Exception('File download failed: $e');
}
}
/// Example: Test network connectivity
Future<bool> testConnection() async {
try {
await dioClient.get('/health');
return true;
} catch (e) {
return false;
}
}
}
/// Authentication API service
class AuthApiService extends BaseApiService {
AuthApiService(super.dioClient);
/// Login with credentials
Future<Map<String, dynamic>> login(String email, String password) async {
return executeRequest(
() => dioClient.post('/auth/login', data: {
'email': email,
'password': password,
}),
(data) => data as Map<String, dynamic>,
);
}
/// Logout
Future<void> logout() async {
try {
await dioClient.post('/auth/logout');
await dioClient.authInterceptor.logout();
} catch (e) {
// Even if the API call fails, clear local tokens
await dioClient.authInterceptor.logout();
}
}
/// Register new user
Future<Map<String, dynamic>> register(Map<String, dynamic> userData) async {
return executeRequest(
() => dioClient.post('/auth/register', data: userData),
(data) => data as Map<String, dynamic>,
);
}
/// Check if user is authenticated
Future<bool> isAuthenticated() async {
return await dioClient.authInterceptor.isAuthenticated();
}
/// Store authentication tokens
Future<void> storeTokens({
required String accessToken,
String? refreshToken,
int? expiresIn,
}) async {
await dioClient.authInterceptor.storeTokens(
accessToken: accessToken,
refreshToken: refreshToken,
expiresIn: expiresIn,
);
}
}

288
lib/core/theme/README.md Normal file
View File

@@ -0,0 +1,288 @@
# Material 3 Theme System
A comprehensive Material 3 (Material You) design system implementation for Flutter applications.
## Overview
This theme system provides:
- **Complete Material 3 design system** with proper color schemes, typography, and spacing
- **Dynamic color support** for Material You theming
- **Light and dark theme configurations** with accessibility-compliant colors
- **Responsive typography** that scales based on screen size
- **Consistent spacing system** following Material Design guidelines
- **Custom component themes** for buttons, cards, inputs, and more
- **Theme switching widgets** with smooth animations
## Quick Start
### 1. Import the theme system
```dart
import 'package:base_flutter/core/theme/theme.dart';
```
### 2. Apply themes to your app
```dart
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeControllerProvider);
return MaterialApp(
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode,
// ... rest of your app
);
}
}
```
### 3. Add theme switching capability
```dart
// Simple toggle button
ThemeToggleIconButton()
// Segmented control
ThemeModeSwitch(
style: ThemeSwitchStyle.segmented,
showLabel: true,
)
// Animated switch
AnimatedThemeModeSwitch()
```
## Components
### AppTheme
Main theme configuration class containing Material 3 light and dark themes.
```dart
// Get themes
ThemeData lightTheme = AppTheme.lightTheme;
ThemeData darkTheme = AppTheme.darkTheme;
// Responsive theme
ThemeData responsiveTheme = AppTheme.responsiveTheme(context, isDark: false);
// Dynamic colors (Material You)
ColorScheme? dynamicLight = AppTheme.dynamicLightColorScheme(context);
ThemeData dynamicTheme = AppTheme.createDynamicTheme(
colorScheme: dynamicLight!,
isDark: false,
);
```
### AppColors
Material 3 color system with semantic colors.
```dart
// Color schemes
ColorScheme lightScheme = AppColors.lightScheme;
ColorScheme darkScheme = AppColors.darkScheme;
// Semantic colors
Color success = AppColors.success;
Color warning = AppColors.warning;
Color info = AppColors.info;
// Surface colors with elevation
Color elevatedSurface = AppColors.getSurfaceColor(2, false);
// Accessibility check
bool isAccessible = AppColors.isAccessible(backgroundColor, textColor);
```
### AppTypography
Material 3 typography system with responsive scaling.
```dart
// Typography styles
TextStyle displayLarge = AppTypography.displayLarge;
TextStyle headlineMedium = AppTypography.headlineMedium;
TextStyle bodyLarge = AppTypography.bodyLarge;
// Responsive typography
TextTheme responsiveTheme = AppTypography.responsiveTextTheme(context);
// Semantic text styles
TextStyle errorStyle = AppTypography.error(context);
TextStyle successStyle = AppTypography.success(context);
```
### AppSpacing
Consistent spacing system based on Material Design grid.
```dart
// Spacing values
double small = AppSpacing.sm; // 8dp
double medium = AppSpacing.md; // 12dp
double large = AppSpacing.lg; // 16dp
// EdgeInsets presets
EdgeInsets padding = AppSpacing.paddingLG;
EdgeInsets screenPadding = AppSpacing.screenPaddingAll;
// SizedBox presets
SizedBox verticalSpace = AppSpacing.verticalSpaceMD;
SizedBox horizontalSpace = AppSpacing.horizontalSpaceLG;
// Responsive padding
EdgeInsets responsivePadding = AppSpacing.responsivePadding(context);
// Border radius
BorderRadius cardRadius = AppSpacing.cardRadius;
BorderRadius buttonRadius = AppSpacing.buttonRadius;
// Screen size checks
bool isMobile = AppSpacing.isMobile(context);
bool isTablet = AppSpacing.isTablet(context);
```
## Theme Extensions
### AppColorsExtension
Additional semantic colors not covered by Material 3 ColorScheme.
```dart
// Access theme extension
final colors = Theme.of(context).extension<AppColorsExtension>();
// Use semantic colors
Color successColor = colors?.success ?? Colors.green;
Color warningColor = colors?.warning ?? Colors.orange;
Color infoColor = colors?.info ?? Colors.blue;
```
## Usage Examples
### Using Colors
```dart
Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: Text(
'Hello World',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
)
```
### Using Typography
```dart
Column(
children: [
Text('Headline', style: Theme.of(context).textTheme.headlineMedium),
AppSpacing.verticalSpaceSM,
Text('Body text', style: Theme.of(context).textTheme.bodyLarge),
],
)
```
### Using Spacing
```dart
Padding(
padding: AppSpacing.responsivePadding(context),
child: Column(
children: [
Card(),
AppSpacing.verticalSpaceLG,
ElevatedButton(),
],
),
)
```
### Theme-aware Widgets
```dart
class ThemedCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
child: Padding(
padding: AppSpacing.paddingLG,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Card Title',
style: theme.textTheme.titleMedium,
),
AppSpacing.verticalSpaceSM,
Text(
'Card content that adapts to the current theme.',
style: theme.textTheme.bodyMedium,
),
],
),
),
);
}
}
```
## Features
### ✅ Material 3 Design System
- Complete Material 3 color roles and palettes
- Proper elevation handling with surface tints
- Accessibility-compliant color combinations
- Support for dynamic colors (Material You)
### ✅ Responsive Design
- Typography that scales based on screen size
- Responsive padding and margins
- Adaptive layouts for mobile, tablet, and desktop
- Screen size utilities and breakpoints
### ✅ Theme Management
- State management integration with Riverpod
- Theme mode persistence support
- Smooth theme transitions
- Multiple theme switching UI options
### ✅ Developer Experience
- Type-safe theme access
- Consistent spacing system
- Semantic color names
- Comprehensive documentation
## Customization
### Custom Colors
```dart
// Extend AppColors for your brand colors
class BrandColors extends AppColors {
static const Color brandPrimary = Color(0xFF1976D2);
static const Color brandSecondary = Color(0xFF0288D1);
}
```
### Custom Typography
```dart
// Override typography for custom fonts
class CustomTypography extends AppTypography {
static const String fontFamily = 'CustomFont';
static TextStyle get customStyle => const TextStyle(
fontFamily: fontFamily,
fontSize: 16,
fontWeight: FontWeight.w500,
);
}
```
### Theme Showcase
See `theme_showcase.dart` for a comprehensive demo of all theme components and how they work together.
---
This theme system provides a solid foundation for building beautiful, consistent Flutter applications that follow Material 3 design principles while remaining flexible and customizable for your specific needs.

View File

@@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
/// App color schemes for light and dark modes following Material 3 guidelines
class AppColors {
// Prevent instantiation
AppColors._();
/// Light theme color scheme
static const ColorScheme lightScheme = ColorScheme(
brightness: Brightness.light,
// Primary colors
primary: Color(0xFF1976D2),
onPrimary: Color(0xFFFFFFFF),
primaryContainer: Color(0xFFBBDEFB),
onPrimaryContainer: Color(0xFF0D47A1),
// Secondary colors
secondary: Color(0xFF0288D1),
onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFFB3E5FC),
onSecondaryContainer: Color(0xFF006064),
// Tertiary colors
tertiary: Color(0xFF7B1FA2),
onTertiary: Color(0xFFFFFFFF),
tertiaryContainer: Color(0xFFE1BEE7),
onTertiaryContainer: Color(0xFF4A148C),
// Error colors
error: Color(0xFFD32F2F),
onError: Color(0xFFFFFFFF),
errorContainer: Color(0xFFFFCDD2),
onErrorContainer: Color(0xFFB71C1C),
// Background colors
surface: Color(0xFFFFFFFF),
onSurface: Color(0xFF212121),
surfaceContainerHighest: Color(0xFFF5F5F5),
onSurfaceVariant: Color(0xFF757575),
// Outline colors
outline: Color(0xFFBDBDBD),
outlineVariant: Color(0xFFE0E0E0),
// Shadow and scrim
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
// Inverse colors
inverseSurface: Color(0xFF303030),
onInverseSurface: Color(0xFFFFFFFF),
inversePrimary: Color(0xFF90CAF9),
);
/// Dark theme color scheme
static const ColorScheme darkScheme = ColorScheme(
brightness: Brightness.dark,
// Primary colors
primary: Color(0xFF90CAF9),
onPrimary: Color(0xFF0D47A1),
primaryContainer: Color(0xFF1565C0),
onPrimaryContainer: Color(0xFFE3F2FD),
// Secondary colors
secondary: Color(0xFF81D4FA),
onSecondary: Color(0xFF006064),
secondaryContainer: Color(0xFF0277BD),
onSecondaryContainer: Color(0xFFE0F7FA),
// Tertiary colors
tertiary: Color(0xFFCE93D8),
onTertiary: Color(0xFF4A148C),
tertiaryContainer: Color(0xFF8E24AA),
onTertiaryContainer: Color(0xFFF3E5F5),
// Error colors
error: Color(0xFFEF5350),
onError: Color(0xFFB71C1C),
errorContainer: Color(0xFFC62828),
onErrorContainer: Color(0xFFFFEBEE),
// Background colors
surface: Color(0xFF121212),
onSurface: Color(0xFFFFFFFF),
surfaceContainerHighest: Color(0xFF1E1E1E),
onSurfaceVariant: Color(0xFFBDBDBD),
// Outline colors
outline: Color(0xFF616161),
outlineVariant: Color(0xFF424242),
// Shadow and scrim
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
// Inverse colors
inverseSurface: Color(0xFFE0E0E0),
onInverseSurface: Color(0xFF303030),
inversePrimary: Color(0xFF1976D2),
);
/// Semantic colors for common use cases
static const Color success = Color(0xFF4CAF50);
static const Color onSuccess = Color(0xFFFFFFFF);
static const Color successContainer = Color(0xFFC8E6C9);
static const Color onSuccessContainer = Color(0xFF1B5E20);
static const Color warning = Color(0xFFFF9800);
static const Color onWarning = Color(0xFF000000);
static const Color warningContainer = Color(0xFFFFE0B2);
static const Color onWarningContainer = Color(0xFFE65100);
static const Color info = Color(0xFF2196F3);
static const Color onInfo = Color(0xFFFFFFFF);
static const Color infoContainer = Color(0xFFBBDEFB);
static const Color onInfoContainer = Color(0xFF0D47A1);
/// Surface elevation tints for Material 3
static const List<Color> surfaceTintLight = [
Color(0xFFFFFFFF), // elevation 0
Color(0xFFFCFCFC), // elevation 1
Color(0xFFF8F8F8), // elevation 2
Color(0xFFF5F5F5), // elevation 3
Color(0xFFF2F2F2), // elevation 4
Color(0xFFEFEFEF), // elevation 5
];
static const List<Color> surfaceTintDark = [
Color(0xFF121212), // elevation 0
Color(0xFF1E1E1E), // elevation 1
Color(0xFF232323), // elevation 2
Color(0xFF252525), // elevation 3
Color(0xFF272727), // elevation 4
Color(0xFF2C2C2C), // elevation 5
];
/// Get surface color with elevation tint
static Color getSurfaceColor(int elevation, bool isDark) {
final tints = isDark ? surfaceTintDark : surfaceTintLight;
final index = elevation.clamp(0, tints.length - 1);
return tints[index];
}
/// Accessibility compliant color pairs
static final Map<Color, Color> accessiblePairs = {
// High contrast pairs for better accessibility
const Color(0xFF000000): const Color(0xFFFFFFFF),
const Color(0xFFFFFFFF): const Color(0xFF000000),
const Color(0xFF1976D2): const Color(0xFFFFFFFF),
const Color(0xFFD32F2F): const Color(0xFFFFFFFF),
const Color(0xFF4CAF50): const Color(0xFFFFFFFF),
const Color(0xFFFF9800): const Color(0xFF000000),
};
/// Check if color combination meets WCAG AA standards
static bool isAccessible(Color background, Color foreground) {
final bgLuminance = background.computeLuminance();
final fgLuminance = foreground.computeLuminance();
final ratio = (bgLuminance + 0.05) / (fgLuminance + 0.05);
return ratio >= 4.5 || (1 / ratio) >= 4.5;
}
}

View File

@@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
/// Consistent spacing system following Material 3 guidelines
class AppSpacing {
// Prevent instantiation
AppSpacing._();
/// Base spacing unit (4dp in Material Design)
static const double base = 4.0;
/// Spacing scale based on Material 3 8dp grid system
static const double xs = base; // 4dp
static const double sm = base * 2; // 8dp
static const double md = base * 3; // 12dp
static const double lg = base * 4; // 16dp
static const double xl = base * 5; // 20dp
static const double xxl = base * 6; // 24dp
static const double xxxl = base * 8; // 32dp
/// Semantic spacing values
static const double tiny = xs; // 4dp
static const double small = sm; // 8dp
static const double medium = lg; // 16dp
static const double large = xxl; // 24dp
static const double huge = xxxl; // 32dp
/// Screen margins and padding
static const double screenPadding = lg; // 16dp
static const double screenPaddingLarge = xxl; // 24dp
static const double screenMargin = lg; // 16dp
static const double screenMarginLarge = xxl; // 24dp
/// Card and container spacing
static const double cardPadding = lg; // 16dp
static const double cardPaddingLarge = xxl; // 24dp
static const double cardMargin = sm; // 8dp
static const double cardBorderRadius = md; // 12dp
static const double cardBorderRadiusLarge = lg; // 16dp
/// Button spacing
static const double buttonPadding = lg; // 16dp
static const double buttonPaddingVertical = md; // 12dp
static const double buttonPaddingHorizontal = xxl; // 24dp
static const double buttonSpacing = sm; // 8dp
static const double buttonBorderRadius = md; // 12dp
/// Icon spacing
static const double iconSpacing = sm; // 8dp
static const double iconMargin = xs; // 4dp
/// List item spacing
static const double listItemPadding = lg; // 16dp
static const double listItemSpacing = sm; // 8dp
static const double listItemMargin = xs; // 4dp
/// Form field spacing
static const double fieldSpacing = lg; // 16dp
static const double fieldPadding = lg; // 16dp
static const double fieldBorderRadius = sm; // 8dp
/// Component spacing
static const double componentSpacing = sm; // 8dp
static const double componentMargin = xs; // 4dp
static const double componentPadding = md; // 12dp
/// Divider and border spacing
static const double dividerSpacing = lg; // 16dp
static const double borderWidth = 1.0;
static const double borderWidthThin = 0.5;
static const double borderWidthThick = 2.0;
/// Common EdgeInsets presets
static const EdgeInsets paddingXS = EdgeInsets.all(xs);
static const EdgeInsets paddingSM = EdgeInsets.all(sm);
static const EdgeInsets paddingMD = EdgeInsets.all(md);
static const EdgeInsets paddingLG = EdgeInsets.all(lg);
static const EdgeInsets paddingXL = EdgeInsets.all(xl);
static const EdgeInsets paddingXXL = EdgeInsets.all(xxl);
static const EdgeInsets paddingXXXL = EdgeInsets.all(xxxl);
/// Horizontal padding presets
static const EdgeInsets horizontalXS = EdgeInsets.symmetric(horizontal: xs);
static const EdgeInsets horizontalSM = EdgeInsets.symmetric(horizontal: sm);
static const EdgeInsets horizontalMD = EdgeInsets.symmetric(horizontal: md);
static const EdgeInsets horizontalLG = EdgeInsets.symmetric(horizontal: lg);
static const EdgeInsets horizontalXL = EdgeInsets.symmetric(horizontal: xl);
static const EdgeInsets horizontalXXL = EdgeInsets.symmetric(horizontal: xxl);
/// Vertical padding presets
static const EdgeInsets verticalXS = EdgeInsets.symmetric(vertical: xs);
static const EdgeInsets verticalSM = EdgeInsets.symmetric(vertical: sm);
static const EdgeInsets verticalMD = EdgeInsets.symmetric(vertical: md);
static const EdgeInsets verticalLG = EdgeInsets.symmetric(vertical: lg);
static const EdgeInsets verticalXL = EdgeInsets.symmetric(vertical: xl);
static const EdgeInsets verticalXXL = EdgeInsets.symmetric(vertical: xxl);
/// Screen padding presets
static const EdgeInsets screenPaddingAll = EdgeInsets.all(screenPadding);
static const EdgeInsets screenPaddingHorizontal = EdgeInsets.symmetric(horizontal: screenPadding);
static const EdgeInsets screenPaddingVertical = EdgeInsets.symmetric(vertical: screenPadding);
/// Safe area padding that respects system UI
static EdgeInsets safeAreaPadding(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
return EdgeInsets.only(
top: mediaQuery.padding.top + screenPadding,
bottom: mediaQuery.padding.bottom + screenPadding,
left: mediaQuery.padding.left + screenPadding,
right: mediaQuery.padding.right + screenPadding,
);
}
/// Responsive padding based on screen size
static EdgeInsets responsivePadding(BuildContext context) {
final size = MediaQuery.of(context).size;
if (size.width < 600) {
return screenPaddingAll; // Phone
} else if (size.width < 840) {
return const EdgeInsets.all(screenPaddingLarge); // Large phone / Small tablet
} else {
return const EdgeInsets.all(xxxl); // Tablet and larger
}
}
/// Responsive horizontal padding
static EdgeInsets responsiveHorizontalPadding(BuildContext context) {
final size = MediaQuery.of(context).size;
if (size.width < 600) {
return screenPaddingHorizontal; // Phone
} else if (size.width < 840) {
return const EdgeInsets.symmetric(horizontal: screenPaddingLarge); // Large phone / Small tablet
} else {
return const EdgeInsets.symmetric(horizontal: xxxl); // Tablet and larger
}
}
/// Common SizedBox presets for spacing
static const SizedBox spaceXS = SizedBox(height: xs, width: xs);
static const SizedBox spaceSM = SizedBox(height: sm, width: sm);
static const SizedBox spaceMD = SizedBox(height: md, width: md);
static const SizedBox spaceLG = SizedBox(height: lg, width: lg);
static const SizedBox spaceXL = SizedBox(height: xl, width: xl);
static const SizedBox spaceXXL = SizedBox(height: xxl, width: xxl);
static const SizedBox spaceXXXL = SizedBox(height: xxxl, width: xxxl);
/// Vertical spacing
static const SizedBox verticalSpaceXS = SizedBox(height: xs);
static const SizedBox verticalSpaceSM = SizedBox(height: sm);
static const SizedBox verticalSpaceMD = SizedBox(height: md);
static const SizedBox verticalSpaceLG = SizedBox(height: lg);
static const SizedBox verticalSpaceXL = SizedBox(height: xl);
static const SizedBox verticalSpaceXXL = SizedBox(height: xxl);
static const SizedBox verticalSpaceXXXL = SizedBox(height: xxxl);
/// Horizontal spacing
static const SizedBox horizontalSpaceXS = SizedBox(width: xs);
static const SizedBox horizontalSpaceSM = SizedBox(width: sm);
static const SizedBox horizontalSpaceMD = SizedBox(width: md);
static const SizedBox horizontalSpaceLG = SizedBox(width: lg);
static const SizedBox horizontalSpaceXL = SizedBox(width: xl);
static const SizedBox horizontalSpaceXXL = SizedBox(width: xxl);
static const SizedBox horizontalSpaceXXXL = SizedBox(width: xxxl);
/// Border radius presets
static BorderRadius radiusXS = BorderRadius.circular(xs);
static BorderRadius radiusSM = BorderRadius.circular(sm);
static BorderRadius radiusMD = BorderRadius.circular(md);
static BorderRadius radiusLG = BorderRadius.circular(lg);
static BorderRadius radiusXL = BorderRadius.circular(xl);
static BorderRadius radiusXXL = BorderRadius.circular(xxl);
/// Component-specific border radius
static BorderRadius get cardRadius => radiusMD;
static BorderRadius get buttonRadius => radiusMD;
static BorderRadius get fieldRadius => radiusSM;
static BorderRadius get dialogRadius => radiusLG;
static BorderRadius get sheetRadius => radiusXL;
/// Animation durations following Material 3
static const Duration animationFast = Duration(milliseconds: 100);
static const Duration animationNormal = Duration(milliseconds: 200);
static const Duration animationSlow = Duration(milliseconds: 300);
static const Duration animationSlower = Duration(milliseconds: 500);
/// Elevation values for Material 3
static const double elevationNone = 0;
static const double elevationLow = 1;
static const double elevationMedium = 3;
static const double elevationHigh = 6;
static const double elevationHigher = 12;
static const double elevationHighest = 24;
/// Icon sizes
static const double iconXS = 16;
static const double iconSM = 20;
static const double iconMD = 24;
static const double iconLG = 32;
static const double iconXL = 40;
static const double iconXXL = 48;
/// Button heights following Material 3
static const double buttonHeightSmall = 32;
static const double buttonHeight = 40;
static const double buttonHeightLarge = 56;
/// Minimum touch target size (accessibility)
static const double minTouchTarget = 48;
/// Breakpoints for responsive design
static const double mobileBreakpoint = 600;
static const double tabletBreakpoint = 840;
static const double desktopBreakpoint = 1200;
/// Check if screen is mobile
static bool isMobile(BuildContext context) {
return MediaQuery.of(context).size.width < mobileBreakpoint;
}
/// Check if screen is tablet
static bool isTablet(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width >= mobileBreakpoint && width < desktopBreakpoint;
}
/// Check if screen is desktop
static bool isDesktop(BuildContext context) {
return MediaQuery.of(context).size.width >= desktopBreakpoint;
}
}

View File

@@ -0,0 +1,509 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app_colors.dart';
import 'app_typography.dart';
import 'app_spacing.dart';
/// Main theme configuration with Material 3 design system
class AppTheme {
// Prevent instantiation
AppTheme._();
/// Light theme configuration
static ThemeData get lightTheme {
return ThemeData(
// Material 3 configuration
useMaterial3: true,
colorScheme: AppColors.lightScheme,
// Typography
textTheme: AppTypography.textTheme,
// App bar theme
appBarTheme: _lightAppBarTheme,
// Card theme
cardTheme: CardThemeData(
elevation: AppSpacing.elevationLow,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.cardRadius,
),
margin: const EdgeInsets.all(AppSpacing.cardMargin),
),
// Button themes
elevatedButtonTheme: _elevatedButtonTheme,
filledButtonTheme: _filledButtonTheme,
outlinedButtonTheme: _outlinedButtonTheme,
textButtonTheme: _textButtonTheme,
iconButtonTheme: _iconButtonTheme,
floatingActionButtonTheme: _lightFabTheme,
// Input field themes
inputDecorationTheme: _inputDecorationTheme,
// Other component themes
bottomNavigationBarTheme: _lightBottomNavTheme,
navigationBarTheme: _lightNavigationBarTheme,
navigationRailTheme: _lightNavigationRailTheme,
drawerTheme: _lightDrawerTheme,
dialogTheme: DialogThemeData(
elevation: AppSpacing.elevationHigher,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.dialogRadius,
),
titleTextStyle: AppTypography.headlineSmall,
contentTextStyle: AppTypography.bodyMedium,
),
bottomSheetTheme: _bottomSheetTheme,
snackBarTheme: _snackBarTheme,
chipTheme: _lightChipTheme,
dividerTheme: _dividerTheme,
listTileTheme: _listTileTheme,
switchTheme: _lightSwitchTheme,
checkboxTheme: _lightCheckboxTheme,
radioTheme: _lightRadioTheme,
sliderTheme: _lightSliderTheme,
progressIndicatorTheme: _progressIndicatorTheme,
// Extensions
extensions: const [
AppColorsExtension.light,
],
// Visual density
visualDensity: VisualDensity.adaptivePlatformDensity,
// Material tap target size
materialTapTargetSize: MaterialTapTargetSize.padded,
// Page transitions
pageTransitionsTheme: _pageTransitionsTheme,
// Splash factory
splashFactory: InkRipple.splashFactory,
);
}
/// Dark theme configuration
static ThemeData get darkTheme {
return ThemeData(
// Material 3 configuration
useMaterial3: true,
colorScheme: AppColors.darkScheme,
// Typography
textTheme: AppTypography.textTheme,
// App bar theme
appBarTheme: _darkAppBarTheme,
// Card theme
cardTheme: CardThemeData(
elevation: AppSpacing.elevationLow,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.cardRadius,
),
margin: const EdgeInsets.all(AppSpacing.cardMargin),
),
// Button themes
elevatedButtonTheme: _elevatedButtonTheme,
filledButtonTheme: _filledButtonTheme,
outlinedButtonTheme: _outlinedButtonTheme,
textButtonTheme: _textButtonTheme,
iconButtonTheme: _iconButtonTheme,
floatingActionButtonTheme: _darkFabTheme,
// Input field themes
inputDecorationTheme: _inputDecorationTheme,
// Other component themes
bottomNavigationBarTheme: _darkBottomNavTheme,
navigationBarTheme: _darkNavigationBarTheme,
navigationRailTheme: _darkNavigationRailTheme,
drawerTheme: _darkDrawerTheme,
dialogTheme: DialogThemeData(
elevation: AppSpacing.elevationHigher,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.dialogRadius,
),
titleTextStyle: AppTypography.headlineSmall,
contentTextStyle: AppTypography.bodyMedium,
),
bottomSheetTheme: _bottomSheetTheme,
snackBarTheme: _snackBarTheme,
chipTheme: _darkChipTheme,
dividerTheme: _dividerTheme,
listTileTheme: _listTileTheme,
switchTheme: _darkSwitchTheme,
checkboxTheme: _darkCheckboxTheme,
radioTheme: _darkRadioTheme,
sliderTheme: _darkSliderTheme,
progressIndicatorTheme: _progressIndicatorTheme,
// Extensions
extensions: const [
AppColorsExtension.dark,
],
// Visual density
visualDensity: VisualDensity.adaptivePlatformDensity,
// Material tap target size
materialTapTargetSize: MaterialTapTargetSize.padded,
// Page transitions
pageTransitionsTheme: _pageTransitionsTheme,
// Splash factory
splashFactory: InkRipple.splashFactory,
);
}
/// System UI overlay styles
static const SystemUiOverlayStyle lightSystemUiOverlay = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: Colors.white,
systemNavigationBarIconBrightness: Brightness.dark,
);
static const SystemUiOverlayStyle darkSystemUiOverlay = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.dark,
systemNavigationBarColor: Color(0xFF121212),
systemNavigationBarIconBrightness: Brightness.light,
);
// Private theme components
static AppBarTheme get _lightAppBarTheme => const AppBarTheme(
elevation: 0,
scrolledUnderElevation: 1,
backgroundColor: Colors.transparent,
foregroundColor: Colors.black87,
centerTitle: false,
systemOverlayStyle: lightSystemUiOverlay,
titleTextStyle: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
);
static AppBarTheme get _darkAppBarTheme => const AppBarTheme(
elevation: 0,
scrolledUnderElevation: 1,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
centerTitle: false,
systemOverlayStyle: darkSystemUiOverlay,
titleTextStyle: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w500,
color: Colors.white,
),
);
static ElevatedButtonThemeData get _elevatedButtonTheme => ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, AppSpacing.buttonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
),
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
textStyle: AppTypography.buttonText,
elevation: AppSpacing.elevationLow,
),
);
static FilledButtonThemeData get _filledButtonTheme => FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(0, AppSpacing.buttonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
),
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
textStyle: AppTypography.buttonText,
),
);
static OutlinedButtonThemeData get _outlinedButtonTheme => OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, AppSpacing.buttonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
),
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
textStyle: AppTypography.buttonText,
side: const BorderSide(width: AppSpacing.borderWidth),
),
);
static TextButtonThemeData get _textButtonTheme => TextButtonThemeData(
style: TextButton.styleFrom(
minimumSize: const Size(0, AppSpacing.buttonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
),
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
textStyle: AppTypography.buttonText,
),
);
static IconButtonThemeData get _iconButtonTheme => IconButtonThemeData(
style: IconButton.styleFrom(
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
iconSize: AppSpacing.iconMD,
),
);
static FloatingActionButtonThemeData get _lightFabTheme => FloatingActionButtonThemeData(
elevation: AppSpacing.elevationMedium,
highlightElevation: AppSpacing.elevationHigh,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusLG,
),
);
static FloatingActionButtonThemeData get _darkFabTheme => FloatingActionButtonThemeData(
elevation: AppSpacing.elevationMedium,
highlightElevation: AppSpacing.elevationHigh,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusLG,
),
);
static InputDecorationTheme get _inputDecorationTheme => InputDecorationTheme(
filled: true,
contentPadding: const EdgeInsets.all(AppSpacing.fieldPadding),
border: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: const BorderSide(width: AppSpacing.borderWidth),
),
enabledBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: const BorderSide(width: AppSpacing.borderWidth),
),
focusedBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: const BorderSide(width: AppSpacing.borderWidthThick),
),
errorBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: const BorderSide(width: AppSpacing.borderWidth),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: const BorderSide(width: AppSpacing.borderWidthThick),
),
errorStyle: AppTypography.errorText,
hintStyle: AppTypography.hintText,
labelStyle: AppTypography.bodyMedium,
);
static BottomNavigationBarThemeData get _lightBottomNavTheme => const BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
elevation: AppSpacing.elevationMedium,
selectedLabelStyle: AppTypography.labelSmall,
unselectedLabelStyle: AppTypography.labelSmall,
);
static BottomNavigationBarThemeData get _darkBottomNavTheme => const BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
elevation: AppSpacing.elevationMedium,
selectedLabelStyle: AppTypography.labelSmall,
unselectedLabelStyle: AppTypography.labelSmall,
);
static NavigationBarThemeData get _lightNavigationBarTheme => NavigationBarThemeData(
height: 80,
elevation: AppSpacing.elevationMedium,
labelTextStyle: WidgetStateProperty.all(AppTypography.labelSmall),
);
static NavigationBarThemeData get _darkNavigationBarTheme => NavigationBarThemeData(
height: 80,
elevation: AppSpacing.elevationMedium,
labelTextStyle: WidgetStateProperty.all(AppTypography.labelSmall),
);
static NavigationRailThemeData get _lightNavigationRailTheme => const NavigationRailThemeData(
elevation: AppSpacing.elevationMedium,
labelType: NavigationRailLabelType.selected,
);
static NavigationRailThemeData get _darkNavigationRailTheme => const NavigationRailThemeData(
elevation: AppSpacing.elevationMedium,
labelType: NavigationRailLabelType.selected,
);
static DrawerThemeData get _lightDrawerTheme => DrawerThemeData(
elevation: AppSpacing.elevationHigh,
shape: const RoundedRectangleBorder(),
);
static DrawerThemeData get _darkDrawerTheme => DrawerThemeData(
elevation: AppSpacing.elevationHigh,
shape: const RoundedRectangleBorder(),
);
static BottomSheetThemeData get _bottomSheetTheme => BottomSheetThemeData(
elevation: AppSpacing.elevationHigh,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(AppSpacing.sheetRadius.topLeft.x),
topRight: Radius.circular(AppSpacing.sheetRadius.topRight.x),
),
),
);
static SnackBarThemeData get _snackBarTheme => SnackBarThemeData(
elevation: AppSpacing.elevationMedium,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusSM,
),
behavior: SnackBarBehavior.floating,
contentTextStyle: AppTypography.bodyMedium,
);
static ChipThemeData get _lightChipTheme => ChipThemeData(
elevation: AppSpacing.elevationLow,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusSM,
),
labelStyle: AppTypography.labelMedium,
);
static ChipThemeData get _darkChipTheme => ChipThemeData(
elevation: AppSpacing.elevationLow,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusSM,
),
labelStyle: AppTypography.labelMedium,
);
static const DividerThemeData _dividerTheme = DividerThemeData(
thickness: AppSpacing.borderWidth,
space: AppSpacing.dividerSpacing,
);
static ListTileThemeData get _listTileTheme => const ListTileThemeData(
contentPadding: EdgeInsets.symmetric(
horizontal: AppSpacing.listItemPadding,
vertical: AppSpacing.listItemMargin,
),
titleTextStyle: AppTypography.titleMedium,
subtitleTextStyle: AppTypography.bodyMedium,
);
static SwitchThemeData get _lightSwitchTheme => SwitchThemeData(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) => null,
),
);
static SwitchThemeData get _darkSwitchTheme => SwitchThemeData(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) => null,
),
);
static CheckboxThemeData get _lightCheckboxTheme => CheckboxThemeData(
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusXS,
),
);
static CheckboxThemeData get _darkCheckboxTheme => CheckboxThemeData(
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusXS,
),
);
static RadioThemeData get _lightRadioTheme => const RadioThemeData();
static RadioThemeData get _darkRadioTheme => const RadioThemeData();
static SliderThemeData get _lightSliderTheme => const SliderThemeData(
trackHeight: 4,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10),
);
static SliderThemeData get _darkSliderTheme => const SliderThemeData(
trackHeight: 4,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10),
);
static const ProgressIndicatorThemeData _progressIndicatorTheme = ProgressIndicatorThemeData(
linearTrackColor: Colors.transparent,
);
static const PageTransitionsTheme _pageTransitionsTheme = PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
},
);
/// Create responsive theme based on screen size
static ThemeData responsiveTheme(BuildContext context, {required bool isDark}) {
final baseTheme = isDark ? darkTheme : lightTheme;
final responsiveTextTheme = AppTypography.responsiveTextTheme(context);
return baseTheme.copyWith(
textTheme: responsiveTextTheme,
);
}
/// Get appropriate system UI overlay style based on theme
static SystemUiOverlayStyle getSystemUiOverlayStyle(bool isDark) {
return isDark ? darkSystemUiOverlay : lightSystemUiOverlay;
}
/// Dynamic color scheme support
static ColorScheme? dynamicLightColorScheme(BuildContext context) {
try {
return ColorScheme.fromSeed(
seedColor: AppColors.lightScheme.primary,
brightness: Brightness.light,
);
} catch (e) {
return null;
}
}
static ColorScheme? dynamicDarkColorScheme(BuildContext context) {
try {
return ColorScheme.fromSeed(
seedColor: AppColors.darkScheme.primary,
brightness: Brightness.dark,
);
} catch (e) {
return null;
}
}
/// Create theme with dynamic colors
static ThemeData createDynamicTheme({
required ColorScheme colorScheme,
required bool isDark,
}) {
final baseTheme = isDark ? darkTheme : lightTheme;
return baseTheme.copyWith(colorScheme: colorScheme);
}
}

View File

@@ -0,0 +1,381 @@
import 'package:flutter/material.dart';
/// Typography system following Material 3 guidelines
class AppTypography {
// Prevent instantiation
AppTypography._();
/// Base font family
static const String fontFamily = 'Roboto';
/// Display styles - Largest, reserved for short, important text
static const TextStyle displayLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 57,
fontWeight: FontWeight.w400,
letterSpacing: -0.25,
height: 1.12,
);
static const TextStyle displayMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 45,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.16,
);
static const TextStyle displaySmall = TextStyle(
fontFamily: fontFamily,
fontSize: 36,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.22,
);
/// Headline styles - High-emphasis text for headings
static const TextStyle headlineLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 32,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.25,
);
static const TextStyle headlineMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 28,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.29,
);
static const TextStyle headlineSmall = TextStyle(
fontFamily: fontFamily,
fontSize: 24,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.33,
);
/// Title styles - Medium-emphasis text for titles
static const TextStyle titleLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 22,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.27,
);
static const TextStyle titleMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 16,
fontWeight: FontWeight.w500,
letterSpacing: 0.15,
height: 1.50,
);
static const TextStyle titleSmall = TextStyle(
fontFamily: fontFamily,
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
);
/// Label styles - Small text for labels and captions
static const TextStyle labelLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
);
static const TextStyle labelMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
height: 1.33,
);
static const TextStyle labelSmall = TextStyle(
fontFamily: fontFamily,
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
height: 1.45,
);
/// Body styles - Used for long-form writing
static const TextStyle bodyLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.15,
height: 1.50,
);
static const TextStyle bodyMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
height: 1.43,
);
static const TextStyle bodySmall = TextStyle(
fontFamily: fontFamily,
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
);
/// Create a complete TextTheme for the app
static TextTheme textTheme = const TextTheme(
// Display styles
displayLarge: displayLarge,
displayMedium: displayMedium,
displaySmall: displaySmall,
// Headline styles
headlineLarge: headlineLarge,
headlineMedium: headlineMedium,
headlineSmall: headlineSmall,
// Title styles
titleLarge: titleLarge,
titleMedium: titleMedium,
titleSmall: titleSmall,
// Label styles
labelLarge: labelLarge,
labelMedium: labelMedium,
labelSmall: labelSmall,
// Body styles
bodyLarge: bodyLarge,
bodyMedium: bodyMedium,
bodySmall: bodySmall,
);
/// Responsive typography that scales based on screen size
static TextTheme responsiveTextTheme(BuildContext context) {
final size = MediaQuery.of(context).size;
final scaleFactor = _getScaleFactor(size.width);
return TextTheme(
displayLarge: displayLarge.copyWith(fontSize: displayLarge.fontSize! * scaleFactor),
displayMedium: displayMedium.copyWith(fontSize: displayMedium.fontSize! * scaleFactor),
displaySmall: displaySmall.copyWith(fontSize: displaySmall.fontSize! * scaleFactor),
headlineLarge: headlineLarge.copyWith(fontSize: headlineLarge.fontSize! * scaleFactor),
headlineMedium: headlineMedium.copyWith(fontSize: headlineMedium.fontSize! * scaleFactor),
headlineSmall: headlineSmall.copyWith(fontSize: headlineSmall.fontSize! * scaleFactor),
titleLarge: titleLarge.copyWith(fontSize: titleLarge.fontSize! * scaleFactor),
titleMedium: titleMedium.copyWith(fontSize: titleMedium.fontSize! * scaleFactor),
titleSmall: titleSmall.copyWith(fontSize: titleSmall.fontSize! * scaleFactor),
labelLarge: labelLarge.copyWith(fontSize: labelLarge.fontSize! * scaleFactor),
labelMedium: labelMedium.copyWith(fontSize: labelMedium.fontSize! * scaleFactor),
labelSmall: labelSmall.copyWith(fontSize: labelSmall.fontSize! * scaleFactor),
bodyLarge: bodyLarge.copyWith(fontSize: bodyLarge.fontSize! * scaleFactor),
bodyMedium: bodyMedium.copyWith(fontSize: bodyMedium.fontSize! * scaleFactor),
bodySmall: bodySmall.copyWith(fontSize: bodySmall.fontSize! * scaleFactor),
);
}
/// Calculate scale factor based on screen width
static double _getScaleFactor(double width) {
if (width < 360) {
return 0.9; // Small phones
} else if (width < 600) {
return 1.0; // Normal phones
} else if (width < 840) {
return 1.1; // Large phones / small tablets
} else {
return 1.2; // Tablets and larger
}
}
/// Font weight extensions for better readability
static const FontWeight thin = FontWeight.w100;
static const FontWeight extraLight = FontWeight.w200;
static const FontWeight light = FontWeight.w300;
static const FontWeight regular = FontWeight.w400;
static const FontWeight medium = FontWeight.w500;
static const FontWeight semiBold = FontWeight.w600;
static const FontWeight bold = FontWeight.w700;
static const FontWeight extraBold = FontWeight.w800;
static const FontWeight black = FontWeight.w900;
/// Common text styles for specific use cases
static const TextStyle buttonText = TextStyle(
fontFamily: fontFamily,
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
);
static const TextStyle captionText = TextStyle(
fontFamily: fontFamily,
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
);
static const TextStyle overlineText = TextStyle(
fontFamily: fontFamily,
fontSize: 10,
fontWeight: FontWeight.w500,
letterSpacing: 1.5,
height: 1.6,
);
static const TextStyle errorText = TextStyle(
fontFamily: fontFamily,
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
);
static const TextStyle hintText = TextStyle(
fontFamily: fontFamily,
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.15,
height: 1.50,
);
/// Text styles with semantic colors
static TextStyle success(BuildContext context) => bodyMedium.copyWith(
color: Theme.of(context).extension<AppColorsExtension>()?.success,
);
static TextStyle warning(BuildContext context) => bodyMedium.copyWith(
color: Theme.of(context).extension<AppColorsExtension>()?.warning,
);
static TextStyle error(BuildContext context) => bodyMedium.copyWith(
color: Theme.of(context).colorScheme.error,
);
static TextStyle info(BuildContext context) => bodyMedium.copyWith(
color: Theme.of(context).extension<AppColorsExtension>()?.info,
);
}
/// Theme extension for custom semantic colors
@immutable
class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
final Color? success;
final Color? onSuccess;
final Color? successContainer;
final Color? onSuccessContainer;
final Color? warning;
final Color? onWarning;
final Color? warningContainer;
final Color? onWarningContainer;
final Color? info;
final Color? onInfo;
final Color? infoContainer;
final Color? onInfoContainer;
const AppColorsExtension({
this.success,
this.onSuccess,
this.successContainer,
this.onSuccessContainer,
this.warning,
this.onWarning,
this.warningContainer,
this.onWarningContainer,
this.info,
this.onInfo,
this.infoContainer,
this.onInfoContainer,
});
@override
AppColorsExtension copyWith({
Color? success,
Color? onSuccess,
Color? successContainer,
Color? onSuccessContainer,
Color? warning,
Color? onWarning,
Color? warningContainer,
Color? onWarningContainer,
Color? info,
Color? onInfo,
Color? infoContainer,
Color? onInfoContainer,
}) {
return AppColorsExtension(
success: success ?? this.success,
onSuccess: onSuccess ?? this.onSuccess,
successContainer: successContainer ?? this.successContainer,
onSuccessContainer: onSuccessContainer ?? this.onSuccessContainer,
warning: warning ?? this.warning,
onWarning: onWarning ?? this.onWarning,
warningContainer: warningContainer ?? this.warningContainer,
onWarningContainer: onWarningContainer ?? this.onWarningContainer,
info: info ?? this.info,
onInfo: onInfo ?? this.onInfo,
infoContainer: infoContainer ?? this.infoContainer,
onInfoContainer: onInfoContainer ?? this.onInfoContainer,
);
}
@override
AppColorsExtension lerp(covariant ThemeExtension<AppColorsExtension>? other, double t) {
if (other is! AppColorsExtension) {
return this;
}
return AppColorsExtension(
success: Color.lerp(success, other.success, t),
onSuccess: Color.lerp(onSuccess, other.onSuccess, t),
successContainer: Color.lerp(successContainer, other.successContainer, t),
onSuccessContainer: Color.lerp(onSuccessContainer, other.onSuccessContainer, t),
warning: Color.lerp(warning, other.warning, t),
onWarning: Color.lerp(onWarning, other.onWarning, t),
warningContainer: Color.lerp(warningContainer, other.warningContainer, t),
onWarningContainer: Color.lerp(onWarningContainer, other.onWarningContainer, t),
info: Color.lerp(info, other.info, t),
onInfo: Color.lerp(onInfo, other.onInfo, t),
infoContainer: Color.lerp(infoContainer, other.infoContainer, t),
onInfoContainer: Color.lerp(onInfoContainer, other.onInfoContainer, t),
);
}
/// Light theme extension
static const AppColorsExtension light = AppColorsExtension(
success: Color(0xFF4CAF50),
onSuccess: Color(0xFFFFFFFF),
successContainer: Color(0xFFC8E6C9),
onSuccessContainer: Color(0xFF1B5E20),
warning: Color(0xFFFF9800),
onWarning: Color(0xFF000000),
warningContainer: Color(0xFFFFE0B2),
onWarningContainer: Color(0xFFE65100),
info: Color(0xFF2196F3),
onInfo: Color(0xFFFFFFFF),
infoContainer: Color(0xFFBBDEFB),
onInfoContainer: Color(0xFF0D47A1),
);
/// Dark theme extension
static const AppColorsExtension dark = AppColorsExtension(
success: Color(0xFF66BB6A),
onSuccess: Color(0xFF1B5E20),
successContainer: Color(0xFF2E7D32),
onSuccessContainer: Color(0xFFC8E6C9),
warning: Color(0xFFFFB74D),
onWarning: Color(0xFFE65100),
warningContainer: Color(0xFFF57C00),
onWarningContainer: Color(0xFFFFE0B2),
info: Color(0xFF42A5F5),
onInfo: Color(0xFF0D47A1),
infoContainer: Color(0xFF1976D2),
onInfoContainer: Color(0xFFBBDEFB),
);
}

19
lib/core/theme/theme.dart Normal file
View File

@@ -0,0 +1,19 @@
// Theme system barrel file for easy imports
//
// This file exports all theme-related components for the Material 3 design system.
// Import this file to get access to all theme utilities, colors, typography, and spacing.
// Core theme configuration
export 'app_theme.dart';
// Color system
export 'app_colors.dart';
// Typography system
export 'app_typography.dart';
// Spacing and layout system
export 'app_spacing.dart';
// Theme widgets
export 'widgets/theme_mode_switch.dart';

View File

@@ -0,0 +1,398 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'theme.dart';
/// Theme showcase page demonstrating Material 3 design system
class ThemeShowcasePage extends ConsumerWidget {
/// Creates a theme showcase page
const ThemeShowcasePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final textTheme = theme.textTheme;
return Scaffold(
appBar: AppBar(
title: const Text('Material 3 Theme Showcase'),
actions: const [
ThemeToggleIconButton(),
SizedBox(width: 16),
],
),
body: SingleChildScrollView(
padding: AppSpacing.responsivePadding(context),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Theme Mode Switch Section
_buildSection(
context,
'Theme Mode Switch',
Column(
children: [
const ThemeModeSwitch(
style: ThemeSwitchStyle.segmented,
showLabel: true,
),
AppSpacing.verticalSpaceLG,
const AnimatedThemeModeSwitch(),
AppSpacing.verticalSpaceLG,
const ThemeModeSwitch(
style: ThemeSwitchStyle.toggle,
showLabel: true,
labelText: 'Dark Mode',
),
],
),
),
// Typography Section
_buildSection(
context,
'Typography',
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Display Large', style: textTheme.displayLarge),
AppSpacing.verticalSpaceSM,
Text('Display Medium', style: textTheme.displayMedium),
AppSpacing.verticalSpaceSM,
Text('Display Small', style: textTheme.displaySmall),
AppSpacing.verticalSpaceMD,
Text('Headline Large', style: textTheme.headlineLarge),
AppSpacing.verticalSpaceXS,
Text('Headline Medium', style: textTheme.headlineMedium),
AppSpacing.verticalSpaceXS,
Text('Headline Small', style: textTheme.headlineSmall),
AppSpacing.verticalSpaceMD,
Text('Title Large', style: textTheme.titleLarge),
AppSpacing.verticalSpaceXS,
Text('Title Medium', style: textTheme.titleMedium),
AppSpacing.verticalSpaceXS,
Text('Title Small', style: textTheme.titleSmall),
AppSpacing.verticalSpaceMD,
Text('Body Large', style: textTheme.bodyLarge),
AppSpacing.verticalSpaceXS,
Text('Body Medium', style: textTheme.bodyMedium),
AppSpacing.verticalSpaceXS,
Text('Body Small', style: textTheme.bodySmall),
AppSpacing.verticalSpaceMD,
Text('Label Large', style: textTheme.labelLarge),
AppSpacing.verticalSpaceXS,
Text('Label Medium', style: textTheme.labelMedium),
AppSpacing.verticalSpaceXS,
Text('Label Small', style: textTheme.labelSmall),
],
),
),
// Color Palette Section
_buildSection(
context,
'Color Palette',
Column(
children: [
_buildColorRow(
context,
'Primary',
colorScheme.primary,
colorScheme.onPrimary,
),
_buildColorRow(
context,
'Primary Container',
colorScheme.primaryContainer,
colorScheme.onPrimaryContainer,
),
_buildColorRow(
context,
'Secondary',
colorScheme.secondary,
colorScheme.onSecondary,
),
_buildColorRow(
context,
'Secondary Container',
colorScheme.secondaryContainer,
colorScheme.onSecondaryContainer,
),
_buildColorRow(
context,
'Tertiary',
colorScheme.tertiary,
colorScheme.onTertiary,
),
_buildColorRow(
context,
'Error',
colorScheme.error,
colorScheme.onError,
),
_buildColorRow(
context,
'Surface',
colorScheme.surface,
colorScheme.onSurface,
),
],
),
),
// Buttons Section
_buildSection(
context,
'Buttons',
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton(
onPressed: () {},
child: const Text('Filled Button'),
),
AppSpacing.verticalSpaceSM,
ElevatedButton(
onPressed: () {},
child: const Text('Elevated Button'),
),
AppSpacing.verticalSpaceSM,
OutlinedButton(
onPressed: () {},
child: const Text('Outlined Button'),
),
AppSpacing.verticalSpaceSM,
TextButton(
onPressed: () {},
child: const Text('Text Button'),
),
AppSpacing.verticalSpaceSM,
Row(
children: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.favorite),
),
AppSpacing.horizontalSpaceSM,
IconButton.filled(
onPressed: () {},
icon: const Icon(Icons.favorite),
),
AppSpacing.horizontalSpaceSM,
IconButton.outlined(
onPressed: () {},
icon: const Icon(Icons.favorite),
),
],
),
],
),
),
// Form Components Section
_buildSection(
context,
'Form Components',
Column(
children: [
const TextField(
decoration: InputDecoration(
labelText: 'Label',
hintText: 'Hint text',
helperText: 'Helper text',
),
),
AppSpacing.verticalSpaceLG,
const TextField(
decoration: InputDecoration(
labelText: 'Error state',
errorText: 'Error message',
prefixIcon: Icon(Icons.error),
),
),
AppSpacing.verticalSpaceLG,
Row(
children: [
Checkbox(
value: true,
onChanged: (value) {},
),
const Text('Checkbox'),
AppSpacing.horizontalSpaceLG,
Radio<bool>(
value: true,
groupValue: true,
onChanged: null,
),
const Text('Radio'),
const Spacer(),
Switch(
value: true,
onChanged: (value) {},
),
],
),
],
),
),
// Cards Section
_buildSection(
context,
'Cards',
Column(
children: [
Card(
child: Padding(
padding: AppSpacing.paddingLG,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Card Title',
style: theme.textTheme.titleMedium,
),
AppSpacing.verticalSpaceSM,
Text(
'Card content with some descriptive text that demonstrates how cards look in the Material 3 design system.',
style: theme.textTheme.bodyMedium,
),
AppSpacing.verticalSpaceSM,
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {},
child: const Text('Action'),
),
],
),
],
),
),
),
AppSpacing.verticalSpaceSM,
Card.filled(
child: Padding(
padding: AppSpacing.paddingLG,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filled Card',
style: theme.textTheme.titleMedium,
),
AppSpacing.verticalSpaceSM,
Text(
'This is a filled card variant.',
style: theme.textTheme.bodyMedium,
),
],
),
),
),
],
),
),
// Spacing Demonstration
_buildSection(
context,
'Spacing System',
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('XS (4dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.xs,
color: colorScheme.primary,
),
AppSpacing.verticalSpaceSM,
Text('SM (8dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.sm,
color: colorScheme.primary,
),
AppSpacing.verticalSpaceSM,
Text('MD (12dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.md,
color: colorScheme.primary,
),
AppSpacing.verticalSpaceSM,
Text('LG (16dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.lg,
color: colorScheme.primary,
),
AppSpacing.verticalSpaceSM,
Text('XL (20dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.xl,
color: colorScheme.primary,
),
AppSpacing.verticalSpaceSM,
Text('XXL (24dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.xxl,
color: colorScheme.primary,
),
],
),
),
],
),
),
);
}
Widget _buildSection(BuildContext context, String title, Widget content) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
),
AppSpacing.verticalSpaceLG,
content,
AppSpacing.verticalSpaceXXL,
],
);
}
Widget _buildColorRow(
BuildContext context,
String label,
Color color,
Color onColor,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
child: Container(
height: 48,
decoration: BoxDecoration(
color: color,
borderRadius: AppSpacing.radiusSM,
border: Border.all(
color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3),
),
),
child: Center(
child: Text(
label,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: onColor,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,593 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app_spacing.dart';
import '../app_typography.dart';
/// Theme mode switch widget with Material 3 design
class ThemeModeSwitch extends ConsumerWidget {
/// Creates a theme mode switch
const ThemeModeSwitch({
super.key,
this.showLabel = true,
this.labelText,
this.onChanged,
this.size = ThemeSwitchSize.medium,
this.style = ThemeSwitchStyle.toggle,
});
/// Whether to show the label
final bool showLabel;
/// Custom label text
final String? labelText;
/// Callback when theme mode changes
final ValueChanged<ThemeMode>? onChanged;
/// Size of the switch
final ThemeSwitchSize size;
/// Style of the switch
final ThemeSwitchStyle style;
@override
Widget build(BuildContext context, WidgetRef ref) {
// For now, we'll use a simple state provider
// In a real app, this would be connected to your theme provider
final themeMode = ref.watch(_themeModeProvider);
final theme = Theme.of(context);
switch (style) {
case ThemeSwitchStyle.toggle:
return _buildToggleSwitch(context, theme, themeMode, ref);
case ThemeSwitchStyle.segmented:
return _buildSegmentedSwitch(context, theme, themeMode, ref);
case ThemeSwitchStyle.radio:
return _buildRadioSwitch(context, theme, themeMode, ref);
case ThemeSwitchStyle.dropdown:
return _buildDropdownSwitch(context, theme, themeMode, ref);
}
}
Widget _buildToggleSwitch(
BuildContext context,
ThemeData theme,
ThemeMode themeMode,
WidgetRef ref,
) {
final isDark = themeMode == ThemeMode.dark;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (showLabel) ...[
Icon(
Icons.light_mode,
size: _getIconSize(),
color: isDark ? theme.colorScheme.onSurface.withValues(alpha: 0.6) : theme.colorScheme.primary,
),
AppSpacing.horizontalSpaceSM,
],
Switch(
value: isDark,
onChanged: (value) {
final newMode = value ? ThemeMode.dark : ThemeMode.light;
ref.read(_themeModeProvider.notifier).state = newMode;
onChanged?.call(newMode);
},
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return Icon(
Icons.dark_mode,
size: _getIconSize() * 0.7,
);
}
return Icon(
Icons.light_mode,
size: _getIconSize() * 0.7,
);
},
),
),
if (showLabel) ...[
AppSpacing.horizontalSpaceSM,
Icon(
Icons.dark_mode,
size: _getIconSize(),
color: isDark ? theme.colorScheme.primary : theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
],
if (showLabel && labelText != null) ...[
AppSpacing.horizontalSpaceSM,
Text(
labelText!,
style: _getLabelStyle(),
),
],
],
);
}
Widget _buildSegmentedSwitch(
BuildContext context,
ThemeData theme,
ThemeMode themeMode,
WidgetRef ref,
) {
return SegmentedButton<ThemeMode>(
segments: [
ButtonSegment<ThemeMode>(
value: ThemeMode.light,
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.light_mode, size: _getIconSize()),
if (showLabel) ...[
AppSpacing.horizontalSpaceXS,
Text('Light', style: _getLabelStyle()),
],
],
),
),
ButtonSegment<ThemeMode>(
value: ThemeMode.system,
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.brightness_auto, size: _getIconSize()),
if (showLabel) ...[
AppSpacing.horizontalSpaceXS,
Text('Auto', style: _getLabelStyle()),
],
],
),
),
ButtonSegment<ThemeMode>(
value: ThemeMode.dark,
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.dark_mode, size: _getIconSize()),
if (showLabel) ...[
AppSpacing.horizontalSpaceXS,
Text('Dark', style: _getLabelStyle()),
],
],
),
),
],
selected: {themeMode},
onSelectionChanged: (Set<ThemeMode> selection) {
final newMode = selection.first;
ref.read(_themeModeProvider.notifier).state = newMode;
onChanged?.call(newMode);
},
);
}
Widget _buildRadioSwitch(
BuildContext context,
ThemeData theme,
ThemeMode themeMode,
WidgetRef ref,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (showLabel && labelText != null)
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: Text(
labelText!,
style: theme.textTheme.titleMedium,
),
),
...ThemeMode.values.map((mode) {
return ListTile(
contentPadding: EdgeInsets.zero,
dense: size == ThemeSwitchSize.small,
leading: Radio<ThemeMode>(
value: mode,
groupValue: themeMode,
onChanged: (ThemeMode? value) {
if (value != null) {
ref.read(_themeModeProvider.notifier).state = value;
onChanged?.call(value);
}
},
),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_getThemeModeIcon(mode), size: _getIconSize()),
AppSpacing.horizontalSpaceSM,
Text(_getThemeModeLabel(mode), style: _getLabelStyle()),
],
),
onTap: () {
ref.read(_themeModeProvider.notifier).state = mode;
onChanged?.call(mode);
},
);
}),
],
);
}
Widget _buildDropdownSwitch(
BuildContext context,
ThemeData theme,
ThemeMode themeMode,
WidgetRef ref,
) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (showLabel && labelText != null) ...[
Text(
labelText!,
style: theme.textTheme.titleMedium,
),
AppSpacing.horizontalSpaceSM,
],
DropdownButton<ThemeMode>(
value: themeMode,
onChanged: (ThemeMode? value) {
if (value != null) {
ref.read(_themeModeProvider.notifier).state = value;
onChanged?.call(value);
}
},
items: ThemeMode.values.map((mode) {
return DropdownMenuItem<ThemeMode>(
value: mode,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_getThemeModeIcon(mode), size: _getIconSize()),
AppSpacing.horizontalSpaceSM,
Text(_getThemeModeLabel(mode), style: _getLabelStyle()),
],
),
);
}).toList(),
underline: const SizedBox(),
),
],
);
}
double _getIconSize() {
switch (size) {
case ThemeSwitchSize.small:
return AppSpacing.iconSM;
case ThemeSwitchSize.medium:
return AppSpacing.iconMD;
case ThemeSwitchSize.large:
return AppSpacing.iconLG;
}
}
TextStyle _getLabelStyle() {
switch (size) {
case ThemeSwitchSize.small:
return AppTypography.labelSmall;
case ThemeSwitchSize.medium:
return AppTypography.labelMedium;
case ThemeSwitchSize.large:
return AppTypography.labelLarge;
}
}
IconData _getThemeModeIcon(ThemeMode mode) {
switch (mode) {
case ThemeMode.light:
return Icons.light_mode;
case ThemeMode.dark:
return Icons.dark_mode;
case ThemeMode.system:
return Icons.brightness_auto;
}
}
String _getThemeModeLabel(ThemeMode mode) {
switch (mode) {
case ThemeMode.light:
return 'Light';
case ThemeMode.dark:
return 'Dark';
case ThemeMode.system:
return 'System';
}
}
}
/// Theme switch sizes
enum ThemeSwitchSize {
small,
medium,
large,
}
/// Theme switch styles
enum ThemeSwitchStyle {
toggle,
segmented,
radio,
dropdown,
}
/// Animated theme mode switch with smooth transitions
class AnimatedThemeModeSwitch extends ConsumerStatefulWidget {
/// Creates an animated theme mode switch
const AnimatedThemeModeSwitch({
super.key,
this.duration = const Duration(milliseconds: 300),
this.curve = Curves.easeInOut,
this.showIcon = true,
this.iconSize = 24.0,
});
/// Animation duration
final Duration duration;
/// Animation curve
final Curve curve;
/// Whether to show the theme icon
final bool showIcon;
/// Size of the theme icon
final double iconSize;
@override
ConsumerState<AnimatedThemeModeSwitch> createState() => _AnimatedThemeModeSwitchState();
}
class _AnimatedThemeModeSwitchState extends ConsumerState<AnimatedThemeModeSwitch>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: widget.curve,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final themeMode = ref.watch(_themeModeProvider);
final isDark = themeMode == ThemeMode.dark;
final theme = Theme.of(context);
// Update animation based on theme mode
if (isDark) {
_controller.forward();
} else {
_controller.reverse();
}
return GestureDetector(
onTap: () {
final newMode = isDark ? ThemeMode.light : ThemeMode.dark;
ref.read(_themeModeProvider.notifier).state = newMode;
},
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: widget.iconSize * 2.5,
height: widget.iconSize * 1.4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.iconSize * 0.7),
color: Color.lerp(
theme.colorScheme.surfaceContainerHighest,
theme.colorScheme.inverseSurface,
_animation.value,
),
border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.3),
width: 1,
),
),
child: Stack(
children: [
// Sun icon
AnimatedPositioned(
duration: widget.duration,
curve: widget.curve,
left: isDark ? -widget.iconSize : widget.iconSize * 0.2,
top: widget.iconSize * 0.2,
child: AnimatedOpacity(
duration: widget.duration,
opacity: isDark ? 0.0 : 1.0,
child: Icon(
Icons.light_mode,
size: widget.iconSize,
color: theme.colorScheme.primary,
),
),
),
// Moon icon
AnimatedPositioned(
duration: widget.duration,
curve: widget.curve,
left: isDark ? widget.iconSize * 1.3 : widget.iconSize * 2.5,
top: widget.iconSize * 0.2,
child: AnimatedOpacity(
duration: widget.duration,
opacity: isDark ? 1.0 : 0.0,
child: Icon(
Icons.dark_mode,
size: widget.iconSize,
color: theme.colorScheme.onInverseSurface,
),
),
),
// Sliding thumb
AnimatedPositioned(
duration: widget.duration,
curve: widget.curve,
left: isDark ? widget.iconSize * 1.1 : widget.iconSize * 0.1,
top: widget.iconSize * 0.1,
child: Container(
width: widget.iconSize * 1.2,
height: widget.iconSize * 1.2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: theme.colorScheme.surface,
boxShadow: [
BoxShadow(
color: theme.colorScheme.shadow.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
),
),
],
),
);
},
),
);
}
}
/// Simple theme toggle icon button
class ThemeToggleIconButton extends ConsumerWidget {
/// Creates a theme toggle icon button
const ThemeToggleIconButton({
super.key,
this.tooltip,
this.iconSize,
this.onPressed,
});
/// Tooltip text
final String? tooltip;
/// Icon size
final double? iconSize;
/// Custom callback
final VoidCallback? onPressed;
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(_themeModeProvider);
final isDark = themeMode == ThemeMode.dark;
return IconButton(
onPressed: onPressed ??
() {
final newMode = isDark ? ThemeMode.light : ThemeMode.dark;
ref.read(_themeModeProvider.notifier).state = newMode;
},
icon: AnimatedSwitcher(
duration: AppSpacing.animationNormal,
transitionBuilder: (Widget child, Animation<double> animation) {
return RotationTransition(turns: animation, child: child);
},
child: Icon(
isDark ? Icons.dark_mode : Icons.light_mode,
key: ValueKey(isDark),
size: iconSize,
),
),
tooltip: tooltip ?? (isDark ? 'Switch to light mode' : 'Switch to dark mode'),
);
}
}
/// Theme mode provider - replace with your actual theme provider
final _themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
/// Theme mode controller for managing theme state
class ThemeModeController extends StateNotifier<ThemeMode> {
ThemeModeController() : super(ThemeMode.system);
/// Switch to light mode
void setLightMode() => state = ThemeMode.light;
/// Switch to dark mode
void setDarkMode() => state = ThemeMode.dark;
/// Switch to system mode
void setSystemMode() => state = ThemeMode.system;
/// Toggle between light and dark (ignoring system)
void toggle() {
state = state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
}
/// Check if current mode is dark
bool get isDark => state == ThemeMode.dark;
/// Check if current mode is light
bool get isLight => state == ThemeMode.light;
/// Check if current mode is system
bool get isSystem => state == ThemeMode.system;
}
/// Provider for theme mode controller
final themeModeControllerProvider = StateNotifierProvider<ThemeModeController, ThemeMode>((ref) {
return ThemeModeController();
});
/// Utility methods for theme mode
extension ThemeModeExtensions on ThemeMode {
/// Get the display name for the theme mode
String get displayName {
switch (this) {
case ThemeMode.light:
return 'Light';
case ThemeMode.dark:
return 'Dark';
case ThemeMode.system:
return 'System';
}
}
/// Get the icon for the theme mode
IconData get icon {
switch (this) {
case ThemeMode.light:
return Icons.light_mode;
case ThemeMode.dark:
return Icons.dark_mode;
case ThemeMode.system:
return Icons.brightness_auto;
}
}
/// Get the description for the theme mode
String get description {
switch (this) {
case ThemeMode.light:
return 'Use light theme';
case ThemeMode.dark:
return 'Use dark theme';
case ThemeMode.system:
return 'Follow system setting';
}
}
}

View File

@@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
/// Extensions for BuildContext
extension BuildContextExtensions on BuildContext {
/// Get the current theme
ThemeData get theme => Theme.of(this);
/// Get the color scheme
ColorScheme get colorScheme => theme.colorScheme;
/// Get text theme
TextTheme get textTheme => theme.textTheme;
/// Get media query
MediaQueryData get mediaQuery => MediaQuery.of(this);
/// Get screen size
Size get screenSize => mediaQuery.size;
/// Get screen width
double get screenWidth => screenSize.width;
/// Get screen height
double get screenHeight => screenSize.height;
/// Check if device is in dark mode
bool get isDarkMode => theme.brightness == Brightness.dark;
/// Show snackbar
void showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? colorScheme.error : null,
behavior: SnackBarBehavior.floating,
),
);
}
/// Hide keyboard
void hideKeyboard() {
FocusScope.of(this).unfocus();
}
}
/// Extensions for String
extension StringExtensions on String {
/// Check if string is email
bool get isValidEmail {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
}
/// Check if string is empty or null
bool get isEmptyOrNull {
return isEmpty;
}
/// Check if string is not empty and not null
bool get isNotEmptyAndNotNull {
return isNotEmpty;
}
/// Capitalize first letter
String get capitalize {
if (isEmpty) return this;
return '${this[0].toUpperCase()}${substring(1).toLowerCase()}';
}
/// Title case
String get titleCase {
return split(' ').map((word) => word.capitalize).join(' ');
}
}
/// Extensions for DateTime
extension DateTimeExtensions on DateTime {
/// Check if date is today
bool get isToday {
final now = DateTime.now();
return day == now.day && month == now.month && year == now.year;
}
/// Check if date is yesterday
bool get isYesterday {
final yesterday = DateTime.now().subtract(const Duration(days: 1));
return day == yesterday.day && month == yesterday.month && year == yesterday.year;
}
/// Check if date is tomorrow
bool get isTomorrow {
final tomorrow = DateTime.now().add(const Duration(days: 1));
return day == tomorrow.day && month == tomorrow.month && year == tomorrow.year;
}
/// Get time ago string
String get timeAgo {
final now = DateTime.now();
final difference = now.difference(this);
if (difference.inDays > 365) {
return '${(difference.inDays / 365).floor()} year${(difference.inDays / 365).floor() == 1 ? '' : 's'} ago';
} else if (difference.inDays > 30) {
return '${(difference.inDays / 30).floor()} month${(difference.inDays / 30).floor() == 1 ? '' : 's'} ago';
} else if (difference.inDays > 0) {
return '${difference.inDays} day${difference.inDays == 1 ? '' : 's'} ago';
} else if (difference.inHours > 0) {
return '${difference.inHours} hour${difference.inHours == 1 ? '' : 's'} ago';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes} minute${difference.inMinutes == 1 ? '' : 's'} ago';
} else {
return 'Just now';
}
}
}
/// Extensions for List
extension ListExtensions<T> on List<T> {
/// Check if list is empty or null
bool get isEmptyOrNull {
return isEmpty;
}
/// Check if list is not empty and not null
bool get isNotEmptyAndNotNull {
return isNotEmpty;
}
/// Get first element or null
T? get firstOrNull {
return isEmpty ? null : first;
}
/// Get last element or null
T? get lastOrNull {
return isEmpty ? null : last;
}
}

View File

@@ -0,0 +1,16 @@
import 'package:fpdart/fpdart.dart';
import '../errors/failures.dart';
/// Common type definitions used throughout the application
/// Result type for operations that can fail
typedef Result<T> = Either<Failure, T>;
/// Async result type
typedef AsyncResult<T> = Future<Result<T>>;
/// Data map type for JSON serialization
typedef DataMap = Map<String, dynamic>;
/// Data list type for JSON serialization
typedef DataList = List<DataMap>;

View File

@@ -0,0 +1,3 @@
// Barrel export file for utilities
export 'extensions.dart';
export 'typedef.dart';

View File

@@ -0,0 +1,351 @@
import 'package:flutter/material.dart';
import '../theme/app_spacing.dart';
import '../theme/app_typography.dart';
/// Customizable button component with multiple variants following Material 3 design
///
/// Supports filled, outlined, text, and icon variants with consistent theming
/// and accessibility features.
class AppButton extends StatelessWidget {
/// Creates an app button with the specified variant and configuration
const AppButton({
super.key,
required this.text,
required this.onPressed,
this.variant = AppButtonVariant.filled,
this.size = AppButtonSize.medium,
this.icon,
this.isLoading = false,
this.isFullWidth = false,
this.backgroundColor,
this.foregroundColor,
this.borderColor,
this.elevation,
this.semanticLabel,
});
/// The text displayed on the button
final String text;
/// Called when the button is pressed
final VoidCallback? onPressed;
/// The visual variant of the button
final AppButtonVariant variant;
/// The size of the button
final AppButtonSize size;
/// Optional icon to display alongside text
final IconData? icon;
/// Whether to show a loading indicator
final bool isLoading;
/// Whether the button should take full width
final bool isFullWidth;
/// Custom background color override
final Color? backgroundColor;
/// Custom foreground color override
final Color? foregroundColor;
/// Custom border color override (for outlined variant)
final Color? borderColor;
/// Custom elevation override
final double? elevation;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// Get button configuration based on size
final buttonHeight = _getButtonHeight();
final buttonPadding = _getButtonPadding();
final textStyle = _getTextStyle();
final iconSize = _getIconSize();
// Create button style
final buttonStyle = _createButtonStyle(
theme: theme,
height: buttonHeight,
padding: buttonPadding,
);
// Handle loading state
if (isLoading) {
return _buildLoadingButton(
context: context,
style: buttonStyle,
height: buttonHeight,
);
}
// Build appropriate button variant
Widget button = switch (variant) {
AppButtonVariant.filled => _buildFilledButton(
context: context,
style: buttonStyle,
textStyle: textStyle,
iconSize: iconSize,
),
AppButtonVariant.outlined => _buildOutlinedButton(
context: context,
style: buttonStyle,
textStyle: textStyle,
iconSize: iconSize,
),
AppButtonVariant.text => _buildTextButton(
context: context,
style: buttonStyle,
textStyle: textStyle,
iconSize: iconSize,
),
AppButtonVariant.icon => _buildIconButton(
context: context,
iconSize: iconSize,
),
};
// Apply full width if needed
if (isFullWidth && variant != AppButtonVariant.icon) {
button = SizedBox(
width: double.infinity,
child: button,
);
}
// Add semantic label for accessibility
if (semanticLabel != null) {
button = Semantics(
label: semanticLabel,
child: button,
);
}
return button;
}
/// Build filled button variant
Widget _buildFilledButton({
required BuildContext context,
required ButtonStyle style,
required TextStyle textStyle,
required double iconSize,
}) {
if (icon != null) {
return FilledButton.icon(
onPressed: onPressed,
icon: Icon(icon, size: iconSize),
label: Text(text, style: textStyle),
style: style,
);
}
return FilledButton(
onPressed: onPressed,
style: style,
child: Text(text, style: textStyle),
);
}
/// Build outlined button variant
Widget _buildOutlinedButton({
required BuildContext context,
required ButtonStyle style,
required TextStyle textStyle,
required double iconSize,
}) {
if (icon != null) {
return OutlinedButton.icon(
onPressed: onPressed,
icon: Icon(icon, size: iconSize),
label: Text(text, style: textStyle),
style: style,
);
}
return OutlinedButton(
onPressed: onPressed,
style: style,
child: Text(text, style: textStyle),
);
}
/// Build text button variant
Widget _buildTextButton({
required BuildContext context,
required ButtonStyle style,
required TextStyle textStyle,
required double iconSize,
}) {
if (icon != null) {
return TextButton.icon(
onPressed: onPressed,
icon: Icon(icon, size: iconSize),
label: Text(text, style: textStyle),
style: style,
);
}
return TextButton(
onPressed: onPressed,
style: style,
child: Text(text, style: textStyle),
);
}
/// Build icon button variant
Widget _buildIconButton({
required BuildContext context,
required double iconSize,
}) {
if (icon == null) {
throw ArgumentError('Icon button requires an icon');
}
final theme = Theme.of(context);
return IconButton(
onPressed: onPressed,
icon: Icon(icon, size: iconSize),
style: IconButton.styleFrom(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor ?? theme.colorScheme.primary,
minimumSize: Size(_getButtonHeight(), _getButtonHeight()),
maximumSize: Size(_getButtonHeight(), _getButtonHeight()),
),
tooltip: text,
);
}
/// Build loading button state
Widget _buildLoadingButton({
required BuildContext context,
required ButtonStyle style,
required double height,
}) {
final theme = Theme.of(context);
return FilledButton(
onPressed: null,
style: style,
child: SizedBox(
height: height * 0.5,
width: height * 0.5,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
foregroundColor ?? theme.colorScheme.onPrimary,
),
),
),
);
}
/// Create button style based on variant and customizations
ButtonStyle _createButtonStyle({
required ThemeData theme,
required double height,
required EdgeInsets padding,
}) {
return ButtonStyle(
minimumSize: WidgetStateProperty.all(Size(0, height)),
padding: WidgetStateProperty.all(padding),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
),
backgroundColor: backgroundColor != null
? WidgetStateProperty.all(backgroundColor)
: null,
foregroundColor: foregroundColor != null
? WidgetStateProperty.all(foregroundColor)
: null,
side: borderColor != null && variant == AppButtonVariant.outlined
? WidgetStateProperty.all(BorderSide(color: borderColor!))
: null,
elevation: elevation != null
? WidgetStateProperty.all(elevation)
: null,
);
}
/// Get button height based on size
double _getButtonHeight() {
return switch (size) {
AppButtonSize.small => AppSpacing.buttonHeightSmall,
AppButtonSize.medium => AppSpacing.buttonHeight,
AppButtonSize.large => AppSpacing.buttonHeightLarge,
};
}
/// Get button padding based on size
EdgeInsets _getButtonPadding() {
return switch (size) {
AppButtonSize.small => const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.sm,
),
AppButtonSize.medium => const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
vertical: AppSpacing.buttonPaddingVertical,
),
AppButtonSize.large => const EdgeInsets.symmetric(
horizontal: AppSpacing.xxxl,
vertical: AppSpacing.lg,
),
};
}
/// Get text style based on size
TextStyle _getTextStyle() {
return switch (size) {
AppButtonSize.small => AppTypography.labelMedium,
AppButtonSize.medium => AppTypography.buttonText,
AppButtonSize.large => AppTypography.labelLarge,
};
}
/// Get icon size based on button size
double _getIconSize() {
return switch (size) {
AppButtonSize.small => AppSpacing.iconSM,
AppButtonSize.medium => AppSpacing.iconMD,
AppButtonSize.large => AppSpacing.iconLG,
};
}
}
/// Available button variants
enum AppButtonVariant {
/// Filled button with background color (primary action)
filled,
/// Outlined button with border (secondary action)
outlined,
/// Text button without background (tertiary action)
text,
/// Icon-only button
icon,
}
/// Available button sizes
enum AppButtonSize {
/// Small button (32dp height)
small,
/// Medium button (40dp height) - default
medium,
/// Large button (56dp height)
large,
}

View File

@@ -0,0 +1,538 @@
import 'package:flutter/material.dart';
import '../theme/app_spacing.dart';
/// Reusable card component with Material 3 styling and customization options
///
/// Provides a consistent card design with support for different variants,
/// elevation levels, and content layouts.
class AppCard extends StatelessWidget {
/// Creates a card with the specified content and styling options
const AppCard({
super.key,
required this.child,
this.variant = AppCardVariant.elevated,
this.padding,
this.margin,
this.backgroundColor,
this.shadowColor,
this.surfaceTintColor,
this.elevation,
this.shape,
this.clipBehavior = Clip.none,
this.onTap,
this.semanticLabel,
}) : title = null,
subtitle = null,
leading = null,
trailing = null,
actions = null,
mediaWidget = null;
/// Creates a card with a title, subtitle, and optional actions
const AppCard.titled({
super.key,
required this.title,
this.subtitle,
this.child,
this.leading,
this.trailing,
this.actions,
this.variant = AppCardVariant.elevated,
this.padding,
this.margin,
this.backgroundColor,
this.shadowColor,
this.surfaceTintColor,
this.elevation,
this.shape,
this.clipBehavior = Clip.none,
this.onTap,
this.semanticLabel,
}) : mediaWidget = null;
/// Creates a card optimized for displaying media content
const AppCard.media({
super.key,
required this.mediaWidget,
this.title,
this.subtitle,
this.child,
this.actions,
this.variant = AppCardVariant.elevated,
this.padding,
this.margin,
this.backgroundColor,
this.shadowColor,
this.surfaceTintColor,
this.elevation,
this.shape,
this.clipBehavior = Clip.antiAlias,
this.onTap,
this.semanticLabel,
}) : leading = null,
trailing = null;
/// The content to display inside the card
final Widget? child;
/// Title for titled cards
final String? title;
/// Subtitle for titled cards
final String? subtitle;
/// Leading widget for titled cards (typically an icon or avatar)
final Widget? leading;
/// Trailing widget for titled cards (typically an icon or button)
final Widget? trailing;
/// Action buttons displayed at the bottom of the card
final List<Widget>? actions;
/// Media widget for media cards (typically an image or video)
final Widget? mediaWidget;
/// The visual variant of the card
final AppCardVariant variant;
/// Internal padding of the card content
final EdgeInsets? padding;
/// External margin around the card
final EdgeInsets? margin;
/// Background color override
final Color? backgroundColor;
/// Shadow color override
final Color? shadowColor;
/// Surface tint color for Material 3
final Color? surfaceTintColor;
/// Elevation level override
final double? elevation;
/// Shape override
final ShapeBorder? shape;
/// Clipping behavior for card content
final Clip clipBehavior;
/// Called when the card is tapped
final VoidCallback? onTap;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cardTheme = theme.cardTheme;
// Determine card content based on constructor used
Widget content;
if (title != null) {
content = _buildTitledContent(context);
} else if (mediaWidget != null) {
content = _buildMediaContent(context);
} else if (child != null) {
content = child!;
} else {
content = const SizedBox.shrink();
}
// Apply padding if specified
if (padding != null) {
content = Padding(
padding: padding!,
child: content,
);
} else if (title != null || mediaWidget != null) {
// Apply default padding for titled/media cards
content = Padding(
padding: const EdgeInsets.all(AppSpacing.cardPadding),
child: content,
);
}
// Create the card widget
Widget card = Card(
elevation: _getElevation(theme),
color: backgroundColor ?? cardTheme.color,
shadowColor: shadowColor ?? cardTheme.shadowColor,
surfaceTintColor: surfaceTintColor ?? cardTheme.surfaceTintColor,
shape: shape ?? cardTheme.shape ??
RoundedRectangleBorder(borderRadius: AppSpacing.cardRadius),
clipBehavior: clipBehavior,
margin: margin ?? cardTheme.margin ?? const EdgeInsets.all(AppSpacing.cardMargin),
child: content,
);
// Make card tappable if onTap is provided
if (onTap != null) {
card = InkWell(
onTap: onTap,
borderRadius: shape is RoundedRectangleBorder
? (shape as RoundedRectangleBorder).borderRadius as BorderRadius?
: AppSpacing.cardRadius,
child: card,
);
}
// Add semantic label for accessibility
if (semanticLabel != null) {
card = Semantics(
label: semanticLabel,
child: card,
);
}
return card;
}
/// Build content for titled cards
Widget _buildTitledContent(BuildContext context) {
final theme = Theme.of(context);
final titleStyle = theme.textTheme.titleMedium;
final subtitleStyle = theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
);
final List<Widget> children = [];
// Add header with title, subtitle, leading, and trailing
if (title != null || leading != null || trailing != null) {
children.add(
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (leading != null) ...[
leading!,
AppSpacing.horizontalSpaceMD,
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (title != null)
Text(
title!,
style: titleStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (subtitle != null) ...[
AppSpacing.verticalSpaceXS,
Text(
subtitle!,
style: subtitleStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
if (trailing != null) ...[
AppSpacing.horizontalSpaceXS,
trailing!,
],
],
),
);
}
// Add main content
if (child != null) {
if (children.isNotEmpty) {
children.add(AppSpacing.verticalSpaceLG);
}
children.add(child!);
}
// Add actions
if (actions != null && actions!.isNotEmpty) {
if (children.isNotEmpty) {
children.add(AppSpacing.verticalSpaceLG);
}
children.add(
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: actions!
.expand((action) => [action, AppSpacing.horizontalSpaceSM])
.take(actions!.length * 2 - 1)
.toList(),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: children,
);
}
/// Build content for media cards
Widget _buildMediaContent(BuildContext context) {
final theme = Theme.of(context);
final titleStyle = theme.textTheme.titleMedium;
final subtitleStyle = theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
);
final List<Widget> children = [];
// Add media widget
if (mediaWidget != null) {
children.add(mediaWidget!);
}
// Add title and subtitle
if (title != null || subtitle != null) {
children.add(
Padding(
padding: const EdgeInsets.all(AppSpacing.cardPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null)
Text(
title!,
style: titleStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (subtitle != null) ...[
AppSpacing.verticalSpaceXS,
Text(
subtitle!,
style: subtitleStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
);
}
// Add main content
if (child != null) {
children.add(
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.cardPadding,
).copyWith(
bottom: AppSpacing.cardPadding,
),
child: child!,
),
);
}
// Add actions
if (actions != null && actions!.isNotEmpty) {
children.add(
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.cardPadding,
).copyWith(
bottom: AppSpacing.cardPadding,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: actions!
.expand((action) => [action, AppSpacing.horizontalSpaceSM])
.take(actions!.length * 2 - 1)
.toList(),
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: children,
);
}
/// Get elevation based on variant and theme
double _getElevation(ThemeData theme) {
if (elevation != null) {
return elevation!;
}
return switch (variant) {
AppCardVariant.elevated => theme.cardTheme.elevation ?? AppSpacing.elevationLow,
AppCardVariant.filled => AppSpacing.elevationNone,
AppCardVariant.outlined => AppSpacing.elevationNone,
};
}
}
/// Available card variants
enum AppCardVariant {
/// Elevated card with shadow (default)
elevated,
/// Filled card with background color and no shadow
filled,
/// Outlined card with border and no shadow
outlined,
}
/// Compact card for list items
class AppListCard extends StatelessWidget {
const AppListCard({
super.key,
required this.title,
this.subtitle,
this.leading,
this.trailing,
this.onTap,
this.padding,
this.margin,
this.semanticLabel,
});
final String title;
final String? subtitle;
final Widget? leading;
final Widget? trailing;
final VoidCallback? onTap;
final EdgeInsets? padding;
final EdgeInsets? margin;
final String? semanticLabel;
@override
Widget build(BuildContext context) {
return AppCard(
variant: AppCardVariant.filled,
padding: padding ?? const EdgeInsets.all(AppSpacing.md),
margin: margin ?? const EdgeInsets.symmetric(
horizontal: AppSpacing.screenPadding,
vertical: AppSpacing.xs,
),
onTap: onTap,
semanticLabel: semanticLabel,
child: Row(
children: [
if (leading != null) ...[
leading!,
AppSpacing.horizontalSpaceMD,
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (subtitle != null) ...[
AppSpacing.verticalSpaceXS,
Text(
subtitle!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
if (trailing != null) ...[
AppSpacing.horizontalSpaceSM,
trailing!,
],
],
),
);
}
}
/// Action card for interactive content
class AppActionCard extends StatelessWidget {
const AppActionCard({
super.key,
required this.title,
required this.onTap,
this.subtitle,
this.icon,
this.backgroundColor,
this.foregroundColor,
this.padding,
this.margin,
this.semanticLabel,
});
final String title;
final String? subtitle;
final IconData? icon;
final VoidCallback onTap;
final Color? backgroundColor;
final Color? foregroundColor;
final EdgeInsets? padding;
final EdgeInsets? margin;
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AppCard(
variant: AppCardVariant.filled,
backgroundColor: backgroundColor ?? theme.colorScheme.surfaceContainerHighest,
padding: padding ?? const EdgeInsets.all(AppSpacing.cardPadding),
margin: margin,
onTap: onTap,
semanticLabel: semanticLabel,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(
icon,
size: AppSpacing.iconXL,
color: foregroundColor ?? theme.colorScheme.primary,
),
AppSpacing.verticalSpaceMD,
],
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
color: foregroundColor,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (subtitle != null) ...[
AppSpacing.verticalSpaceXS,
Text(
subtitle!,
style: theme.textTheme.bodySmall?.copyWith(
color: foregroundColor ?? theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
);
}
}

View File

@@ -0,0 +1,696 @@
import 'package:flutter/material.dart';
import '../theme/app_spacing.dart';
import '../theme/app_typography.dart';
import 'app_button.dart';
/// Custom dialog components with Material 3 styling and consistent design
///
/// Provides reusable dialog widgets for common use cases like confirmation,
/// information display, and custom content dialogs.
class AppDialog extends StatelessWidget {
/// Creates a dialog with the specified content and actions
const AppDialog({
super.key,
this.title,
this.content,
this.actions,
this.icon,
this.backgroundColor,
this.elevation,
this.shape,
this.insetPadding,
this.contentPadding,
this.actionsPadding,
this.semanticLabel,
}) : onConfirm = null,
onCancel = null,
onOk = null,
confirmText = '',
cancelText = '',
okText = '',
isDestructive = false;
/// Creates a confirmation dialog
const AppDialog.confirmation({
super.key,
required this.title,
required this.content,
this.icon,
required this.onConfirm,
required this.onCancel,
this.confirmText = 'Confirm',
this.cancelText = 'Cancel',
this.isDestructive = false,
this.backgroundColor,
this.elevation,
this.shape,
this.insetPadding,
this.contentPadding,
this.actionsPadding,
this.semanticLabel,
}) : actions = null,
onOk = null,
okText = '';
/// Creates an information dialog
const AppDialog.info({
super.key,
required this.title,
required this.content,
this.icon = Icons.info_outline,
this.onOk,
this.okText = 'OK',
this.backgroundColor,
this.elevation,
this.shape,
this.insetPadding,
this.contentPadding,
this.actionsPadding,
this.semanticLabel,
}) : actions = null,
onConfirm = null,
onCancel = null,
confirmText = 'OK',
cancelText = '',
isDestructive = false;
/// Creates a warning dialog
const AppDialog.warning({
super.key,
required this.title,
required this.content,
this.icon = Icons.warning_outlined,
required this.onConfirm,
required this.onCancel,
this.confirmText = 'Continue',
this.cancelText = 'Cancel',
this.backgroundColor,
this.elevation,
this.shape,
this.insetPadding,
this.contentPadding,
this.actionsPadding,
this.semanticLabel,
}) : actions = null,
isDestructive = true,
onOk = null,
okText = '';
/// Creates an error dialog
const AppDialog.error({
super.key,
this.title = 'Error',
required this.content,
this.icon = Icons.error_outline,
this.onOk,
this.okText = 'OK',
this.backgroundColor,
this.elevation,
this.shape,
this.insetPadding,
this.contentPadding,
this.actionsPadding,
this.semanticLabel,
}) : actions = null,
onConfirm = null,
onCancel = null,
confirmText = 'OK',
cancelText = '',
isDestructive = false;
/// Dialog title
final String? title;
/// Dialog content (text or widget)
final dynamic content;
/// Custom action widgets
final List<Widget>? actions;
/// Dialog icon
final IconData? icon;
/// Confirm button callback
final VoidCallback? onConfirm;
/// Cancel button callback
final VoidCallback? onCancel;
/// OK button callback (for info/error dialogs)
final VoidCallback? onOk;
/// Confirm button text
final String confirmText;
/// Cancel button text
final String cancelText;
/// OK button text
final String okText;
/// Whether the confirm action is destructive
final bool isDestructive;
/// Background color override
final Color? backgroundColor;
/// Elevation override
final double? elevation;
/// Shape override
final ShapeBorder? shape;
/// Inset padding override
final EdgeInsets? insetPadding;
/// Content padding override
final EdgeInsets? contentPadding;
/// Actions padding override
final EdgeInsets? actionsPadding;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
Widget? titleWidget;
if (title != null || icon != null) {
titleWidget = _buildTitle(context);
}
Widget? contentWidget;
if (content != null) {
contentWidget = _buildContent(context);
}
List<Widget>? actionWidgets;
if (actions != null) {
actionWidgets = actions;
} else {
actionWidgets = _buildDefaultActions(context);
}
Widget dialog = AlertDialog(
title: titleWidget,
content: contentWidget,
actions: actionWidgets,
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape ??
RoundedRectangleBorder(
borderRadius: AppSpacing.dialogRadius,
),
insetPadding: insetPadding ??
const EdgeInsets.symmetric(
horizontal: AppSpacing.screenPadding,
vertical: AppSpacing.screenPaddingLarge,
),
contentPadding: contentPadding ??
const EdgeInsets.fromLTRB(
AppSpacing.screenPaddingLarge,
AppSpacing.lg,
AppSpacing.screenPaddingLarge,
AppSpacing.sm,
),
actionsPadding: actionsPadding ??
const EdgeInsets.fromLTRB(
AppSpacing.screenPaddingLarge,
0,
AppSpacing.screenPaddingLarge,
AppSpacing.lg,
),
);
// Add semantic label for accessibility
if (semanticLabel != null) {
dialog = Semantics(
label: semanticLabel,
child: dialog,
);
}
return dialog;
}
/// Build dialog title with optional icon
Widget _buildTitle(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
if (icon != null && title != null) {
return Row(
children: [
Icon(
icon,
size: AppSpacing.iconMD,
color: _getIconColor(colorScheme),
),
AppSpacing.horizontalSpaceMD,
Expanded(
child: Text(
title!,
style: theme.dialogTheme.titleTextStyle ??
AppTypography.headlineSmall,
),
),
],
);
} else if (icon != null) {
return Icon(
icon,
size: AppSpacing.iconLG,
color: _getIconColor(colorScheme),
);
} else if (title != null) {
return Text(
title!,
style: theme.dialogTheme.titleTextStyle ??
AppTypography.headlineSmall,
);
}
return const SizedBox.shrink();
}
/// Build dialog content
Widget _buildContent(BuildContext context) {
final theme = Theme.of(context);
if (content is Widget) {
return content as Widget;
} else if (content is String) {
return Text(
content as String,
style: theme.dialogTheme.contentTextStyle ??
AppTypography.bodyMedium,
);
}
return const SizedBox.shrink();
}
/// Build default action buttons based on dialog type
List<Widget>? _buildDefaultActions(BuildContext context) {
// Info/Error dialog with single OK button
if (onOk != null) {
return [
AppButton(
text: okText,
onPressed: () {
Navigator.of(context).pop();
onOk?.call();
},
variant: AppButtonVariant.text,
),
];
}
// Confirmation dialog with Cancel and Confirm buttons
if (onConfirm != null && onCancel != null) {
return [
AppButton(
text: cancelText,
onPressed: () {
Navigator.of(context).pop(false);
onCancel?.call();
},
variant: AppButtonVariant.text,
),
AppSpacing.horizontalSpaceSM,
AppButton(
text: confirmText,
onPressed: () {
Navigator.of(context).pop(true);
onConfirm?.call();
},
variant: isDestructive
? AppButtonVariant.filled
: AppButtonVariant.filled,
backgroundColor: isDestructive
? Theme.of(context).colorScheme.error
: null,
),
];
}
return null;
}
/// Get appropriate icon color based on dialog type
Color _getIconColor(ColorScheme colorScheme) {
if (icon == Icons.error_outline) {
return colorScheme.error;
} else if (icon == Icons.warning_outlined) {
return colorScheme.error;
} else if (icon == Icons.info_outline) {
return colorScheme.primary;
}
return colorScheme.onSurfaceVariant;
}
/// Show this dialog
static Future<T?> show<T extends Object?>({
required BuildContext context,
required AppDialog dialog,
bool barrierDismissible = true,
}) {
return showDialog<T>(
context: context,
barrierDismissible: barrierDismissible,
builder: (context) => dialog,
);
}
}
/// Simple confirmation dialog helper
class AppConfirmDialog {
/// Show a confirmation dialog
static Future<bool?> show({
required BuildContext context,
required String title,
required String message,
String confirmText = 'Confirm',
String cancelText = 'Cancel',
bool isDestructive = false,
IconData? icon,
bool barrierDismissible = true,
}) {
return AppDialog.show<bool>(
context: context,
barrierDismissible: barrierDismissible,
dialog: AppDialog.confirmation(
title: title,
content: message,
icon: icon,
confirmText: confirmText,
cancelText: cancelText,
isDestructive: isDestructive,
onConfirm: () {},
onCancel: () {},
),
);
}
/// Show a delete confirmation dialog
static Future<bool?> showDelete({
required BuildContext context,
String title = 'Delete Item',
String message = 'Are you sure you want to delete this item? This action cannot be undone.',
String confirmText = 'Delete',
String cancelText = 'Cancel',
bool barrierDismissible = true,
}) {
return show(
context: context,
title: title,
message: message,
confirmText: confirmText,
cancelText: cancelText,
isDestructive: true,
icon: Icons.delete_outline,
barrierDismissible: barrierDismissible,
);
}
}
/// Simple info dialog helper
class AppInfoDialog {
/// Show an information dialog
static Future<void> show({
required BuildContext context,
required String title,
required String message,
String okText = 'OK',
IconData icon = Icons.info_outline,
bool barrierDismissible = true,
}) {
return AppDialog.show<void>(
context: context,
barrierDismissible: barrierDismissible,
dialog: AppDialog.info(
title: title,
content: message,
icon: icon,
okText: okText,
onOk: () {},
),
);
}
/// Show a success dialog
static Future<void> showSuccess({
required BuildContext context,
String title = 'Success',
required String message,
String okText = 'OK',
bool barrierDismissible = true,
}) {
return show(
context: context,
title: title,
message: message,
okText: okText,
icon: Icons.check_circle_outline,
barrierDismissible: barrierDismissible,
);
}
/// Show an error dialog
static Future<void> showError({
required BuildContext context,
String title = 'Error',
required String message,
String okText = 'OK',
bool barrierDismissible = true,
}) {
return AppDialog.show<void>(
context: context,
barrierDismissible: barrierDismissible,
dialog: AppDialog.error(
title: title,
content: message,
okText: okText,
onOk: () {},
),
);
}
}
/// Loading dialog that shows a progress indicator
class AppLoadingDialog extends StatelessWidget {
/// Creates a loading dialog
const AppLoadingDialog({
super.key,
this.message = 'Loading...',
this.showProgress = false,
this.progress,
this.barrierDismissible = false,
});
/// Loading message
final String message;
/// Whether to show determinate progress
final bool showProgress;
/// Progress value (0.0 to 1.0)
final double? progress;
/// Whether the dialog can be dismissed
final bool barrierDismissible;
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.dialogRadius,
),
content: IntrinsicHeight(
child: Column(
children: [
if (showProgress && progress != null)
LinearProgressIndicator(value: progress)
else
const CircularProgressIndicator(),
AppSpacing.verticalSpaceLG,
Text(
message,
style: AppTypography.bodyMedium,
textAlign: TextAlign.center,
),
],
),
),
);
}
/// Show loading dialog
static Future<T> show<T>({
required BuildContext context,
required Future<T> future,
String message = 'Loading...',
bool showProgress = false,
bool barrierDismissible = false,
}) async {
showDialog<void>(
context: context,
barrierDismissible: barrierDismissible,
builder: (context) => AppLoadingDialog(
message: message,
showProgress: showProgress,
barrierDismissible: barrierDismissible,
),
);
try {
final result = await future;
if (context.mounted) {
Navigator.of(context).pop();
}
return result;
} catch (error) {
if (context.mounted) {
Navigator.of(context).pop();
}
rethrow;
}
}
}
/// Custom bottom sheet dialog
class AppBottomSheetDialog extends StatelessWidget {
/// Creates a bottom sheet dialog
const AppBottomSheetDialog({
super.key,
this.title,
required this.child,
this.actions,
this.showDragHandle = true,
this.isScrollControlled = false,
this.maxHeight,
this.padding,
this.semanticLabel,
});
/// Dialog title
final String? title;
/// Dialog content
final Widget child;
/// Action buttons
final List<Widget>? actions;
/// Whether to show drag handle
final bool showDragHandle;
/// Whether the sheet should be full screen
final bool isScrollControlled;
/// Maximum height of the sheet
final double? maxHeight;
/// Content padding
final EdgeInsets? padding;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final defaultMaxHeight = mediaQuery.size.height * 0.9;
Widget content = Container(
constraints: BoxConstraints(
maxHeight: maxHeight ?? defaultMaxHeight,
),
decoration: BoxDecoration(
color: theme.bottomSheetTheme.backgroundColor ??
theme.colorScheme.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(28),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (showDragHandle)
Container(
width: 32,
height: 4,
margin: const EdgeInsets.only(top: 12, bottom: 8),
decoration: BoxDecoration(
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.4),
borderRadius: BorderRadius.circular(2),
),
),
if (title != null)
Padding(
padding: const EdgeInsets.all(AppSpacing.screenPaddingLarge),
child: Text(
title!,
style: AppTypography.headlineSmall,
textAlign: TextAlign.center,
),
),
Flexible(
child: Padding(
padding: padding ??
const EdgeInsets.symmetric(
horizontal: AppSpacing.screenPaddingLarge,
),
child: child,
),
),
if (actions != null && actions!.isNotEmpty)
Padding(
padding: const EdgeInsets.all(AppSpacing.screenPaddingLarge),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: actions!
.expand((action) => [action, AppSpacing.horizontalSpaceSM])
.take(actions!.length * 2 - 1)
.toList(),
),
),
],
),
);
// Add semantic label for accessibility
if (semanticLabel != null) {
content = Semantics(
label: semanticLabel,
child: content,
);
}
return content;
}
/// Show bottom sheet dialog
static Future<T?> show<T>({
required BuildContext context,
required AppBottomSheetDialog dialog,
bool isDismissible = true,
bool enableDrag = true,
}) {
return showModalBottomSheet<T>(
context: context,
builder: (context) => dialog,
isScrollControlled: dialog.isScrollControlled,
isDismissible: isDismissible,
enableDrag: enableDrag,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(28),
),
),
);
}
}

View File

@@ -0,0 +1,501 @@
import 'package:flutter/material.dart';
import '../theme/app_spacing.dart';
import '../theme/app_typography.dart';
import 'app_button.dart';
/// Empty state widget with icon, message, and optional action button
///
/// Provides a consistent empty state design with customizable content
/// and actions for when no data is available.
class AppEmptyState extends StatelessWidget {
/// Creates an empty state widget with the specified content
const AppEmptyState({
super.key,
this.icon,
this.title,
this.message,
this.actionText,
this.onActionPressed,
this.secondaryActionText,
this.onSecondaryActionPressed,
this.illustration,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
});
/// Creates a "no data" empty state
const AppEmptyState.noData({
super.key,
this.title = 'No data available',
this.message = 'There is no data to display at the moment.',
this.actionText,
this.onActionPressed,
this.secondaryActionText,
this.onSecondaryActionPressed,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
}) : icon = Icons.inbox_outlined,
illustration = null;
/// Creates a "no search results" empty state
const AppEmptyState.noSearchResults({
super.key,
this.title = 'No results found',
this.message = 'Try adjusting your search criteria or filters.',
this.actionText = 'Clear filters',
this.onActionPressed,
this.secondaryActionText,
this.onSecondaryActionPressed,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
}) : icon = Icons.search_off_outlined,
illustration = null;
/// Creates a "no network" empty state
const AppEmptyState.noNetwork({
super.key,
this.title = 'No internet connection',
this.message = 'Please check your network connection and try again.',
this.actionText = 'Retry',
this.onActionPressed,
this.secondaryActionText,
this.onSecondaryActionPressed,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
}) : icon = Icons.wifi_off_outlined,
illustration = null;
/// Creates an "error" empty state
const AppEmptyState.error({
super.key,
this.title = 'Something went wrong',
this.message = 'An error occurred while loading the data.',
this.actionText = 'Try again',
this.onActionPressed,
this.secondaryActionText,
this.onSecondaryActionPressed,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
}) : icon = Icons.error_outline,
illustration = null;
/// Creates a "coming soon" empty state
const AppEmptyState.comingSoon({
super.key,
this.title = 'Coming soon',
this.message = 'This feature is currently under development.',
this.actionText,
this.onActionPressed,
this.secondaryActionText,
this.onSecondaryActionPressed,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
}) : icon = Icons.construction_outlined,
illustration = null;
/// Creates an "access denied" empty state
const AppEmptyState.accessDenied({
super.key,
this.title = 'Access denied',
this.message = 'You do not have permission to view this content.',
this.actionText = 'Request access',
this.onActionPressed,
this.secondaryActionText,
this.onSecondaryActionPressed,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
}) : icon = Icons.lock_outline,
illustration = null;
/// Icon to display (ignored if illustration is provided)
final IconData? icon;
/// Custom illustration widget
final Widget? illustration;
/// Main title text
final String? title;
/// Descriptive message text
final String? message;
/// Primary action button text
final String? actionText;
/// Callback for primary action
final VoidCallback? onActionPressed;
/// Secondary action button text
final String? secondaryActionText;
/// Callback for secondary action
final VoidCallback? onSecondaryActionPressed;
/// Maximum width of the empty state content
final double maxWidth;
/// Padding around the content
final EdgeInsets? padding;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
Widget content = Column(
mainAxisSize: MainAxisSize.min,
children: [
// Icon or illustration
if (illustration != null)
illustration!
else if (icon != null)
Icon(
icon,
size: AppSpacing.iconXXL * 2, // 96dp
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
),
AppSpacing.verticalSpaceXXL,
// Title
if (title != null)
Text(
title!,
style: AppTypography.headlineSmall.copyWith(
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
// Message
if (message != null) ...[
if (title != null) AppSpacing.verticalSpaceMD,
Text(
message!,
style: AppTypography.bodyMedium.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
],
// Actions
if (actionText != null || secondaryActionText != null) ...[
AppSpacing.verticalSpaceXXL,
_buildActions(context),
],
],
);
// Apply maximum width constraint
content = ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: content,
);
// Apply padding
content = Padding(
padding: padding ?? const EdgeInsets.all(AppSpacing.screenPadding),
child: content,
);
// Center the content
content = Center(child: content);
// Add semantic label for accessibility
if (semanticLabel != null) {
content = Semantics(
label: semanticLabel,
child: content,
);
}
return content;
}
/// Build action buttons
Widget _buildActions(BuildContext context) {
final List<Widget> actions = [];
// Primary action
if (actionText != null && onActionPressed != null) {
actions.add(
AppButton(
text: actionText!,
onPressed: onActionPressed,
variant: AppButtonVariant.filled,
isFullWidth: secondaryActionText == null, // Full width if only one button
),
);
}
// Secondary action
if (secondaryActionText != null && onSecondaryActionPressed != null) {
if (actions.isNotEmpty) {
actions.add(AppSpacing.verticalSpaceMD);
}
actions.add(
AppButton(
text: secondaryActionText!,
onPressed: onSecondaryActionPressed,
variant: AppButtonVariant.outlined,
isFullWidth: true,
),
);
}
// If both actions exist and we want them side by side
if (actions.length >= 3) {
return Column(children: actions);
}
// Single action or side-by-side layout
if (actions.length == 1) {
return actions.first;
}
return Column(children: actions);
}
}
/// Compact empty state for smaller spaces (like lists)
class AppCompactEmptyState extends StatelessWidget {
/// Creates a compact empty state widget
const AppCompactEmptyState({
super.key,
this.icon = Icons.inbox_outlined,
required this.message,
this.actionText,
this.onActionPressed,
this.padding,
this.semanticLabel,
});
/// Icon to display
final IconData icon;
/// Message text
final String message;
/// Action button text
final String? actionText;
/// Callback for action
final VoidCallback? onActionPressed;
/// Padding around the content
final EdgeInsets? padding;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
Widget content = Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: AppSpacing.iconLG,
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
),
AppSpacing.verticalSpaceMD,
Text(
message,
style: AppTypography.bodyMedium.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (actionText != null && onActionPressed != null) ...[
AppSpacing.verticalSpaceMD,
AppButton(
text: actionText!,
onPressed: onActionPressed,
variant: AppButtonVariant.text,
size: AppButtonSize.small,
),
],
],
);
// Apply padding
content = Padding(
padding: padding ?? const EdgeInsets.all(AppSpacing.lg),
child: content,
);
// Center the content
content = Center(child: content);
// Add semantic label for accessibility
if (semanticLabel != null) {
content = Semantics(
label: semanticLabel,
child: content,
);
}
return content;
}
}
/// Empty state for specific list/grid scenarios
class AppListEmptyState extends StatelessWidget {
/// Creates an empty state for lists
const AppListEmptyState({
super.key,
this.type = AppListEmptyType.noItems,
this.title,
this.message,
this.actionText,
this.onActionPressed,
this.searchQuery,
this.padding,
this.semanticLabel,
});
/// Type of empty state
final AppListEmptyType type;
/// Custom title (overrides default)
final String? title;
/// Custom message (overrides default)
final String? message;
/// Action button text
final String? actionText;
/// Callback for action
final VoidCallback? onActionPressed;
/// Search query for search results empty state
final String? searchQuery;
/// Padding around the content
final EdgeInsets? padding;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final config = _getEmptyStateConfig();
return AppEmptyState(
icon: config.icon,
title: title ?? config.title,
message: message ?? _getMessageWithQuery(config.message),
actionText: actionText ?? config.actionText,
onActionPressed: onActionPressed,
padding: padding,
semanticLabel: semanticLabel,
);
}
/// Get configuration based on type
_EmptyStateConfig _getEmptyStateConfig() {
return switch (type) {
AppListEmptyType.noItems => _EmptyStateConfig(
icon: Icons.inbox_outlined,
title: 'No items',
message: 'There are no items to display.',
actionText: 'Add item',
),
AppListEmptyType.noFavorites => _EmptyStateConfig(
icon: Icons.favorite_border_outlined,
title: 'No favorites',
message: 'Items you mark as favorite will appear here.',
actionText: 'Browse items',
),
AppListEmptyType.noSearchResults => _EmptyStateConfig(
icon: Icons.search_off_outlined,
title: 'No results found',
message: 'No results found for your search.',
actionText: 'Clear search',
),
AppListEmptyType.noNotifications => _EmptyStateConfig(
icon: Icons.notifications_none_outlined,
title: 'No notifications',
message: 'You have no new notifications.',
actionText: null,
),
AppListEmptyType.noMessages => _EmptyStateConfig(
icon: Icons.message_outlined,
title: 'No messages',
message: 'You have no messages yet.',
actionText: 'Start conversation',
),
AppListEmptyType.noHistory => _EmptyStateConfig(
icon: Icons.history,
title: 'No history',
message: 'Your activity history will appear here.',
actionText: null,
),
};
}
/// Get message with search query if applicable
String _getMessageWithQuery(String defaultMessage) {
if (type == AppListEmptyType.noSearchResults && searchQuery != null) {
return 'No results found for "$searchQuery". Try different keywords.';
}
return defaultMessage;
}
}
/// Configuration for empty state
class _EmptyStateConfig {
const _EmptyStateConfig({
required this.icon,
required this.title,
required this.message,
this.actionText,
});
final IconData icon;
final String title;
final String message;
final String? actionText;
}
/// Types of list empty states
enum AppListEmptyType {
/// Generic no items state
noItems,
/// No favorite items
noFavorites,
/// No search results
noSearchResults,
/// No notifications
noNotifications,
/// No messages
noMessages,
/// No history
noHistory,
}

View File

@@ -0,0 +1,579 @@
import 'package:flutter/material.dart';
import '../theme/app_spacing.dart';
import '../theme/app_typography.dart';
import 'app_button.dart';
/// Error display widget with retry action and customizable styling
///
/// Provides consistent error handling UI with support for different error types,
/// retry functionality, and accessibility features.
class AppErrorWidget extends StatelessWidget {
/// Creates an error widget with the specified configuration
const AppErrorWidget({
super.key,
this.error,
this.stackTrace,
this.title,
this.message,
this.icon,
this.onRetry,
this.retryText = 'Retry',
this.secondaryActionText,
this.onSecondaryAction,
this.showDetails = false,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
});
/// Creates a network error widget
const AppErrorWidget.network({
super.key,
this.error,
this.stackTrace,
this.title = 'Network Error',
this.message = 'Please check your internet connection and try again.',
this.onRetry,
this.retryText = 'Retry',
this.secondaryActionText,
this.onSecondaryAction,
this.showDetails = false,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
}) : icon = Icons.wifi_off_outlined;
/// Creates a server error widget
const AppErrorWidget.server({
super.key,
this.error,
this.stackTrace,
this.title = 'Server Error',
this.message = 'Something went wrong on our end. Please try again later.',
this.onRetry,
this.retryText = 'Retry',
this.secondaryActionText,
this.onSecondaryAction,
this.showDetails = false,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
}) : icon = Icons.dns_outlined;
/// Creates a not found error widget
const AppErrorWidget.notFound({
super.key,
this.error,
this.stackTrace,
this.title = 'Not Found',
this.message = 'The requested content could not be found.',
this.onRetry,
this.retryText = 'Go Back',
this.secondaryActionText,
this.onSecondaryAction,
this.showDetails = false,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
}) : icon = Icons.search_off_outlined;
/// Creates a generic error widget
const AppErrorWidget.generic({
super.key,
this.error,
this.stackTrace,
this.title = 'Something went wrong',
this.message = 'An unexpected error occurred. Please try again.',
this.onRetry,
this.retryText = 'Retry',
this.secondaryActionText,
this.onSecondaryAction,
this.showDetails = false,
this.maxWidth = 320,
this.padding,
this.semanticLabel,
}) : icon = Icons.error_outline;
/// The error object (for development/debugging)
final Object? error;
/// Stack trace (for development/debugging)
final StackTrace? stackTrace;
/// Error title
final String? title;
/// Error message
final String? message;
/// Error icon
final IconData? icon;
/// Retry callback
final VoidCallback? onRetry;
/// Retry button text
final String retryText;
/// Secondary action text
final String? secondaryActionText;
/// Secondary action callback
final VoidCallback? onSecondaryAction;
/// Whether to show error details (for debugging)
final bool showDetails;
/// Maximum width of the error content
final double maxWidth;
/// Padding around the content
final EdgeInsets? padding;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final isDebug = _isDebugMode();
Widget content = Column(
mainAxisSize: MainAxisSize.min,
children: [
// Error icon
if (icon != null)
Icon(
icon,
size: AppSpacing.iconXXL * 1.5, // 72dp
color: colorScheme.error.withOpacity(0.8),
),
AppSpacing.verticalSpaceXL,
// Error title
if (title != null)
Text(
title!,
style: AppTypography.headlineSmall.copyWith(
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
// Error message
if (message != null) ...[
if (title != null) AppSpacing.verticalSpaceMD,
Text(
message!,
style: AppTypography.bodyMedium.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
],
// Error details (debug mode only)
if (isDebug && showDetails && error != null) ...[
AppSpacing.verticalSpaceLG,
_buildErrorDetails(context),
],
// Action buttons
if (onRetry != null || onSecondaryAction != null) ...[
AppSpacing.verticalSpaceXXL,
_buildActions(context),
],
],
);
// Apply maximum width constraint
content = ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: content,
);
// Apply padding
content = Padding(
padding: padding ?? const EdgeInsets.all(AppSpacing.screenPadding),
child: content,
);
// Center the content
content = Center(child: content);
// Add semantic label for accessibility
if (semanticLabel != null) {
content = Semantics(
label: semanticLabel,
child: content,
);
}
return content;
}
/// Build action buttons
Widget _buildActions(BuildContext context) {
final List<Widget> actions = [];
// Retry button
if (onRetry != null) {
actions.add(
AppButton(
text: retryText,
onPressed: onRetry,
variant: AppButtonVariant.filled,
isFullWidth: secondaryActionText == null,
),
);
}
// Secondary action
if (secondaryActionText != null && onSecondaryAction != null) {
if (actions.isNotEmpty) {
actions.add(AppSpacing.verticalSpaceMD);
}
actions.add(
AppButton(
text: secondaryActionText!,
onPressed: onSecondaryAction,
variant: AppButtonVariant.outlined,
isFullWidth: true,
),
);
}
return Column(children: actions);
}
/// Build error details for debugging
Widget _buildErrorDetails(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.errorContainer.withOpacity(0.1),
borderRadius: AppSpacing.radiusMD,
border: Border.all(
color: colorScheme.error.withOpacity(0.3),
width: AppSpacing.borderWidth,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.bug_report_outlined,
size: AppSpacing.iconSM,
color: colorScheme.error,
),
AppSpacing.horizontalSpaceXS,
Text(
'Debug Information',
style: AppTypography.labelMedium.copyWith(
color: colorScheme.error,
),
),
],
),
AppSpacing.verticalSpaceXS,
Text(
error.toString(),
style: AppTypography.bodySmall.copyWith(
color: colorScheme.onErrorContainer,
fontFamily: 'monospace',
),
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
if (stackTrace != null) ...[
AppSpacing.verticalSpaceXS,
Text(
'Stack trace available (check console)',
style: AppTypography.bodySmall.copyWith(
color: colorScheme.onErrorContainer.withOpacity(0.7),
fontStyle: FontStyle.italic,
),
),
],
],
),
);
}
/// Check if app is in debug mode
bool _isDebugMode() {
bool inDebugMode = false;
assert(inDebugMode = true);
return inDebugMode;
}
}
/// Compact error widget for smaller spaces
class AppCompactErrorWidget extends StatelessWidget {
/// Creates a compact error widget
const AppCompactErrorWidget({
super.key,
this.error,
this.message = 'An error occurred',
this.onRetry,
this.retryText = 'Retry',
this.padding,
this.semanticLabel,
});
/// The error object
final Object? error;
/// Error message
final String message;
/// Retry callback
final VoidCallback? onRetry;
/// Retry button text
final String retryText;
/// Padding around the content
final EdgeInsets? padding;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
Widget content = Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
size: AppSpacing.iconLG,
color: colorScheme.error.withOpacity(0.8),
),
AppSpacing.verticalSpaceMD,
Text(
message,
style: AppTypography.bodyMedium.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (onRetry != null) ...[
AppSpacing.verticalSpaceMD,
AppButton(
text: retryText,
onPressed: onRetry,
variant: AppButtonVariant.text,
size: AppButtonSize.small,
),
],
],
);
// Apply padding
content = Padding(
padding: padding ?? const EdgeInsets.all(AppSpacing.lg),
child: content,
);
// Center the content
content = Center(child: content);
// Add semantic label for accessibility
if (semanticLabel != null) {
content = Semantics(
label: semanticLabel,
child: content,
);
}
return content;
}
}
/// Inline error widget for form fields and small spaces
class AppInlineErrorWidget extends StatelessWidget {
/// Creates an inline error widget
const AppInlineErrorWidget({
super.key,
required this.message,
this.icon = Icons.error_outline,
this.onRetry,
this.retryText = 'Retry',
this.color,
this.backgroundColor,
this.padding,
this.semanticLabel,
});
/// Error message
final String message;
/// Error icon
final IconData icon;
/// Retry callback
final VoidCallback? onRetry;
/// Retry button text
final String retryText;
/// Error color override
final Color? color;
/// Background color override
final Color? backgroundColor;
/// Padding around the content
final EdgeInsets? padding;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final errorColor = color ?? colorScheme.error;
Widget content = Container(
padding: padding ?? const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: backgroundColor ?? errorColor.withOpacity(0.1),
borderRadius: AppSpacing.radiusSM,
border: Border.all(
color: errorColor.withOpacity(0.3),
width: AppSpacing.borderWidth,
),
),
child: Row(
children: [
Icon(
icon,
size: AppSpacing.iconSM,
color: errorColor,
),
AppSpacing.horizontalSpaceXS,
Expanded(
child: Text(
message,
style: AppTypography.bodySmall.copyWith(
color: errorColor,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (onRetry != null) ...[
AppSpacing.horizontalSpaceSM,
AppButton(
text: retryText,
onPressed: onRetry,
variant: AppButtonVariant.text,
size: AppButtonSize.small,
),
],
],
),
);
// Add semantic label for accessibility
if (semanticLabel != null) {
content = Semantics(
label: semanticLabel,
child: content,
);
}
return content;
}
}
/// Error boundary widget that catches errors in child widgets
class AppErrorBoundary extends StatefulWidget {
/// Creates an error boundary that catches errors in child widgets
const AppErrorBoundary({
super.key,
required this.child,
this.onError,
this.errorWidgetBuilder,
this.fallbackWidget,
});
/// The child widget to wrap with error handling
final Widget child;
/// Callback when an error occurs
final void Function(Object error, StackTrace stackTrace)? onError;
/// Custom error widget builder
final Widget Function(Object error, StackTrace stackTrace)? errorWidgetBuilder;
/// Fallback widget to show when error occurs (used if errorWidgetBuilder is null)
final Widget? fallbackWidget;
@override
State<AppErrorBoundary> createState() => _AppErrorBoundaryState();
}
class _AppErrorBoundaryState extends State<AppErrorBoundary> {
Object? _error;
StackTrace? _stackTrace;
@override
void initState() {
super.initState();
ErrorWidget.builder = (FlutterErrorDetails details) {
if (mounted) {
setState(() {
_error = details.exception;
_stackTrace = details.stack;
});
}
widget.onError?.call(details.exception, details.stack ?? StackTrace.current);
return widget.errorWidgetBuilder?.call(
details.exception,
details.stack ?? StackTrace.current,
) ?? widget.fallbackWidget ?? const AppErrorWidget.generic();
};
}
@override
Widget build(BuildContext context) {
if (_error != null) {
return widget.errorWidgetBuilder?.call(_error!, _stackTrace!) ??
widget.fallbackWidget ??
AppErrorWidget.generic(
error: _error,
stackTrace: _stackTrace,
onRetry: _handleRetry,
);
}
return widget.child;
}
void _handleRetry() {
setState(() {
_error = null;
_stackTrace = null;
});
}
}

View File

@@ -0,0 +1,529 @@
import 'package:flutter/material.dart';
import '../theme/app_spacing.dart';
import '../theme/app_typography.dart';
/// Various loading states including circular, linear, and skeleton loaders
///
/// Provides consistent loading indicators with customizable styling
/// and accessibility features.
class AppLoadingIndicator extends StatelessWidget {
/// Creates a loading indicator with the specified type and styling
const AppLoadingIndicator({
super.key,
this.type = AppLoadingType.circular,
this.size = AppLoadingSize.medium,
this.color,
this.backgroundColor,
this.strokeWidth,
this.value,
this.message,
this.semanticLabel,
});
/// The type of loading indicator to display
final AppLoadingType type;
/// The size of the loading indicator
final AppLoadingSize size;
/// Color override for the indicator
final Color? color;
/// Background color override (for linear progress)
final Color? backgroundColor;
/// Stroke width override (for circular progress)
final double? strokeWidth;
/// Progress value (0.0 to 1.0) for determinate progress indicators
final double? value;
/// Optional message to display below the indicator
final String? message;
/// Semantic label for accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
Widget indicator = switch (type) {
AppLoadingType.circular => _buildCircularIndicator(colorScheme),
AppLoadingType.linear => _buildLinearIndicator(colorScheme),
AppLoadingType.adaptive => _buildAdaptiveIndicator(context, colorScheme),
AppLoadingType.refresh => _buildRefreshIndicator(colorScheme),
};
// Add message if provided
if (message != null) {
indicator = Column(
mainAxisSize: MainAxisSize.min,
children: [
indicator,
AppSpacing.verticalSpaceMD,
Text(
message!,
style: AppTypography.bodyMedium.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
);
}
// Add semantic label for accessibility
if (semanticLabel != null) {
indicator = Semantics(
label: semanticLabel,
child: indicator,
);
}
return indicator;
}
/// Build circular progress indicator
Widget _buildCircularIndicator(ColorScheme colorScheme) {
final indicatorSize = _getIndicatorSize();
return SizedBox(
width: indicatorSize,
height: indicatorSize,
child: CircularProgressIndicator(
value: value,
color: color ?? colorScheme.primary,
backgroundColor: backgroundColor,
strokeWidth: strokeWidth ?? _getStrokeWidth(),
),
);
}
/// Build linear progress indicator
Widget _buildLinearIndicator(ColorScheme colorScheme) {
return LinearProgressIndicator(
value: value,
color: color ?? colorScheme.primary,
backgroundColor: backgroundColor ?? colorScheme.surfaceContainerHighest,
minHeight: _getLinearHeight(),
);
}
/// Build adaptive progress indicator (Material on Android, Cupertino on iOS)
Widget _buildAdaptiveIndicator(BuildContext context, ColorScheme colorScheme) {
final indicatorSize = _getIndicatorSize();
return SizedBox(
width: indicatorSize,
height: indicatorSize,
child: CircularProgressIndicator.adaptive(
value: value,
backgroundColor: backgroundColor,
strokeWidth: strokeWidth ?? _getStrokeWidth(),
),
);
}
/// Build refresh indicator style circular indicator
Widget _buildRefreshIndicator(ColorScheme colorScheme) {
final indicatorSize = _getIndicatorSize();
return SizedBox(
width: indicatorSize,
height: indicatorSize,
child: RefreshProgressIndicator(
value: value,
color: color ?? colorScheme.primary,
backgroundColor: backgroundColor ?? colorScheme.surface,
strokeWidth: strokeWidth ?? _getStrokeWidth(),
),
);
}
/// Get indicator size based on size enum
double _getIndicatorSize() {
return switch (size) {
AppLoadingSize.small => AppSpacing.iconMD,
AppLoadingSize.medium => AppSpacing.iconLG,
AppLoadingSize.large => AppSpacing.iconXL,
AppLoadingSize.extraLarge => AppSpacing.iconXXL,
};
}
/// Get stroke width based on size
double _getStrokeWidth() {
return switch (size) {
AppLoadingSize.small => 2.0,
AppLoadingSize.medium => 3.0,
AppLoadingSize.large => 4.0,
AppLoadingSize.extraLarge => 5.0,
};
}
/// Get linear indicator height based on size
double _getLinearHeight() {
return switch (size) {
AppLoadingSize.small => 2.0,
AppLoadingSize.medium => 4.0,
AppLoadingSize.large => 6.0,
AppLoadingSize.extraLarge => 8.0,
};
}
}
/// Available loading indicator types
enum AppLoadingType {
/// Circular progress indicator
circular,
/// Linear progress indicator
linear,
/// Adaptive indicator (platform-specific)
adaptive,
/// Refresh indicator style
refresh,
}
/// Available loading indicator sizes
enum AppLoadingSize {
/// Small indicator (24dp)
small,
/// Medium indicator (32dp) - default
medium,
/// Large indicator (40dp)
large,
/// Extra large indicator (48dp)
extraLarge,
}
/// Skeleton loader for content placeholders
class AppSkeletonLoader extends StatefulWidget {
/// Creates a skeleton loader with the specified dimensions and styling
const AppSkeletonLoader({
super.key,
this.width,
this.height = 16.0,
this.borderRadius,
this.baseColor,
this.highlightColor,
this.animationDuration = const Duration(milliseconds: 1500),
});
/// Creates a rectangular skeleton loader
const AppSkeletonLoader.rectangle({
super.key,
required this.width,
required this.height,
this.borderRadius,
this.baseColor,
this.highlightColor,
this.animationDuration = const Duration(milliseconds: 1500),
});
/// Creates a circular skeleton loader (avatar placeholder)
AppSkeletonLoader.circle({
super.key,
required double diameter,
this.baseColor,
this.highlightColor,
this.animationDuration = const Duration(milliseconds: 1500),
}) : width = diameter,
height = diameter,
borderRadius = BorderRadius.circular(diameter / 2.0);
/// Creates a text line skeleton loader
const AppSkeletonLoader.text({
super.key,
this.width,
this.height = 16.0,
this.baseColor,
this.highlightColor,
this.animationDuration = const Duration(milliseconds: 1500),
}) : borderRadius = const BorderRadius.all(Radius.circular(8.0));
/// Width of the skeleton loader
final double? width;
/// Height of the skeleton loader
final double height;
/// Border radius for rounded corners
final BorderRadius? borderRadius;
/// Base color for the skeleton
final Color? baseColor;
/// Highlight color for the animation
final Color? highlightColor;
/// Duration of the shimmer animation
final Duration animationDuration;
@override
State<AppSkeletonLoader> createState() => _AppSkeletonLoaderState();
}
class _AppSkeletonLoaderState extends State<AppSkeletonLoader>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: widget.animationDuration,
vsync: this,
);
_animation = Tween<double>(begin: -1.0, end: 2.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.ease),
);
_animationController.repeat();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final baseColor = widget.baseColor ??
(theme.brightness == Brightness.light
? colorScheme.surfaceContainerHighest
: colorScheme.surfaceContainerLowest);
final highlightColor = widget.highlightColor ??
(theme.brightness == Brightness.light
? colorScheme.surface
: colorScheme.surfaceContainerHigh);
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
borderRadius: widget.borderRadius ?? AppSpacing.radiusSM,
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
baseColor,
highlightColor,
baseColor,
],
stops: [
0.0,
0.5,
1.0,
],
transform: GradientRotation(_animation.value),
),
),
);
},
);
}
}
/// Skeleton loader for list items
class AppListItemSkeleton extends StatelessWidget {
/// Creates a skeleton loader for list items with avatar and text
const AppListItemSkeleton({
super.key,
this.hasAvatar = true,
this.hasSubtitle = true,
this.hasTrailing = false,
this.padding,
});
/// Whether to show avatar placeholder
final bool hasAvatar;
/// Whether to show subtitle placeholder
final bool hasSubtitle;
/// Whether to show trailing element placeholder
final bool hasTrailing;
/// Padding around the skeleton
final EdgeInsets? padding;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding ?? const EdgeInsets.all(AppSpacing.md),
child: Row(
children: [
if (hasAvatar) ...[
AppSkeletonLoader.circle(diameter: 40.0),
AppSpacing.horizontalSpaceMD,
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const AppSkeletonLoader.text(width: double.infinity),
if (hasSubtitle) ...[
AppSpacing.verticalSpaceXS,
const AppSkeletonLoader.text(width: 200),
],
],
),
),
if (hasTrailing) ...[
AppSpacing.horizontalSpaceMD,
const AppSkeletonLoader.rectangle(width: 60, height: 32),
],
],
),
);
}
}
/// Skeleton loader for card content
class AppCardSkeleton extends StatelessWidget {
/// Creates a skeleton loader for card content
const AppCardSkeleton({
super.key,
this.hasImage = false,
this.hasTitle = true,
this.hasSubtitle = true,
this.hasContent = true,
this.hasActions = false,
this.padding,
});
/// Whether to show image placeholder
final bool hasImage;
/// Whether to show title placeholder
final bool hasTitle;
/// Whether to show subtitle placeholder
final bool hasSubtitle;
/// Whether to show content placeholder
final bool hasContent;
/// Whether to show action buttons placeholder
final bool hasActions;
/// Padding around the skeleton
final EdgeInsets? padding;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding ?? const EdgeInsets.all(AppSpacing.cardPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (hasImage) ...[
const AppSkeletonLoader.rectangle(
width: double.infinity,
height: 200,
),
AppSpacing.verticalSpaceMD,
],
if (hasTitle) ...[
const AppSkeletonLoader.text(width: 250),
AppSpacing.verticalSpaceXS,
],
if (hasSubtitle) ...[
const AppSkeletonLoader.text(width: 180),
AppSpacing.verticalSpaceMD,
],
if (hasContent) ...[
const AppSkeletonLoader.text(width: double.infinity),
AppSpacing.verticalSpaceXS,
const AppSkeletonLoader.text(width: double.infinity),
AppSpacing.verticalSpaceXS,
const AppSkeletonLoader.text(width: 300),
],
if (hasActions) ...[
AppSpacing.verticalSpaceMD,
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const AppSkeletonLoader.rectangle(width: 80, height: 36),
AppSpacing.horizontalSpaceSM,
const AppSkeletonLoader.rectangle(width: 80, height: 36),
],
),
],
],
),
);
}
}
/// Loading overlay that can be displayed over content
class AppLoadingOverlay extends StatelessWidget {
/// Creates a loading overlay with the specified content and styling
const AppLoadingOverlay({
super.key,
required this.isLoading,
required this.child,
this.loadingWidget,
this.backgroundColor,
this.message,
this.dismissible = false,
});
/// Whether the loading overlay should be displayed
final bool isLoading;
/// The content to display behind the overlay
final Widget child;
/// Custom loading widget (defaults to circular indicator)
final Widget? loadingWidget;
/// Background color of the overlay
final Color? backgroundColor;
/// Optional message to display with the loading indicator
final String? message;
/// Whether the overlay can be dismissed by tapping
final bool dismissible;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Stack(
children: [
child,
if (isLoading)
Positioned.fill(
child: Container(
color: backgroundColor ??
theme.colorScheme.surface.withOpacity(0.8),
child: Center(
child: loadingWidget ??
AppLoadingIndicator(
message: message,
semanticLabel: 'Loading content',
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,621 @@
import 'package:flutter/material.dart';
import '../theme/app_spacing.dart';
import '../theme/app_typography.dart';
/// Snackbar utilities for consistent notifications and user feedback
///
/// Provides easy-to-use static methods for showing different types of
/// snackbars with Material 3 styling and customization options.
class AppSnackbar {
// Prevent instantiation
AppSnackbar._();
/// Show a basic snackbar
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> show({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 4),
SnackBarAction? action,
Color? backgroundColor,
Color? textColor,
double? elevation,
EdgeInsets? margin,
ShapeBorder? shape,
SnackBarBehavior behavior = SnackBarBehavior.floating,
bool showCloseIcon = false,
VoidCallback? onVisible,
}) {
final theme = Theme.of(context);
final snackBarTheme = theme.snackBarTheme;
final snackBar = SnackBar(
content: Text(
message,
style: AppTypography.bodyMedium.copyWith(
color: textColor ?? snackBarTheme.contentTextStyle?.color,
),
),
duration: duration,
action: action,
backgroundColor: backgroundColor ?? snackBarTheme.backgroundColor,
elevation: elevation ?? snackBarTheme.elevation,
margin: margin ?? _getDefaultMargin(context),
shape: shape ?? snackBarTheme.shape,
behavior: behavior,
showCloseIcon: showCloseIcon,
onVisible: onVisible,
);
return ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
/// Show a success snackbar
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSuccess({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 3),
SnackBarAction? action,
bool showCloseIcon = false,
VoidCallback? onVisible,
}) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final successColor = _getSuccessColor(theme);
return _showWithIcon(
context: context,
message: message,
icon: Icons.check_circle_outline,
backgroundColor: successColor,
textColor: _getOnColor(successColor),
iconColor: _getOnColor(successColor),
duration: duration,
action: action,
showCloseIcon: showCloseIcon,
onVisible: onVisible,
);
}
/// Show an error snackbar
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showError({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 5),
SnackBarAction? action,
bool showCloseIcon = true,
VoidCallback? onVisible,
}) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return _showWithIcon(
context: context,
message: message,
icon: Icons.error_outline,
backgroundColor: colorScheme.error,
textColor: colorScheme.onError,
iconColor: colorScheme.onError,
duration: duration,
action: action,
showCloseIcon: showCloseIcon,
onVisible: onVisible,
);
}
/// Show a warning snackbar
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showWarning({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 4),
SnackBarAction? action,
bool showCloseIcon = false,
VoidCallback? onVisible,
}) {
final theme = Theme.of(context);
final warningColor = _getWarningColor(theme);
return _showWithIcon(
context: context,
message: message,
icon: Icons.warning_outlined,
backgroundColor: warningColor,
textColor: _getOnColor(warningColor),
iconColor: _getOnColor(warningColor),
duration: duration,
action: action,
showCloseIcon: showCloseIcon,
onVisible: onVisible,
);
}
/// Show an info snackbar
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showInfo({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 4),
SnackBarAction? action,
bool showCloseIcon = false,
VoidCallback? onVisible,
}) {
final theme = Theme.of(context);
final infoColor = _getInfoColor(theme);
return _showWithIcon(
context: context,
message: message,
icon: Icons.info_outline,
backgroundColor: infoColor,
textColor: _getOnColor(infoColor),
iconColor: _getOnColor(infoColor),
duration: duration,
action: action,
showCloseIcon: showCloseIcon,
onVisible: onVisible,
);
}
/// Show a loading snackbar
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showLoading({
required BuildContext context,
String message = 'Loading...',
Duration duration = const Duration(seconds: 30),
bool showCloseIcon = false,
VoidCallback? onVisible,
}) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final snackBar = SnackBar(
content: Row(
children: [
SizedBox(
width: AppSpacing.iconSM,
height: AppSpacing.iconSM,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
colorScheme.onSurfaceVariant,
),
),
),
AppSpacing.horizontalSpaceMD,
Expanded(
child: Text(
message,
style: AppTypography.bodyMedium.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
],
),
duration: duration,
backgroundColor: colorScheme.surfaceContainerHighest,
margin: _getDefaultMargin(context),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusSM,
),
showCloseIcon: showCloseIcon,
onVisible: onVisible,
);
return ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
/// Show an action snackbar with custom button
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showAction({
required BuildContext context,
required String message,
required String actionLabel,
required VoidCallback onActionPressed,
Duration duration = const Duration(seconds: 6),
Color? backgroundColor,
Color? textColor,
Color? actionColor,
bool showCloseIcon = false,
VoidCallback? onVisible,
}) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return show(
context: context,
message: message,
duration: duration,
backgroundColor: backgroundColor,
textColor: textColor,
showCloseIcon: showCloseIcon,
onVisible: onVisible,
action: SnackBarAction(
label: actionLabel,
onPressed: onActionPressed,
textColor: actionColor ?? colorScheme.inversePrimary,
),
);
}
/// Show an undo snackbar
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showUndo({
required BuildContext context,
required String message,
required VoidCallback onUndo,
String undoLabel = 'Undo',
Duration duration = const Duration(seconds: 5),
VoidCallback? onVisible,
}) {
return showAction(
context: context,
message: message,
actionLabel: undoLabel,
onActionPressed: onUndo,
duration: duration,
onVisible: onVisible,
);
}
/// Show a retry snackbar
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showRetry({
required BuildContext context,
String message = 'Something went wrong',
required VoidCallback onRetry,
String retryLabel = 'Retry',
Duration duration = const Duration(seconds: 6),
VoidCallback? onVisible,
}) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return _showWithIcon(
context: context,
message: message,
icon: Icons.refresh,
backgroundColor: colorScheme.errorContainer,
textColor: colorScheme.onErrorContainer,
iconColor: colorScheme.onErrorContainer,
duration: duration,
action: SnackBarAction(
label: retryLabel,
onPressed: onRetry,
textColor: colorScheme.error,
),
onVisible: onVisible,
);
}
/// Show a custom snackbar with icon
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showCustom({
required BuildContext context,
required String message,
IconData? icon,
Color? iconColor,
Color? backgroundColor,
Color? textColor,
Duration duration = const Duration(seconds: 4),
SnackBarAction? action,
bool showCloseIcon = false,
VoidCallback? onVisible,
}) {
if (icon != null) {
return _showWithIcon(
context: context,
message: message,
icon: icon,
backgroundColor: backgroundColor,
textColor: textColor,
iconColor: iconColor,
duration: duration,
action: action,
showCloseIcon: showCloseIcon,
onVisible: onVisible,
);
}
return show(
context: context,
message: message,
backgroundColor: backgroundColor,
textColor: textColor,
duration: duration,
action: action,
showCloseIcon: showCloseIcon,
onVisible: onVisible,
);
}
/// Hide current snackbar
static void hide(BuildContext context) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
}
/// Remove all snackbars
static void removeAll(BuildContext context) {
ScaffoldMessenger.of(context).clearSnackBars();
}
// Helper methods
/// Show snackbar with icon
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> _showWithIcon({
required BuildContext context,
required String message,
required IconData icon,
Color? backgroundColor,
Color? textColor,
Color? iconColor,
Duration duration = const Duration(seconds: 4),
SnackBarAction? action,
bool showCloseIcon = false,
VoidCallback? onVisible,
}) {
final theme = Theme.of(context);
final snackBarTheme = theme.snackBarTheme;
final snackBar = SnackBar(
content: Row(
children: [
Icon(
icon,
size: AppSpacing.iconSM,
color: iconColor ?? textColor,
),
AppSpacing.horizontalSpaceMD,
Expanded(
child: Text(
message,
style: AppTypography.bodyMedium.copyWith(
color: textColor ?? snackBarTheme.contentTextStyle?.color,
),
),
),
],
),
duration: duration,
action: action,
backgroundColor: backgroundColor ?? snackBarTheme.backgroundColor,
elevation: snackBarTheme.elevation,
margin: _getDefaultMargin(context),
shape: snackBarTheme.shape,
behavior: SnackBarBehavior.floating,
showCloseIcon: showCloseIcon,
onVisible: onVisible,
);
return ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
/// Get default margin for floating snackbars
static EdgeInsets _getDefaultMargin(BuildContext context) {
return const EdgeInsets.all(AppSpacing.md);
}
/// Get success color from theme extension or fallback
static Color _getSuccessColor(ThemeData theme) {
return const Color(0xFF4CAF50); // Material Green 500
}
/// Get warning color from theme extension or fallback
static Color _getWarningColor(ThemeData theme) {
return const Color(0xFFFF9800); // Material Orange 500
}
/// Get info color from theme extension or fallback
static Color _getInfoColor(ThemeData theme) {
return theme.colorScheme.primary;
}
/// Get appropriate "on" color for given background color
static Color _getOnColor(Color backgroundColor) {
// Simple calculation based on luminance
return backgroundColor.computeLuminance() > 0.5
? Colors.black87
: Colors.white;
}
}
/// Custom snackbar widget for more complex layouts
class AppCustomSnackbar extends StatelessWidget {
/// Creates a custom snackbar widget
const AppCustomSnackbar({
super.key,
required this.child,
this.backgroundColor,
this.elevation,
this.margin,
this.shape,
this.behavior = SnackBarBehavior.floating,
this.width,
this.padding,
});
/// The content to display in the snackbar
final Widget child;
/// Background color override
final Color? backgroundColor;
/// Elevation override
final double? elevation;
/// Margin around the snackbar
final EdgeInsets? margin;
/// Shape override
final ShapeBorder? shape;
/// Snackbar behavior
final SnackBarBehavior behavior;
/// Fixed width override
final double? width;
/// Content padding
final EdgeInsets? padding;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final snackBarTheme = theme.snackBarTheme;
Widget content = Container(
width: width,
padding: padding ?? const EdgeInsets.all(AppSpacing.md),
child: child,
);
return SnackBar(
content: content,
backgroundColor: backgroundColor ?? snackBarTheme.backgroundColor,
elevation: elevation ?? snackBarTheme.elevation,
margin: margin ?? const EdgeInsets.all(AppSpacing.md),
shape: shape ?? snackBarTheme.shape,
behavior: behavior,
padding: EdgeInsets.zero, // Remove default padding since we handle it
);
}
/// Show this custom snackbar
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> show({
required BuildContext context,
Duration duration = const Duration(seconds: 4),
}) {
return ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: child,
backgroundColor: backgroundColor,
elevation: elevation,
margin: margin ?? const EdgeInsets.all(AppSpacing.md),
shape: shape,
behavior: behavior,
duration: duration,
padding: EdgeInsets.zero,
),
);
}
}
/// Snackbar with rich content (title, subtitle, actions)
class AppRichSnackbar extends StatelessWidget {
/// Creates a rich snackbar with title, subtitle, and actions
const AppRichSnackbar({
super.key,
required this.title,
this.subtitle,
this.icon,
this.actions,
this.backgroundColor,
this.onTap,
});
/// Main title text
final String title;
/// Optional subtitle text
final String? subtitle;
/// Optional leading icon
final IconData? icon;
/// Optional action widgets
final List<Widget>? actions;
/// Background color override
final Color? backgroundColor;
/// Tap callback for the entire snackbar
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
Widget content = Row(
children: [
if (icon != null) ...[
Icon(
icon,
size: AppSpacing.iconMD,
color: backgroundColor != null
? _getOnColor(backgroundColor!)
: colorScheme.onSurfaceVariant,
),
AppSpacing.horizontalSpaceMD,
],
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.titleSmall.copyWith(
color: backgroundColor != null
? _getOnColor(backgroundColor!)
: colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (subtitle != null) ...[
AppSpacing.verticalSpaceXS,
Text(
subtitle!,
style: AppTypography.bodySmall.copyWith(
color: backgroundColor != null
? _getOnColor(backgroundColor!).withOpacity(0.8)
: colorScheme.onSurfaceVariant.withOpacity(0.8),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
if (actions != null && actions!.isNotEmpty) ...[
AppSpacing.horizontalSpaceSM,
Row(children: actions!),
],
],
);
if (onTap != null) {
content = InkWell(
onTap: onTap,
borderRadius: AppSpacing.radiusSM,
child: content,
);
}
return AppCustomSnackbar(
backgroundColor: backgroundColor,
child: content,
);
}
/// Show this rich snackbar
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> show({
required BuildContext context,
Duration duration = const Duration(seconds: 5),
}) {
return ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: this,
duration: duration,
backgroundColor: Colors.transparent,
elevation: 0,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(AppSpacing.md),
padding: EdgeInsets.zero,
),
);
}
/// Get appropriate "on" color for given background color
static Color _getOnColor(Color backgroundColor) {
return backgroundColor.computeLuminance() > 0.5
? Colors.black87
: Colors.white;
}
}

View File

@@ -0,0 +1,463 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_spacing.dart';
import '../theme/app_typography.dart';
/// Text input field with validation, error handling, and Material 3 styling
///
/// Provides comprehensive form input functionality with built-in validation,
/// accessibility support, and consistent theming.
class AppTextField extends StatefulWidget {
/// Creates a text input field with the specified configuration
const AppTextField({
super.key,
this.controller,
this.initialValue,
this.labelText,
this.hintText,
this.helperText,
this.errorText,
this.prefixIcon,
this.suffixIcon,
this.onChanged,
this.onSubmitted,
this.onTap,
this.validator,
this.enabled = true,
this.readOnly = false,
this.obscureText = false,
this.autocorrect = true,
this.enableSuggestions = true,
this.maxLines = 1,
this.minLines,
this.maxLength,
this.keyboardType,
this.textInputAction,
this.inputFormatters,
this.focusNode,
this.textCapitalization = TextCapitalization.none,
this.textAlign = TextAlign.start,
this.style,
this.filled,
this.fillColor,
this.borderRadius,
this.contentPadding,
this.semanticLabel,
});
/// Controls the text being edited
final TextEditingController? controller;
/// Initial value for the field (if no controller provided)
final String? initialValue;
/// Label text displayed above the field
final String? labelText;
/// Hint text displayed when field is empty
final String? hintText;
/// Helper text displayed below the field
final String? helperText;
/// Error text displayed below the field (overrides helper text)
final String? errorText;
/// Icon displayed at the start of the field
final Widget? prefixIcon;
/// Icon displayed at the end of the field
final Widget? suffixIcon;
/// Called when the field value changes
final ValueChanged<String>? onChanged;
/// Called when the field is submitted
final ValueChanged<String>? onSubmitted;
/// Called when the field is tapped
final VoidCallback? onTap;
/// Validator function for form validation
final FormFieldValidator<String>? validator;
/// Whether the field is enabled
final bool enabled;
/// Whether the field is read-only
final bool readOnly;
/// Whether to obscure the text (for passwords)
final bool obscureText;
/// Whether to enable autocorrect
final bool autocorrect;
/// Whether to enable input suggestions
final bool enableSuggestions;
/// Maximum number of lines
final int? maxLines;
/// Minimum number of lines
final int? minLines;
/// Maximum character length
final int? maxLength;
/// Keyboard type for input
final TextInputType? keyboardType;
/// Text input action for the keyboard
final TextInputAction? textInputAction;
/// Input formatters to apply
final List<TextInputFormatter>? inputFormatters;
/// Focus node for the field
final FocusNode? focusNode;
/// Text capitalization behavior
final TextCapitalization textCapitalization;
/// Text alignment within the field
final TextAlign textAlign;
/// Text style override
final TextStyle? style;
/// Whether the field should be filled
final bool? filled;
/// Fill color override
final Color? fillColor;
/// Border radius override
final BorderRadius? borderRadius;
/// Content padding override
final EdgeInsets? contentPadding;
/// Semantic label for accessibility
final String? semanticLabel;
@override
State<AppTextField> createState() => _AppTextFieldState();
}
class _AppTextFieldState extends State<AppTextField> {
late TextEditingController _controller;
late FocusNode _focusNode;
bool _obscureText = false;
bool _isFocused = false;
@override
void initState() {
super.initState();
_controller = widget.controller ?? TextEditingController(text: widget.initialValue);
_focusNode = widget.focusNode ?? FocusNode();
_obscureText = widget.obscureText;
_focusNode.addListener(_onFocusChange);
}
@override
void dispose() {
if (widget.controller == null) {
_controller.dispose();
}
if (widget.focusNode == null) {
_focusNode.dispose();
}
super.dispose();
}
void _onFocusChange() {
setState(() {
_isFocused = _focusNode.hasFocus;
});
}
void _toggleObscureText() {
setState(() {
_obscureText = !_obscureText;
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
// Build suffix icon with password visibility toggle if needed
Widget? suffixIcon = widget.suffixIcon;
if (widget.obscureText) {
suffixIcon = IconButton(
icon: Icon(
_obscureText ? Icons.visibility : Icons.visibility_off,
size: AppSpacing.iconMD,
),
onPressed: _toggleObscureText,
tooltip: _obscureText ? 'Show password' : 'Hide password',
);
}
// Create input decoration
final inputDecoration = InputDecoration(
labelText: widget.labelText,
hintText: widget.hintText,
helperText: widget.helperText,
errorText: widget.errorText,
prefixIcon: widget.prefixIcon,
suffixIcon: suffixIcon,
filled: widget.filled ?? theme.inputDecorationTheme.filled,
fillColor: widget.fillColor ??
(widget.enabled
? theme.inputDecorationTheme.fillColor
: colorScheme.surface.withOpacity(0.1)),
contentPadding: widget.contentPadding ??
theme.inputDecorationTheme.contentPadding,
border: _createBorder(theme, null),
enabledBorder: _createBorder(theme, colorScheme.outline),
focusedBorder: _createBorder(theme, colorScheme.primary),
errorBorder: _createBorder(theme, colorScheme.error),
focusedErrorBorder: _createBorder(theme, colorScheme.error),
disabledBorder: _createBorder(theme, colorScheme.outline.withOpacity(0.38)),
labelStyle: theme.inputDecorationTheme.labelStyle?.copyWith(
color: _getLabelColor(theme, colorScheme),
),
hintStyle: widget.style ?? theme.inputDecorationTheme.hintStyle,
errorStyle: theme.inputDecorationTheme.errorStyle,
helperStyle: theme.inputDecorationTheme.helperStyle,
counterStyle: AppTypography.bodySmall.copyWith(
color: colorScheme.onSurfaceVariant,
),
);
// Create the text field
Widget textField = TextFormField(
controller: _controller,
focusNode: _focusNode,
decoration: inputDecoration,
enabled: widget.enabled,
readOnly: widget.readOnly,
obscureText: _obscureText,
autocorrect: widget.autocorrect,
enableSuggestions: widget.enableSuggestions,
maxLines: widget.maxLines,
minLines: widget.minLines,
maxLength: widget.maxLength,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
inputFormatters: widget.inputFormatters,
textCapitalization: widget.textCapitalization,
textAlign: widget.textAlign,
style: widget.style ?? theme.inputDecorationTheme.labelStyle,
validator: widget.validator,
onChanged: widget.onChanged,
onFieldSubmitted: widget.onSubmitted,
onTap: widget.onTap,
);
// Add semantic label if provided
if (widget.semanticLabel != null) {
textField = Semantics(
label: widget.semanticLabel,
child: textField,
);
}
return textField;
}
/// Create input border with custom styling
InputBorder _createBorder(ThemeData theme, Color? borderColor) {
return OutlineInputBorder(
borderRadius: widget.borderRadius ?? AppSpacing.fieldRadius,
borderSide: BorderSide(
color: borderColor ?? Colors.transparent,
width: _isFocused ? AppSpacing.borderWidthThick : AppSpacing.borderWidth,
),
);
}
/// Get appropriate label color based on state
Color _getLabelColor(ThemeData theme, ColorScheme colorScheme) {
if (!widget.enabled) {
return colorScheme.onSurface.withOpacity(0.38);
}
if (widget.errorText != null) {
return colorScheme.error;
}
if (_isFocused) {
return colorScheme.primary;
}
return colorScheme.onSurfaceVariant;
}
}
/// Email validation text field
class AppEmailField extends StatelessWidget {
const AppEmailField({
super.key,
this.controller,
this.labelText = 'Email',
this.hintText = 'Enter your email address',
this.onChanged,
this.validator,
this.enabled = true,
this.readOnly = false,
});
final TextEditingController? controller;
final String labelText;
final String hintText;
final ValueChanged<String>? onChanged;
final FormFieldValidator<String>? validator;
final bool enabled;
final bool readOnly;
@override
Widget build(BuildContext context) {
return AppTextField(
controller: controller,
labelText: labelText,
hintText: hintText,
prefixIcon: const Icon(Icons.email_outlined),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
autocorrect: false,
enableSuggestions: false,
textCapitalization: TextCapitalization.none,
onChanged: onChanged,
validator: validator ?? _defaultEmailValidator,
enabled: enabled,
readOnly: readOnly,
semanticLabel: 'Email address input field',
);
}
String? _defaultEmailValidator(String? value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'Enter a valid email address';
}
return null;
}
}
/// Password validation text field
class AppPasswordField extends StatelessWidget {
const AppPasswordField({
super.key,
this.controller,
this.labelText = 'Password',
this.hintText = 'Enter your password',
this.onChanged,
this.validator,
this.enabled = true,
this.readOnly = false,
this.requireStrong = false,
});
final TextEditingController? controller;
final String labelText;
final String hintText;
final ValueChanged<String>? onChanged;
final FormFieldValidator<String>? validator;
final bool enabled;
final bool readOnly;
final bool requireStrong;
@override
Widget build(BuildContext context) {
return AppTextField(
controller: controller,
labelText: labelText,
hintText: hintText,
prefixIcon: const Icon(Icons.lock_outline),
obscureText: true,
textInputAction: TextInputAction.done,
autocorrect: false,
enableSuggestions: false,
onChanged: onChanged,
validator: validator ?? (requireStrong ? _strongPasswordValidator : _defaultPasswordValidator),
enabled: enabled,
readOnly: readOnly,
semanticLabel: 'Password input field',
);
}
String? _defaultPasswordValidator(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
}
String? _strongPasswordValidator(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Password must be at least 8 characters';
}
if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(value)) {
return 'Password must contain uppercase, lowercase, and number';
}
return null;
}
}
/// Search text field with built-in search functionality
class AppSearchField extends StatelessWidget {
const AppSearchField({
super.key,
this.controller,
this.hintText = 'Search...',
this.onChanged,
this.onSubmitted,
this.onClear,
this.enabled = true,
this.autofocus = false,
});
final TextEditingController? controller;
final String hintText;
final ValueChanged<String>? onChanged;
final ValueChanged<String>? onSubmitted;
final VoidCallback? onClear;
final bool enabled;
final bool autofocus;
@override
Widget build(BuildContext context) {
return AppTextField(
controller: controller,
hintText: hintText,
prefixIcon: const Icon(Icons.search),
suffixIcon: controller?.text.isNotEmpty == true
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
controller?.clear();
onClear?.call();
},
tooltip: 'Clear search',
)
: null,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
onChanged: onChanged,
onSubmitted: onSubmitted,
enabled: enabled,
semanticLabel: 'Search input field',
);
}
}

View File

@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
/// A customizable error display widget
class AppErrorWidget extends StatelessWidget {
final String message;
final String? title;
final VoidCallback? onRetry;
final IconData? icon;
const AppErrorWidget({
super.key,
required this.message,
this.title,
this.onRetry,
this.icon,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon ?? Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
if (title != null) ...[
Text(
title!,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
],
Text(
message,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
if (onRetry != null) ...[
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(120, 40),
),
),
],
],
),
),
);
}
}
/// A compact error widget for inline use
class InlineErrorWidget extends StatelessWidget {
final String message;
final VoidCallback? onRetry;
const InlineErrorWidget({
super.key,
required this.message,
this.onRetry,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.error.withOpacity(0.2),
),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.error,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
if (onRetry != null) ...[
const SizedBox(width: 8),
TextButton(
onPressed: onRetry,
style: TextButton.styleFrom(
minimumSize: const Size(60, 32),
padding: const EdgeInsets.symmetric(horizontal: 12),
),
child: Text(
'Retry',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
),
),
],
],
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
/// A customizable loading widget
class LoadingWidget extends StatelessWidget {
final String? message;
final double size;
final Color? color;
const LoadingWidget({
super.key,
this.message,
this.size = 24.0,
this.color,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
color: color ?? Theme.of(context).colorScheme.primary,
strokeWidth: 3.0,
),
),
if (message != null) ...[
const SizedBox(height: 16),
Text(
message!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
],
),
);
}
}
/// A small loading indicator for buttons
class SmallLoadingIndicator extends StatelessWidget {
final Color? color;
final double size;
const SmallLoadingIndicator({
super.key,
this.color,
this.size = 16.0,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
color: color ?? Theme.of(context).colorScheme.onPrimary,
strokeWidth: 2.0,
),
);
}
}

View File

@@ -0,0 +1,3 @@
// Barrel export file for core widgets
export 'error_widget.dart';
export 'loading_widget.dart';

View File

@@ -0,0 +1,269 @@
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isPasswordVisible = false;
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
final emailRegExp = RegExp(r'^[^@]+@[^@]+\.[^@]+');
if (!emailRegExp.hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
if (value.length < 6) {
return 'Password must be at least 6 characters long';
}
return null;
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
// Simulate API call
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
// Simple demo login - accept any valid email/password
if (_emailController.text.isNotEmpty && _passwordController.text.length >= 6) {
// Navigate to home screen (will be implemented when routing is set up)
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login successful!'),
backgroundColor: Colors.green,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login failed. Please check your credentials.'),
backgroundColor: Colors.red,
),
);
}
setState(() {
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// App Logo/Title
Icon(
Icons.lock_outline,
size: 80,
color: colorScheme.primary,
),
const SizedBox(height: 24),
// Welcome Text
Text(
'Welcome Back',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Sign in to your account',
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
// Email Field
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
decoration: InputDecoration(
labelText: 'Email',
hintText: 'Enter your email address',
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: colorScheme.surfaceVariant.withOpacity(0.3),
),
),
const SizedBox(height: 16),
// Password Field
TextFormField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
textInputAction: TextInputAction.done,
validator: _validatePassword,
onFieldSubmitted: (_) => _handleLogin(),
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: colorScheme.surfaceVariant.withOpacity(0.3),
),
),
const SizedBox(height: 24),
// Forgot Password Link
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// Handle forgot password
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Forgot password functionality not implemented yet'),
),
);
},
child: Text(
'Forgot Password?',
style: TextStyle(color: colorScheme.primary),
),
),
),
const SizedBox(height: 24),
// Login Button
FilledButton(
onPressed: _isLoading ? null : _handleLogin,
style: FilledButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: colorScheme.onPrimary,
),
)
: const Text(
'Sign In',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 32),
// Sign up link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Don't have an account? ",
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
TextButton(
onPressed: () {
// Handle sign up
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sign up functionality not implemented yet'),
),
);
},
child: Text(
'Sign Up',
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,511 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/routing/route_paths.dart';
import '../../../../core/routing/route_guards.dart';
import '../../../../shared/presentation/providers/app_providers.dart';
/// Home page with navigation to different features
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authStateProvider);
final themeMode = ref.watch(themeModeProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Base Flutter App'),
actions: [
// Theme toggle button
IconButton(
onPressed: () {
ref.read(themeModeProvider.notifier).toggleTheme();
},
icon: Icon(
themeMode == ThemeMode.dark
? Icons.light_mode
: themeMode == ThemeMode.light
? Icons.dark_mode
: Icons.brightness_auto,
),
tooltip: 'Toggle theme',
),
// Settings button
IconButton(
onPressed: () => context.push(RoutePaths.settings),
icon: const Icon(Icons.settings),
tooltip: 'Settings',
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Welcome section
_WelcomeCard(authState: authState),
const SizedBox(height: 24),
// Quick actions section
Text(
'Quick Actions',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_QuickActionsGrid(),
const SizedBox(height: 24),
// Features section
Text(
'Features',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_FeaturesGrid(),
const SizedBox(height: 24),
// Recent activity section
_RecentActivityCard(),
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => context.push(RoutePaths.addTodo),
icon: const Icon(Icons.add),
label: const Text('Add Todo'),
),
);
}
}
class _WelcomeCard extends StatelessWidget {
final AuthState authState;
const _WelcomeCard({
required this.authState,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 24,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Icon(
Icons.person,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
authState == AuthState.authenticated
? 'Welcome back!'
: 'Welcome!',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
authState == AuthState.authenticated
? 'Ready to be productive today?'
: 'Get started with the base Flutter app.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
],
),
),
],
),
if (authState == AuthState.unauthenticated) ...[
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => context.push(RoutePaths.login),
child: const Text('Login'),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton(
onPressed: () => context.push(RoutePaths.register),
child: const Text('Register'),
),
),
],
),
],
],
),
),
);
}
}
class _QuickActionsGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
final quickActions = [
_QuickAction(
icon: Icons.add_task,
title: 'Add Todo',
subtitle: 'Create a new task',
onTap: () => context.push(RoutePaths.addTodo),
),
_QuickAction(
icon: Icons.list,
title: 'View Todos',
subtitle: 'See all tasks',
onTap: () => context.push(RoutePaths.todos),
),
_QuickAction(
icon: Icons.settings,
title: 'Settings',
subtitle: 'App preferences',
onTap: () => context.push(RoutePaths.settings),
),
_QuickAction(
icon: Icons.info,
title: 'About',
subtitle: 'App information',
onTap: () => context.push(RoutePaths.about),
),
];
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.2,
),
itemCount: quickActions.length,
itemBuilder: (context, index) {
return _QuickActionCard(action: quickActions[index]);
},
);
}
}
class _FeaturesGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
final features = [
_Feature(
icon: Icons.check_circle_outline,
title: 'Todo Management',
description: 'Create, edit, and track your tasks efficiently.',
color: Colors.blue,
onTap: () => context.push(RoutePaths.todos),
),
_Feature(
icon: Icons.palette,
title: 'Theming',
description: 'Switch between light, dark, and system themes.',
color: Colors.purple,
onTap: () => context.push(RoutePaths.settingsTheme),
),
_Feature(
icon: Icons.storage,
title: 'Local Storage',
description: 'Hive database integration for offline support.',
color: Colors.orange,
onTap: () => context.push(RoutePaths.settings),
),
_Feature(
icon: Icons.security,
title: 'Secure Storage',
description: 'Protected storage for sensitive data.',
color: Colors.green,
onTap: () => context.push(RoutePaths.settingsPrivacy),
),
];
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.8,
),
itemCount: features.length,
itemBuilder: (context, index) {
return _FeatureCard(feature: features[index]);
},
);
}
}
class _RecentActivityCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Recent Activity',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () => context.push(RoutePaths.todos),
child: const Text('View All'),
),
],
),
const SizedBox(height: 12),
_ActivityItem(
icon: Icons.add_task,
title: 'Welcome to Base Flutter App',
subtitle: 'Your journey starts here',
time: 'Just now',
),
_ActivityItem(
icon: Icons.info,
title: 'App initialized',
subtitle: 'Database and services ready',
time: 'A few seconds ago',
),
],
),
),
);
}
}
class _QuickActionCard extends StatelessWidget {
final _QuickAction action;
const _QuickActionCard({
required this.action,
});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: action.onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
action.icon,
size: 32,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 8),
Text(
action.title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
action.subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
}
}
class _FeatureCard extends StatelessWidget {
final _Feature feature;
const _FeatureCard({
required this.feature,
});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: feature.onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: feature.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
feature.icon,
color: feature.color,
size: 24,
),
),
const SizedBox(height: 12),
Text(
feature.title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Expanded(
child: Text(
feature.description,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
);
}
}
class _ActivityItem extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final String time;
const _ActivityItem({
required this.icon,
required this.title,
required this.subtitle,
required this.time,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
Text(
time,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
),
),
],
),
);
}
}
class _QuickAction {
final IconData icon;
final String title;
final String subtitle;
final VoidCallback onTap;
const _QuickAction({
required this.icon,
required this.title,
required this.subtitle,
required this.onTap,
});
}
class _Feature {
final IconData icon;
final String title;
final String description;
final Color color;
final VoidCallback onTap;
const _Feature({
required this.icon,
required this.title,
required this.description,
required this.color,
required this.onTap,
});
}

View File

@@ -0,0 +1,426 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/routing/route_paths.dart';
import '../../../../core/routing/route_guards.dart';
import '../../../../shared/presentation/providers/app_providers.dart';
/// Main settings page with theme switcher and navigation to other settings
class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
final authState = ref.watch(authStateProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Theme Section
_SectionHeader(title: 'Appearance'),
_ThemeSection(themeMode: themeMode, ref: ref),
const Divider(),
// Account Section
_SectionHeader(title: 'Account'),
_AccountSection(authState: authState, ref: ref),
const Divider(),
// App Settings Section
_SectionHeader(title: 'App Settings'),
_AppSettingsSection(),
const Divider(),
// Privacy & Security Section
_SectionHeader(title: 'Privacy & Security'),
_PrivacySection(),
const Divider(),
// About Section
_SectionHeader(title: 'About'),
_AboutSection(),
const SizedBox(height: 24),
],
),
),
);
}
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
);
}
}
class _ThemeSection extends StatelessWidget {
final ThemeMode themeMode;
final WidgetRef ref;
const _ThemeSection({
required this.themeMode,
required this.ref,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
ListTile(
leading: Icon(
Icons.palette_outlined,
color: Theme.of(context).colorScheme.onSurface,
),
title: const Text('Theme'),
subtitle: Text(_getThemeModeText(themeMode)),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push(RoutePaths.settingsTheme),
),
ListTile(
leading: Icon(
themeMode == ThemeMode.dark
? Icons.dark_mode
: themeMode == ThemeMode.light
? Icons.light_mode
: Icons.brightness_auto,
color: Theme.of(context).colorScheme.onSurface,
),
title: const Text('Quick Theme Toggle'),
subtitle: const Text('Switch between light and dark mode'),
trailing: Switch(
value: themeMode == ThemeMode.dark,
onChanged: (value) {
ref.read(themeModeProvider.notifier).setThemeMode(
value ? ThemeMode.dark : ThemeMode.light,
);
},
),
),
],
);
}
String _getThemeModeText(ThemeMode mode) {
switch (mode) {
case ThemeMode.light:
return 'Light mode';
case ThemeMode.dark:
return 'Dark mode';
case ThemeMode.system:
return 'Follow system';
}
}
}
class _AccountSection extends StatelessWidget {
final AuthState authState;
final WidgetRef ref;
const _AccountSection({
required this.authState,
required this.ref,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
if (authState == AuthState.authenticated) ...[
ListTile(
leading: const Icon(Icons.person_outline),
title: const Text('Profile'),
subtitle: const Text('Manage your profile information'),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push(RoutePaths.profile),
),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('Sign Out'),
subtitle: const Text('Sign out of your account'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showSignOutDialog(context, ref),
),
] else ...[
ListTile(
leading: const Icon(Icons.login),
title: const Text('Sign In'),
subtitle: const Text('Sign in to your account'),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push(RoutePaths.login),
),
ListTile(
leading: const Icon(Icons.person_add_outlined),
title: const Text('Create Account'),
subtitle: const Text('Sign up for a new account'),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push(RoutePaths.register),
),
],
],
);
}
void _showSignOutDialog(BuildContext context, WidgetRef ref) {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Sign Out'),
content: const Text('Are you sure you want to sign out?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop();
ref.read(authStateProvider.notifier).logout();
},
child: const Text('Sign Out'),
),
],
);
},
);
}
}
class _AppSettingsSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
ListTile(
leading: const Icon(Icons.notifications_outlined),
title: const Text('Notifications'),
subtitle: const Text('Manage notification preferences'),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push(RoutePaths.settingsNotifications),
),
ListTile(
leading: const Icon(Icons.language),
title: const Text('Language'),
subtitle: const Text('English (United States)'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Language settings coming soon!')),
);
},
),
ListTile(
leading: const Icon(Icons.storage_outlined),
title: const Text('Storage'),
subtitle: const Text('Manage local data and cache'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Storage settings coming soon!')),
);
},
),
],
);
}
}
class _PrivacySection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
ListTile(
leading: const Icon(Icons.privacy_tip_outlined),
title: const Text('Privacy'),
subtitle: const Text('Privacy settings and data protection'),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push(RoutePaths.settingsPrivacy),
),
ListTile(
leading: const Icon(Icons.security),
title: const Text('Security'),
subtitle: const Text('App security and permissions'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Security settings coming soon!')),
);
},
),
],
);
}
}
class _AboutSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
ListTile(
leading: const Icon(Icons.info_outlined),
title: const Text('About'),
subtitle: const Text('App version and information'),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push(RoutePaths.about),
),
ListTile(
leading: const Icon(Icons.help_outline),
title: const Text('Help & Support'),
subtitle: const Text('Get help and contact support'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Help & Support coming soon!')),
);
},
),
ListTile(
leading: const Icon(Icons.article_outlined),
title: const Text('Terms of Service'),
subtitle: const Text('View terms and conditions'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Terms of Service coming soon!')),
);
},
),
ListTile(
leading: const Icon(Icons.policy_outlined),
title: const Text('Privacy Policy'),
subtitle: const Text('View privacy policy'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Privacy Policy coming soon!')),
);
},
),
],
);
}
}
/// Theme settings page
class ThemeSettingsPage extends ConsumerWidget {
const ThemeSettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Theme Settings'),
),
body: Column(
children: [
const SizedBox(height: 16),
RadioListTile<ThemeMode>(
title: const Text('Light'),
subtitle: const Text('Use light theme'),
value: ThemeMode.light,
groupValue: themeMode,
onChanged: (value) {
if (value != null) {
ref.read(themeModeProvider.notifier).setThemeMode(value);
}
},
),
RadioListTile<ThemeMode>(
title: const Text('Dark'),
subtitle: const Text('Use dark theme'),
value: ThemeMode.dark,
groupValue: themeMode,
onChanged: (value) {
if (value != null) {
ref.read(themeModeProvider.notifier).setThemeMode(value);
}
},
),
RadioListTile<ThemeMode>(
title: const Text('System'),
subtitle: const Text('Follow system theme'),
value: ThemeMode.system,
groupValue: themeMode,
onChanged: (value) {
if (value != null) {
ref.read(themeModeProvider.notifier).setThemeMode(value);
}
},
),
const SizedBox(height: 32),
Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Preview',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: FilledButton(
onPressed: () {},
child: const Text('Filled Button'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton(
onPressed: () {},
child: const Text('Outlined Button'),
),
),
],
),
const SizedBox(height: 12),
Text(
'This is how your theme looks with sample content. The theme will be applied across the entire app.',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,352 @@
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool _isLoading = false;
List<Map<String, dynamic>> _todos = [];
String _searchQuery = '';
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_loadTodos();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
// Mock todos data - will be replaced with API call
Future<void> _loadTodos() async {
setState(() {
_isLoading = true;
});
// Simulate API call delay
await Future.delayed(const Duration(seconds: 1));
// Mock data simulating JSONPlaceholder response
final mockTodos = [
{
'id': 1,
'title': 'Complete project documentation',
'completed': false,
'userId': 1,
},
{
'id': 2,
'title': 'Review code changes',
'completed': true,
'userId': 1,
},
{
'id': 3,
'title': 'Update Flutter dependencies',
'completed': false,
'userId': 1,
},
{
'id': 4,
'title': 'Write unit tests',
'completed': false,
'userId': 2,
},
{
'id': 5,
'title': 'Fix navigation bug',
'completed': true,
'userId': 2,
},
];
if (mounted) {
setState(() {
_todos = mockTodos;
_isLoading = false;
});
}
}
List<Map<String, dynamic>> get _filteredTodos {
if (_searchQuery.isEmpty) {
return _todos;
}
return _todos.where((todo) {
return todo['title']
.toString()
.toLowerCase()
.contains(_searchQuery.toLowerCase());
}).toList();
}
void _toggleTodoStatus(int id) {
setState(() {
final todoIndex = _todos.indexWhere((todo) => todo['id'] == id);
if (todoIndex != -1) {
_todos[todoIndex]['completed'] = !_todos[todoIndex]['completed'];
}
});
}
Future<void> _refreshTodos() async {
await _loadTodos();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('My Todos'),
elevation: 0,
backgroundColor: colorScheme.surfaceVariant,
foregroundColor: colorScheme.onSurfaceVariant,
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
// Handle logout - will be connected to auth logic later
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Logout functionality will be implemented'),
),
);
},
),
],
),
body: Column(
children: [
// Search Bar
Container(
padding: const EdgeInsets.all(16.0),
color: colorScheme.surfaceVariant,
child: TextField(
controller: _searchController,
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
decoration: InputDecoration(
hintText: 'Search todos...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = '';
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: colorScheme.surface,
),
),
),
// Todos List
Expanded(
child: _isLoading
? const Center(
child: CircularProgressIndicator(),
)
: _todos.isEmpty
? _buildEmptyState()
: RefreshIndicator(
onRefresh: _refreshTodos,
child: _filteredTodos.isEmpty
? _buildNoResultsState()
: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _filteredTodos.length,
itemBuilder: (context, index) {
final todo = _filteredTodos[index];
return _buildTodoCard(todo);
},
),
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
// Handle add new todo
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Add todo functionality will be implemented'),
),
);
},
icon: const Icon(Icons.add),
label: const Text('Add Todo'),
),
);
}
Widget _buildTodoCard(Map<String, dynamic> todo) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final isCompleted = todo['completed'] as bool;
return Card(
margin: const EdgeInsets.only(bottom: 8.0),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
leading: Checkbox(
value: isCompleted,
onChanged: (_) => _toggleTodoStatus(todo['id']),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
title: Text(
todo['title'],
style: theme.textTheme.bodyLarge?.copyWith(
decoration: isCompleted ? TextDecoration.lineThrough : null,
color: isCompleted
? colorScheme.onSurfaceVariant
: colorScheme.onSurface,
),
),
subtitle: Text(
'ID: ${todo['id']} • User: ${todo['userId']}',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
trailing: PopupMenuButton<String>(
onSelected: (value) {
switch (value) {
case 'edit':
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Edit functionality will be implemented'),
),
);
break;
case 'delete':
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Delete functionality will be implemented'),
),
);
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit),
SizedBox(width: 8),
Text('Edit'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete),
SizedBox(width: 8),
Text('Delete'),
],
),
),
],
),
),
);
}
Widget _buildEmptyState() {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.task_outlined,
size: 64,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No todos yet',
style: theme.textTheme.headlineSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Text(
'Add your first todo to get started!',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
Widget _buildNoResultsState() {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No todos found',
style: theme.textTheme.headlineSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Text(
'Try adjusting your search terms',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}

View File

@@ -1,122 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const MyApp());
}
import 'core/constants/app_constants.dart';
import 'core/database/hive_service.dart';
import 'core/theme/app_theme.dart';
import 'core/routing/routing.dart';
import 'shared/presentation/providers/app_providers.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
try {
// Initialize Hive database service
await HiveService.initialize();
runApp(
const ProviderScope(
child: MyApp(),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
} catch (e, stackTrace) {
// Handle initialization error
debugPrint('❌ Failed to initialize app: $e');
debugPrint('Stack trace: $stackTrace');
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
// You might want to show an error screen here
runApp(MaterialApp(
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.red, size: 64),
const SizedBox(height: 16),
const Text('Failed to initialize app'),
const SizedBox(height: 8),
Text(e.toString()),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
));
}
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
final router = ref.watch(routerProvider);
return MaterialApp.router(
title: AppConstants.appName,
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode,
routerConfig: router,
);
}
}

View File

@@ -0,0 +1,22 @@
import '../../../core/utils/typedef.dart';
/// Base usecase class for implementing clean architecture use cases
abstract class UseCase<T, Params> {
const UseCase();
/// Execute the use case with given parameters
AsyncResult<T> call(Params params);
}
/// Use case that doesn't require any parameters
abstract class UseCaseWithoutParams<T> {
const UseCaseWithoutParams();
/// Execute the use case without parameters
AsyncResult<T> call();
}
/// No parameters class for use cases that don't need parameters
class NoParams {
const NoParams();
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../../../core/constants/storage_constants.dart';
import '../../../core/network/dio_client.dart';
import '../../../core/providers/network_providers.dart';
/// Secure storage provider
final secureStorageProvider = Provider<FlutterSecureStorage>(
(ref) => const FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
iOptions: IOSOptions(),
),
);
/// HTTP client provider
final httpClientProvider = Provider<DioClient>(
(ref) {
final networkInfo = ref.watch(networkInfoProvider);
final secureStorage = ref.watch(secureStorageProvider);
return DioClient(
networkInfo: networkInfo,
secureStorage: secureStorage,
);
},
);
/// App settings Hive box provider
final appSettingsBoxProvider = Provider<Box>(
(ref) => Hive.box(StorageConstants.appSettingsBox),
);
/// Cache Hive box provider
final cacheBoxProvider = Provider<Box>(
(ref) => Hive.box(StorageConstants.cacheBox),
);
/// User data Hive box provider
final userDataBoxProvider = Provider<Box>(
(ref) => Hive.box(StorageConstants.userDataBox),
);
/// Theme mode provider
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
(ref) => ThemeModeNotifier(ref.watch(appSettingsBoxProvider)),
);
/// Theme mode notifier
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
final Box _box;
ThemeModeNotifier(this._box) : super(ThemeMode.system) {
_loadThemeMode();
}
void _loadThemeMode() {
final isDarkMode = _box.get(StorageConstants.isDarkModeKey, defaultValue: null);
if (isDarkMode == null) {
state = ThemeMode.system;
} else {
state = isDarkMode ? ThemeMode.dark : ThemeMode.light;
}
}
Future<void> setThemeMode(ThemeMode mode) async {
state = mode;
switch (mode) {
case ThemeMode.system:
await _box.delete(StorageConstants.isDarkModeKey);
break;
case ThemeMode.light:
await _box.put(StorageConstants.isDarkModeKey, false);
break;
case ThemeMode.dark:
await _box.put(StorageConstants.isDarkModeKey, true);
break;
}
}
Future<void> toggleTheme() async {
switch (state) {
case ThemeMode.system:
case ThemeMode.light:
await setThemeMode(ThemeMode.dark);
break;
case ThemeMode.dark:
await setThemeMode(ThemeMode.light);
break;
}
}
}

View File

@@ -0,0 +1,345 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter/foundation.dart';
part 'connectivity_providers.g.dart';
/// Network connection type
enum NetworkConnectionType {
wifi,
mobile,
ethernet,
bluetooth,
vpn,
other,
none,
}
/// Network status data class
class NetworkStatus {
final bool isConnected;
final NetworkConnectionType connectionType;
final DateTime lastUpdated;
final String? errorMessage;
const NetworkStatus({
required this.isConnected,
required this.connectionType,
required this.lastUpdated,
this.errorMessage,
});
NetworkStatus copyWith({
bool? isConnected,
NetworkConnectionType? connectionType,
DateTime? lastUpdated,
String? errorMessage,
}) {
return NetworkStatus(
isConnected: isConnected ?? this.isConnected,
connectionType: connectionType ?? this.connectionType,
lastUpdated: lastUpdated ?? this.lastUpdated,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
String toString() {
return 'NetworkStatus{isConnected: $isConnected, connectionType: $connectionType, lastUpdated: $lastUpdated}';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is NetworkStatus &&
other.isConnected == isConnected &&
other.connectionType == connectionType;
}
@override
int get hashCode {
return isConnected.hashCode ^ connectionType.hashCode;
}
}
/// Convert ConnectivityResult to NetworkConnectionType
NetworkConnectionType _getConnectionType(List<ConnectivityResult> results) {
if (results.isEmpty || results.contains(ConnectivityResult.none)) {
return NetworkConnectionType.none;
}
if (results.contains(ConnectivityResult.wifi)) {
return NetworkConnectionType.wifi;
}
if (results.contains(ConnectivityResult.mobile)) {
return NetworkConnectionType.mobile;
}
if (results.contains(ConnectivityResult.ethernet)) {
return NetworkConnectionType.ethernet;
}
if (results.contains(ConnectivityResult.bluetooth)) {
return NetworkConnectionType.bluetooth;
}
if (results.contains(ConnectivityResult.vpn)) {
return NetworkConnectionType.vpn;
}
if (results.contains(ConnectivityResult.other)) {
return NetworkConnectionType.other;
}
return NetworkConnectionType.none;
}
/// Connectivity instance provider
@riverpod
Connectivity connectivity(ConnectivityRef ref) {
return Connectivity();
}
/// Network connectivity stream provider
@riverpod
Stream<NetworkStatus> networkConnectivityStream(NetworkConnectivityStreamRef ref) {
final connectivity = ref.watch(connectivityProvider);
return connectivity.onConnectivityChanged.map((results) {
final connectionType = _getConnectionType(results);
final isConnected = connectionType != NetworkConnectionType.none;
return NetworkStatus(
isConnected: isConnected,
connectionType: connectionType,
lastUpdated: DateTime.now(),
);
}).handleError((error) {
debugPrint('❌ Connectivity stream error: $error');
return NetworkStatus(
isConnected: false,
connectionType: NetworkConnectionType.none,
lastUpdated: DateTime.now(),
errorMessage: error.toString(),
);
});
}
/// Current network status provider
@riverpod
class NetworkStatusNotifier extends _$NetworkStatusNotifier {
@override
NetworkStatus build() {
// Start listening to connectivity changes
final streamValue = ref.watch(networkConnectivityStreamProvider);
streamValue.whenData((status) {
state = status;
_logConnectionChange(status);
});
// Get initial connectivity state
_checkInitialConnectivity();
// Return initial state
return NetworkStatus(
isConnected: false,
connectionType: NetworkConnectionType.none,
lastUpdated: DateTime.now(),
);
}
Future<void> _checkInitialConnectivity() async {
try {
final connectivity = ref.read(connectivityProvider);
final result = await connectivity.checkConnectivity();
final connectionType = _getConnectionType(result);
final isConnected = connectionType != NetworkConnectionType.none;
state = NetworkStatus(
isConnected: isConnected,
connectionType: connectionType,
lastUpdated: DateTime.now(),
);
debugPrint('📡 Initial connectivity: ${isConnected ? '✅ Connected' : '❌ Disconnected'} ($connectionType)');
} catch (error) {
debugPrint('❌ Error checking initial connectivity: $error');
state = NetworkStatus(
isConnected: false,
connectionType: NetworkConnectionType.none,
lastUpdated: DateTime.now(),
errorMessage: error.toString(),
);
}
}
void _logConnectionChange(NetworkStatus status) {
final icon = status.isConnected ? '📶' : '📵';
final statusText = status.isConnected ? 'Connected' : 'Disconnected';
debugPrint('$icon Network status changed: $statusText (${status.connectionType})');
}
/// Force refresh network status
Future<void> refresh() async {
await _checkInitialConnectivity();
}
/// Get connection strength (mock implementation)
double getConnectionStrength() {
switch (state.connectionType) {
case NetworkConnectionType.wifi:
return 1.0; // Assume strong Wi-Fi
case NetworkConnectionType.ethernet:
return 1.0; // Assume strong ethernet
case NetworkConnectionType.mobile:
return 0.7; // Assume moderate mobile
case NetworkConnectionType.bluetooth:
return 0.5; // Assume weak bluetooth
case NetworkConnectionType.vpn:
return 0.8; // Assume good VPN
case NetworkConnectionType.other:
return 0.6; // Assume moderate other
case NetworkConnectionType.none:
return 0.0; // No connection
}
}
}
/// Simple connectivity status provider
@riverpod
bool isConnected(IsConnectedRef ref) {
final networkStatus = ref.watch(networkStatusNotifierProvider);
return networkStatus.isConnected;
}
/// Connection type provider
@riverpod
NetworkConnectionType connectionType(ConnectionTypeRef ref) {
final networkStatus = ref.watch(networkStatusNotifierProvider);
return networkStatus.connectionType;
}
/// Is Wi-Fi connected provider
@riverpod
bool isWifiConnected(IsWifiConnectedRef ref) {
final networkStatus = ref.watch(networkStatusNotifierProvider);
return networkStatus.isConnected && networkStatus.connectionType == NetworkConnectionType.wifi;
}
/// Is mobile data connected provider
@riverpod
bool isMobileConnected(IsMobileConnectedRef ref) {
final networkStatus = ref.watch(networkStatusNotifierProvider);
return networkStatus.isConnected && networkStatus.connectionType == NetworkConnectionType.mobile;
}
/// Network quality indicator provider
@riverpod
String networkQuality(NetworkQualityRef ref) {
final networkStatus = ref.watch(networkStatusNotifierProvider);
if (!networkStatus.isConnected) {
return 'No Connection';
}
switch (networkStatus.connectionType) {
case NetworkConnectionType.wifi:
return 'Excellent';
case NetworkConnectionType.ethernet:
return 'Excellent';
case NetworkConnectionType.mobile:
return 'Good';
case NetworkConnectionType.vpn:
return 'Good';
case NetworkConnectionType.other:
return 'Fair';
case NetworkConnectionType.bluetooth:
return 'Poor';
case NetworkConnectionType.none:
return 'No Connection';
}
}
/// Network history provider for tracking connection changes
@riverpod
class NetworkHistoryNotifier extends _$NetworkHistoryNotifier {
static const int _maxHistorySize = 50;
@override
List<NetworkStatus> build() {
// Listen to network status changes and add to history
ref.listen(networkStatusNotifierProvider, (previous, next) {
if (previous != next) {
_addToHistory(next);
}
});
return [];
}
void _addToHistory(NetworkStatus status) {
final newHistory = [...state, status];
// Keep only the last _maxHistorySize entries
if (newHistory.length > _maxHistorySize) {
newHistory.removeRange(0, newHistory.length - _maxHistorySize);
}
state = newHistory;
}
/// Get recent connection changes
List<NetworkStatus> getRecentChanges({int count = 10}) {
return state.reversed.take(count).toList();
}
/// Get connection uptime percentage
double getUptimePercentage({Duration? period}) {
if (state.isEmpty) return 0.0;
final now = DateTime.now();
final startTime = period != null ? now.subtract(period) : state.first.lastUpdated;
final relevantEntries = state.where((status) => status.lastUpdated.isAfter(startTime)).toList();
if (relevantEntries.isEmpty) return 0.0;
final connectedCount = relevantEntries.where((status) => status.isConnected).length;
return connectedCount / relevantEntries.length;
}
/// Clear history
void clearHistory() {
state = [];
}
/// Get connection statistics
Map<String, dynamic> getConnectionStats() {
if (state.isEmpty) {
return {
'totalChanges': 0,
'uptimePercentage': 0.0,
'mostCommonConnection': 'Unknown',
'connectionTypes': <String, int>{},
};
}
final connectionTypeCounts = <NetworkConnectionType, int>{};
for (final status in state) {
connectionTypeCounts[status.connectionType] = (connectionTypeCounts[status.connectionType] ?? 0) + 1;
}
final mostCommonType = connectionTypeCounts.entries.reduce((a, b) => a.value > b.value ? a : b).key;
return {
'totalChanges': state.length,
'uptimePercentage': getUptimePercentage(),
'mostCommonConnection': mostCommonType.toString().split('.').last,
'connectionTypes': connectionTypeCounts.map((key, value) => MapEntry(key.toString().split('.').last, value)),
};
}
}

View File

@@ -0,0 +1,169 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'connectivity_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$connectivityHash() => r'da8080dfc40288eff97ff9cb96e9d9577714a9a0';
/// Connectivity instance provider
///
/// Copied from [connectivity].
@ProviderFor(connectivity)
final connectivityProvider = AutoDisposeProvider<Connectivity>.internal(
connectivity,
name: r'connectivityProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$connectivityHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef ConnectivityRef = AutoDisposeProviderRef<Connectivity>;
String _$networkConnectivityStreamHash() =>
r'0850402a3f1ed68481cfa9b8a3a371c804c358f3';
/// Network connectivity stream provider
///
/// Copied from [networkConnectivityStream].
@ProviderFor(networkConnectivityStream)
final networkConnectivityStreamProvider =
AutoDisposeStreamProvider<NetworkStatus>.internal(
networkConnectivityStream,
name: r'networkConnectivityStreamProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$networkConnectivityStreamHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef NetworkConnectivityStreamRef
= AutoDisposeStreamProviderRef<NetworkStatus>;
String _$isConnectedHash() => r'89efbfc9ecb21e2ff1a7f6eea736457e35bed181';
/// Simple connectivity status provider
///
/// Copied from [isConnected].
@ProviderFor(isConnected)
final isConnectedProvider = AutoDisposeProvider<bool>.internal(
isConnected,
name: r'isConnectedProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$isConnectedHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef IsConnectedRef = AutoDisposeProviderRef<bool>;
String _$connectionTypeHash() => r'fd1d65f0ae9afe2b04b358755ed4347e27a0515f';
/// Connection type provider
///
/// Copied from [connectionType].
@ProviderFor(connectionType)
final connectionTypeProvider =
AutoDisposeProvider<NetworkConnectionType>.internal(
connectionType,
name: r'connectionTypeProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$connectionTypeHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef ConnectionTypeRef = AutoDisposeProviderRef<NetworkConnectionType>;
String _$isWifiConnectedHash() => r'6ab4a8f83d5073544d77620bea093f4b34d61e05';
/// Is Wi-Fi connected provider
///
/// Copied from [isWifiConnected].
@ProviderFor(isWifiConnected)
final isWifiConnectedProvider = AutoDisposeProvider<bool>.internal(
isWifiConnected,
name: r'isWifiConnectedProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$isWifiConnectedHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef IsWifiConnectedRef = AutoDisposeProviderRef<bool>;
String _$isMobileConnectedHash() => r'1e03a490b5a59ac598fe75b45c42b353cec26129';
/// Is mobile data connected provider
///
/// Copied from [isMobileConnected].
@ProviderFor(isMobileConnected)
final isMobileConnectedProvider = AutoDisposeProvider<bool>.internal(
isMobileConnected,
name: r'isMobileConnectedProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$isMobileConnectedHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef IsMobileConnectedRef = AutoDisposeProviderRef<bool>;
String _$networkQualityHash() => r'b72cb19e0b8537514827d11fbe2f46bba4e94ac2';
/// Network quality indicator provider
///
/// Copied from [networkQuality].
@ProviderFor(networkQuality)
final networkQualityProvider = AutoDisposeProvider<String>.internal(
networkQuality,
name: r'networkQualityProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$networkQualityHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef NetworkQualityRef = AutoDisposeProviderRef<String>;
String _$networkStatusNotifierHash() =>
r'adebb286dce36d8cb54504f04a67dd4c00dceade';
/// Current network status provider
///
/// Copied from [NetworkStatusNotifier].
@ProviderFor(NetworkStatusNotifier)
final networkStatusNotifierProvider =
AutoDisposeNotifierProvider<NetworkStatusNotifier, NetworkStatus>.internal(
NetworkStatusNotifier.new,
name: r'networkStatusNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$networkStatusNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$NetworkStatusNotifier = AutoDisposeNotifier<NetworkStatus>;
String _$networkHistoryNotifierHash() =>
r'6498139c6e6e8472c81cb3f1789bcabfc4779943';
/// Network history provider for tracking connection changes
///
/// Copied from [NetworkHistoryNotifier].
@ProviderFor(NetworkHistoryNotifier)
final networkHistoryNotifierProvider = AutoDisposeNotifierProvider<
NetworkHistoryNotifier, List<NetworkStatus>>.internal(
NetworkHistoryNotifier.new,
name: r'networkHistoryNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$networkHistoryNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$NetworkHistoryNotifier = AutoDisposeNotifier<List<NetworkStatus>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
const CustomAppBar({
super.key,
required this.title,
this.actions,
this.leading,
this.backgroundColor,
this.foregroundColor,
this.elevation = 0,
});
final String title;
final List<Widget>? actions;
final Widget? leading;
final Color? backgroundColor;
final Color? foregroundColor;
final double elevation;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return AppBar(
title: Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
leading: leading,
actions: actions,
elevation: elevation,
backgroundColor: backgroundColor ?? colorScheme.surfaceVariant,
foregroundColor: foregroundColor ?? colorScheme.onSurfaceVariant,
centerTitle: true,
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

View File

@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
class EmptyStateWidget extends StatelessWidget {
const EmptyStateWidget({
super.key,
required this.icon,
required this.title,
required this.subtitle,
this.actionButton,
this.iconSize = 64.0,
});
final IconData icon;
final String title;
final String subtitle;
final Widget? actionButton;
final double iconSize;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: iconSize,
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
),
const SizedBox(height: 24),
Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
subtitle,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
if (actionButton != null) ...[
const SizedBox(height: 32),
actionButton!,
],
],
),
),
);
}
}

View File

@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
class LoadingWidget extends StatelessWidget {
const LoadingWidget({
super.key,
this.message,
this.size = 24.0,
});
final String? message;
final double size;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
strokeWidth: 2.0,
color: colorScheme.primary,
),
),
if (message != null) ...[
const SizedBox(height: 16),
Text(
message!,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
],
),
);
}
}
class LoadingOverlay extends StatelessWidget {
const LoadingOverlay({
super.key,
required this.isLoading,
required this.child,
this.message,
});
final bool isLoading;
final Widget child;
final String? message;
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
if (isLoading)
Container(
color: Colors.black.withOpacity(0.3),
child: LoadingWidget(message: message),
),
],
);
}
}