12 KiB
Theme Implementation Guide - HMG Patient App
Overview
This Flutter application implements a comprehensive dual-theme system supporting both Light Mode and Dark Mode. The theme system is built with a combination of Flutter's native theming and a custom color management system.
Architecture
1. Core Components
A. AppTheme Class (lib/theme/app_theme.dart)
- Purpose: Defines the Material theme configurations for both light and dark modes
- Key Methods:
getTheme(isArabic)- Returns light theme configurationgetDarkTheme(isArabic)- Returns dark theme configuration
Features:
- Dynamic font family selection based on locale (Arabic:
CairoArabic, English:Poppins) - Platform-specific page transitions (Android: Zoom, iOS: Cupertino)
- Consistent styling for AppBar, BottomSheet, FloatingActionButton
- Transparent splash colors for better UX
- System overlay styles (Dark for light theme, Light for dark theme)
B. AppColors Class (lib/theme/colors.dart)
- Purpose: Centralized color management with dual-palette support
- Architecture: Three-tier color system
Three Access Patterns:
-
Static Getters (Global State-Based)
AppColors.primaryRedColor // Uses AppColors.isDarkMode flag- Checks global
AppColors.isDarkModeboolean - Returns dark or light palette value accordingly
- Checks global
-
AppColorsDark Class (Dark Palette)
AppColors.dark.primaryRedColor // Always dark variant- Constant dark mode color values
- Used as source for dark theme colors
-
BuildContext Extension (Theme-Aware)
context.primaryRedColor // Uses Theme brightness- Reads
Theme.of(context).brightness - Automatically adapts to MaterialApp's themeMode
- Reads
2. Theme State Management
ProfileSettingsViewModel
Located in lib/features/profile_settings/profile_settings_view_model.dart
Responsibilities:
- Manages dark mode state (
_isDarkModeboolean) - Persists preference to local storage (CacheService)
- Triggers UI rebuilds on theme changes
Key Methods:
// Called at app startup (before first frame)
void loadDarkMode() {
final saved = _cacheService.getBool(key: _darkModeKey);
_isDarkMode = saved ?? false;
AppColors.isDarkMode = _isDarkMode;
// No notifyListeners() - called before build
}
// Toggle theme and persist
void toggleDarkMode(bool value) {
_isDarkMode = value;
AppColors.isDarkMode = value;
_cacheService.saveBool(key: _darkModeKey, value: value);
notifyListeners(); // Triggers rebuild
}
3. Application Lifecycle
Initialization Flow (main.dart)
Future<void> callInitializations() async {
// ... Firebase, dependencies setup ...
// Restore dark mode BEFORE first frame
getIt.get<ProfileSettingsViewModel>().loadDarkMode();
}
Theme Application in Widget Tree
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ProfileSettingsViewModel>(
builder: (context, profileVm, _) {
final isArabic = EasyLocalization.of(context)?.locale.languageCode == "ar";
return MaterialApp(
key: ValueKey('app_theme_${profileVm.isDarkMode}'), // Force rebuild on theme change
theme: AppTheme.getTheme(isArabic),
darkTheme: AppTheme.getDarkTheme(isArabic),
themeMode: profileVm.isDarkMode ? ThemeMode.dark : ThemeMode.light,
// ... other properties
);
},
);
}
}
Key Mechanism:
Consumer<ProfileSettingsViewModel>listens for theme changes- When
toggleDarkMode()is called →notifyListeners()→ Widget rebuilds themeModeproperty switches betweenThemeMode.darkandThemeMode.lightValueKeyforces MaterialApp to rebuild entirely on theme change
4. Color System Design
Dual-Palette Architecture
class AppColors {
static bool isDarkMode = false; // Global flag
// Pattern: Conditional getter
static Color get primaryRedColor =>
isDarkMode ? dark.primaryRedColor : const Color(0xFFED1C2B);
// Dark palette instance
static const AppColorsDark dark = AppColorsDark();
}
class AppColorsDark {
const AppColorsDark();
// Dark mode variants
Color get primaryRedColor => const Color(0xFFDE5C5D);
Color get scaffoldBgColor => const Color(0xFF191919);
Color get textColor => const Color(0xFFFFFFFF);
// ... 100+ color definitions
}
BuildContext Extension for Theme-Aware Colors
extension AppColorsContext on BuildContext {
bool get _isDark => Theme.of(this).brightness == Brightness.dark;
Color get scaffoldBgColor =>
_isDark ? AppColors.dark.scaffoldBgColor : const Color(0xFFF8F8F8);
Color get textColor =>
_isDark ? AppColors.dark.textColor : const Color(0xFF2E3039);
}
Usage in Widgets:
// Option 1: Static (uses global flag)
color: AppColors.primaryRedColor
// Option 2: Extension (uses Theme.of(context))
color: context.primaryRedColor
// Option 3: Direct dark access
color: AppColors.dark.primaryRedColor
5. User Interface Implementation
Theme Toggle Control (profile_settings.dart)
Consumer<ProfileSettingsViewModel>(
builder: (context, profileVm, _) {
return actionItem(
AppAssets.darkModeIcon,
LocaleKeys.darkMode.tr(context: context),
() {
profileVm.toggleDarkMode(!profileVm.isDarkMode);
},
switchValue: profileVm.isDarkMode,
onSwitchChanged: (value) {
profileVm.toggleDarkMode(value);
},
);
},
)
6. Color Categories
Organized Color Groups
| Category | Light Example | Dark Example |
|---|---|---|
| Scaffold/Background | 0xFFF8F8F8 |
0xFF191919 |
| Primary Brand | 0xFFED1C2B (Red) |
0xFFDE5C5D (Softer Red) |
| Text | 0xFF2E3039 (Dark) |
0xFFFFFFFF (White) |
| Success | 0xFF18C273 (Green) |
0xFF18C273 (Same) |
| Error | 0xFFED1C2B |
0xFFD63D48 |
| Card Surface | 0xFFFFFFFF |
0xFF1E1E1E |
| Borders | 0x332E3039 |
0x55ECECEC |
| Shimmer Base | 0xFFE0E0E0 |
0xFF2C2C2C |
Special Purpose Colors
- Rating Stars: Constant across themes (
0xFFFFA726) - Health Calculators: Feature-specific colors maintained in both themes
- Status Colors: Pending, Processing, Completed, Rejected
- Info Banners: Warning backgrounds with adjusted opacity for dark mode
7. Best Practices for Developers
Adding New Colors
- Define in Light Theme:
static Color get newFeatureColor =>
isDarkMode ? dark.newFeatureColor : const Color(0xFFXXXXXX);
- Add Dark Variant:
class AppColorsDark {
Color get newFeatureColor => const Color(0xFFYYYYYY);
}
- Optional: Add to BuildContext Extension:
extension AppColorsContext on BuildContext {
Color get newFeatureColor =>
_isDark ? AppColors.dark.newFeatureColor : const Color(0xFFXXXXXX);
}
Using Colors in Widgets
Recommended Approach:
// For colors that should adapt to theme
Container(
color: AppColors.scaffoldBgColor, // Uses global isDarkMode
)
// Or using context extension
Container(
color: context.scaffoldBgColor, // Uses Theme.brightness
)
Avoid:
// Don't hardcode colors
Container(
color: Color(0xFFED1C2B), // ❌ Won't adapt to dark mode
)
Testing Theme Changes
// Toggle theme programmatically
context.read<ProfileSettingsViewModel>().toggleDarkMode(true);
// Check current state
final isDark = context.read<ProfileSettingsViewModel>().isDarkMode;
8. Persistence Layer
CacheService Integration
- Uses shared_preferences or similar local storage
- Key:
_darkModeKey(defined in ProfileSettingsViewModel) - Automatically loads on app startup
- Saves immediately on toggle
Flow:
- App launches →
loadDarkMode()reads from cache - User toggles →
toggleDarkMode(value)saves to cache - App restart → Previous preference restored
9. Font Handling
Locale-Specific Fonts
// In AppTheme
fontFamily: isArabic ? 'CairoArabic' : 'Poppins'
textTheme: const TextTheme(
displayLarge: TextStyle(fontFamily: 'CairoArabic'),
bodyLarge: TextStyle(fontFamily: 'CairoArabic'),
)
Rationale: Arabic text requires specialized fonts for proper rendering
10. System Integration
AppBar System Overlay
// Light Theme
systemOverlayStyle: SystemUiOverlayStyle.dark // Dark status bar icons
// Dark Theme
systemOverlayStyle: SystemUiOverlayStyle.light // Light status bar icons
Platform-Specific Adjustments
// SafeArea handling
SafeArea(
top: false,
bottom: Platform.isIOS ? false : true,
)
11. Advanced Features
Gradient Support
static const LinearGradient aiLinearGradient = LinearGradient(
colors: [Color(0xFF8A38F5), Color(0xFFE20BBB)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
Note: Gradients remain constant across themes for brand consistency
Transparency Management
static const transparent = Colors.transparent;
// Used for splash colors, bottomSheet backgrounds
12. Common Pitfalls & Solutions
Issue 1: Colors Not Updating
Problem: Widget doesn't reflect theme change Solution: Ensure widget rebuilds on theme change
// Wrap with Consumer or use context.watch
Consumer<ProfileSettingsViewModel>(
builder: (context, vm, _) => YourWidget(),
)
Issue 2: Inconsistent Colors
Problem: Some UI elements use wrong theme
Solution: Always use AppColors.* or context.*, never hardcode
Issue 3: Theme Not Persisting
Problem: Theme resets on app restart
Solution: Verify loadDarkMode() is called in callInitializations()
13. File Structure
lib/
├── theme/
│ ├── app_theme.dart # Theme configurations
│ └── colors.dart # Color definitions & dark palette
├── features/
│ └── profile_settings/
│ └── profile_settings_view_model.dart # Theme state management
├── presentation/
│ └── profile_settings/
│ └── profile_settings.dart # UI with theme toggle
└── main.dart # Theme initialization & application
14. Summary
Workflow
- Initialization: Load saved theme preference → Set
AppColors.isDarkMode - User Action: Toggle switch in Settings → Call
toggleDarkMode() - State Update: Update flag → Save to cache → Notify listeners
- UI Rebuild: Consumer rebuilds → MaterialApp switches
themeMode - Color Resolution: All
AppColors.*getters return appropriate palette
Key Advantages
✅ Centralized: Single source of truth for colors
✅ Type-Safe: Compile-time color checking
✅ Persistent: Survives app restarts
✅ Flexible: Multiple access patterns for different use cases
✅ Maintainable: Easy to add/modify colors
✅ Performance: No runtime color calculations, just conditional returns
15. Migration Checklist for New Features
When adding a new screen/feature:
- Use
AppColors.*for all color references - Test in both light and dark modes
- Ensure text contrast meets accessibility standards
- Add dark variants for any new colors
- Verify with Arabic locale (font rendering)
- Check shimmer/loading states in both themes
- Test on both iOS and Android
Last Updated: January 2024
Maintained By: HMG Development Team