52 KiB
Dynamic Multi-Theme Implementation Plan
HMG Patient App - Firebase Remote Config Driven Themes
🔥 Firebase-First Approach: All theme configurations, colors, dates, and features are controlled via Firebase Remote Config. Zero app updates needed for new events!
🎯 Quick Overview
What We're Building
A dynamic theming system that allows you to change your app's appearance for special events (National Day, Ramadan, Eid, etc.) without deploying new app versions. Everything is controlled via Firebase Remote Config.
Why Firebase Remote Config?
- ✅ Instant Updates: Change themes without app store approval
- ✅ A/B Testing: Test different theme variations
- ✅ Gradual Rollout: Deploy to 10%, 50%, 100% of users
- ✅ Instant Rollback: Disable themes immediately if needed
- ✅ Targeting: Different themes for different regions/languages
- ✅ No Code Changes: Add unlimited new events via JSON
Example: National Day 2026
September 19, 2026 (Firebase Console):
1. Create JSON config with Saudi green colors
2. Set dates: Sept 20-24
3. Click "Publish"
4. ✨ ALL users see green theme within 1 hour!
September 25, 2026:
- Theme automatically deactivates (date passed)
- App returns to normal colors
- Zero developer intervention
Dynamic Multi-Theme Implementation Plan
HMG Patient App - National Day & International Events Theming
📋 Executive Summary
Goal: Implement dynamic event-based themes (National Day, Eid, New Year, etc.) while maintaining the existing Light/Dark mode architecture with minimal changes, controlled via Firebase Remote Config.
Strategy: Extend the current dual-theme system to support a Theme Layer System where event themes override base colors while preserving dark mode functionality. All theme activation, dates, colors, and features are controlled remotely via Firebase.
Impact:
- ✅ Zero breaking changes to existing code
- ✅ Maintains current dark/light mode toggle
- ✅ Supports unlimited event themes
- ✅ 100% controlled via Firebase Remote Config (no app updates needed)
- ✅ Real-time theme activation/deactivation
- ✅ A/B testing support for event themes
- ✅ Instant rollback capability
🏗️ Architecture Overview
Current Architecture (Preserved)
User Toggle → ProfileSettingsViewModel → AppColors.isDarkMode → Theme Switch
New Architecture (Firebase-Driven)
Firebase Remote Config → RemoteConfigService → EventThemeManager → Active Event Theme
↓
Color Overlay
↓
User Toggle → ProfileSettingsViewModel → Dark/Light Mode → AppColors Resolution → Final UI
Data Flow
1. App Startup → Fetch Remote Config
2. Parse Active Event Themes (JSON)
3. Check Current Date/Time
4. Activate Highest Priority Theme
5. Apply Color Overrides
6. Listen for Config Updates (Real-time)
📁 File Structure Changes
New Files to Create (9 files)
lib/
├── theme/
│ ├── app_theme.dart # [MODIFY] - Add event theme support
│ ├── colors.dart # [MODIFY] - Add event color override system
│ ├── event_themes/ # [NEW DIRECTORY]
│ │ ├── event_theme_config.dart # [NEW] - Event theme definitions from Remote Config
│ │ ├── event_theme_manager.dart # [NEW] - Theme activation logic
│ │ ├── remote_config_service.dart # [NEW] - Firebase Remote Config wrapper
│ │ ├── themes/ # [NEW DIRECTORY]
│ │ │ ├── base_event_theme.dart # [NEW] - Abstract base class
│ │ │ ├── dynamic_event_theme.dart # [NEW] - Dynamic theme from Remote Config
│ │ └── models/
│ │ ├── event_theme_model.dart # [NEW] - Event metadata model
│ │ └── remote_theme_config.dart # [NEW] - Remote Config data model
├── features/
│ └── profile_settings/
│ └── profile_settings_view_model.dart # [MODIFY] - Add event theme state
└── core/
└── app_state.dart # [MODIFY] - Add event theme flags
Modified Files (4 files)
lib/theme/app_theme.dart- Add event theme injectionlib/theme/colors.dart- Add event color override getterslib/features/profile_settings/profile_settings_view_model.dart- Add event theme statelib/main.dart- Initialize theme manager and Remote Config
Firebase Remote Config Keys
event_themes_config- JSON array of active event themesenable_event_themes- Global on/off switchdebug_event_theme- Force specific theme for testing
🎨 Design Pattern: Theme Layer System
Layer Hierarchy (Bottom to Top)
1. Base Light Colors (Always present)
↓
2. Base Dark Colors (When isDarkMode = true)
↓
3. Event Theme Colors (When active event exists)
↓
4. Final Rendered Color
Color Resolution Logic
Color resolveColor() {
if (hasActiveEvent && eventTheme.overridesThisColor) {
return eventTheme.getColor(isDarkMode); // Event-specific color
}
return isDarkMode ? darkColor : lightColor; // Standard color
}
🔧 Implementation Steps
Phase 1: Firebase Remote Config Setup (Day 1-2)
Step 1.0: Firebase Remote Config Service
File: lib/theme/event_themes/remote_config_service.dart (NEW)
import 'dart:convert';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
class RemoteConfigService extends ChangeNotifier {
static RemoteConfigService? _instance;
factory RemoteConfigService() => _instance ??= RemoteConfigService._internal();
RemoteConfigService._internal();
final FirebaseRemoteConfig _remoteConfig = FirebaseRemoteConfig.instance;
final LoggerService _logger = LoggerService();
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
// Remote Config Keys
static const String _eventThemesConfigKey = 'event_themes_config';
static const String _enableEventThemesKey = 'enable_event_themes';
static const String _debugEventThemeKey = 'debug_event_theme';
/// Initialize Remote Config with defaults
Future<void> initialize() async {
try {
await _remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(minutes: 1),
minimumFetchInterval: const Duration(hours: 1),
));
// Set default values
await _remoteConfig.setDefaults({
_eventThemesConfigKey: _getDefaultThemesConfig(),
_enableEventThemesKey: true,
_debugEventThemeKey: '',
});
// Fetch and activate
await _remoteConfig.fetchAndActivate();
_isInitialized = true;
_logger.logInfo('Remote Config initialized successfully');
// Listen for real-time updates
_remoteConfig.onConfigUpdated.listen((event) async {
await _remoteConfig.activate();
_logger.logInfo('Remote Config updated');
notifyListeners(); // Notify listeners of config changes
});
} catch (e) {
_logger.logError('Remote Config initialization failed: $e');
_isInitialized = false;
}
}
/// Get event themes configuration from Remote Config
List<Map<String, dynamic>> getEventThemesConfig() {
try {
final jsonString = _remoteConfig.getString(_eventThemesConfigKey);
if (jsonString.isEmpty) return [];
final decoded = jsonDecode(jsonString);
if (decoded is List) {
return List<Map<String, dynamic>>.from(
decoded.map((item) => Map<String, dynamic>.from(item))
);
}
return [];
} catch (e) {
_logger.logError('Error parsing event themes config: $e');
return [];
}
}
/// Check if event themes are globally enabled
bool areEventThemesEnabled() {
return _remoteConfig.getBool(_enableEventThemesKey);
}
/// Get debug theme (for testing)
String? getDebugEventTheme() {
final value = _remoteConfig.getString(_debugEventThemeKey);
return value.isEmpty ? null : value;
}
/// Force refresh config (call when app comes to foreground)
Future<void> refresh() async {
try {
await _remoteConfig.fetchAndActivate();
notifyListeners();
} catch (e) {
_logger.logError('Remote Config refresh failed: $e');
}
}
/// Default configuration (fallback)
String _getDefaultThemesConfig() {
return jsonEncode([
// Default: No active themes
// Themes are activated via Firebase Console
]);
}
}
Step 1.1: Remote Theme Configuration Model
File: lib/theme/event_themes/models/remote_theme_config.dart (NEW)
import 'package:flutter/material.dart';
/// Model for event theme configuration from Firebase Remote Config
class RemoteThemeConfig {
final String id;
final String type; // 'nationalDay', 'eid', 'ramadan', etc.
final String nameEn;
final String nameAr;
final DateTime startDate;
final DateTime endDate;
final bool isActive;
final int priority;
// Color overrides (hex strings from Remote Config)
final String? primaryColor;
final String? primaryColorDark;
final String? secondaryColor;
final String? secondaryColorDark;
final String? accentColor;
final String? accentColorDark;
final String? scaffoldBgColor;
final String? scaffoldBgColorDark;
final String? cardBgColor;
final String? cardBgColorDark;
final String? appBarColor;
final String? appBarColorDark;
// Gradient configuration
final List<String>? gradientColors;
final String? gradientStart; // 'topLeft', 'topCenter', etc.
final String? gradientEnd;
// Special features
final String? bannerImageUrl;
final bool showConfetti;
final bool enableAnimations;
RemoteThemeConfig({
required this.id,
required this.type,
required this.nameEn,
required this.nameAr,
required this.startDate,
required this.endDate,
required this.isActive,
required this.priority,
this.primaryColor,
this.primaryColorDark,
this.secondaryColor,
this.secondaryColorDark,
this.accentColor,
this.accentColorDark,
this.scaffoldBgColor,
this.scaffoldBgColorDark,
this.cardBgColor,
this.cardBgColorDark,
this.appBarColor,
this.appBarColorDark,
this.gradientColors,
this.gradientStart,
this.gradientEnd,
this.bannerImageUrl,
this.showConfetti = false,
this.enableAnimations = false,
});
/// Create from Remote Config JSON
factory RemoteThemeConfig.fromJson(Map<String, dynamic> json) {
return RemoteThemeConfig(
id: json['id'] ?? '',
type: json['type'] ?? '',
nameEn: json['nameEn'] ?? '',
nameAr: json['nameAr'] ?? '',
startDate: DateTime.parse(json['startDate'] ?? DateTime.now().toIso8601String()),
endDate: DateTime.parse(json['endDate'] ?? DateTime.now().toIso8601String()),
isActive: json['isActive'] ?? false,
priority: json['priority'] ?? 0,
primaryColor: json['primaryColor'],
primaryColorDark: json['primaryColorDark'],
secondaryColor: json['secondaryColor'],
secondaryColorDark: json['secondaryColorDark'],
accentColor: json['accentColor'],
accentColorDark: json['accentColorDark'],
scaffoldBgColor: json['scaffoldBgColor'],
scaffoldBgColorDark: json['scaffoldBgColorDark'],
cardBgColor: json['cardBgColor'],
cardBgColorDark: json['cardBgColorDark'],
appBarColor: json['appBarColor'],
appBarColorDark: json['appBarColorDark'],
gradientColors: json['gradientColors'] != null
? List<String>.from(json['gradientColors'])
: null,
gradientStart: json['gradientStart'],
gradientEnd: json['gradientEnd'],
bannerImageUrl: json['bannerImageUrl'],
showConfetti: json['showConfetti'] ?? false,
enableAnimations: json['enableAnimations'] ?? false,
);
}
/// Check if theme should be active based on dates
bool get shouldActivate {
final now = DateTime.now();
return now.isAfter(startDate) &&
now.isBefore(endDate) &&
isActive;
}
/// Parse hex color string to Color
Color? parseColor(String? hexString) {
if (hexString == null || hexString.isEmpty) return null;
try {
final hex = hexString.replaceAll('#', '');
if (hex.length == 6) {
return Color(int.parse('FF$hex', radix: 16));
} else if (hex.length == 8) {
return Color(int.parse(hex, radix: 16));
}
} catch (e) {
// Invalid color format
}
return null;
}
/// Get gradient alignment from string
Alignment _parseAlignment(String? alignment) {
switch (alignment?.toLowerCase()) {
case 'topleft': return Alignment.topLeft;
case 'topcenter': return Alignment.topCenter;
case 'topright': return Alignment.topRight;
case 'centerleft': return Alignment.centerLeft;
case 'center': return Alignment.center;
case 'centerright': return Alignment.centerRight;
case 'bottomleft': return Alignment.bottomLeft;
case 'bottomcenter': return Alignment.bottomCenter;
case 'bottomright': return Alignment.bottomRight;
default: return Alignment.topLeft;
}
}
/// Build gradient from configuration
LinearGradient? get gradient {
if (gradientColors == null || gradientColors!.length < 2) return null;
final colors = gradientColors!
.map((hex) => parseColor(hex))
.whereType<Color>()
.toList();
if (colors.length < 2) return null;
return LinearGradient(
colors: colors,
begin: _parseAlignment(gradientStart),
end: _parseAlignment(gradientEnd),
);
}
}
Step 1.2: Create Base Event Theme System
File: lib/theme/event_themes/models/event_theme_model.dart
enum EventType {
nationalDay,
eid,
newYear,
ramadan,
foundingDay,
motherDay,
custom
}
class EventThemeModel {
final String id;
final EventType type;
final String nameEn;
final String nameAr;
final DateTime startDate;
final DateTime endDate;
final bool isActive;
final int priority; // Higher number = higher priority
// Auto-activate based on current date
bool get shouldActivate {
final now = DateTime.now();
return now.isAfter(startDate) &&
now.isBefore(endDate) &&
isActive;
}
}
File: lib/theme/event_themes/themes/base_event_theme.dart
abstract class BaseEventTheme {
EventThemeModel get metadata;
// Colors that override base theme
Color? get primaryBrandColor;
Color? get secondaryBrandColor;
Color? get accentColor;
Color? get scaffoldBackgroundColor;
Color? get cardBackgroundColor;
Color? get appBarColor;
// Dark mode variants (optional)
Color? get primaryBrandColorDark;
Color? get secondaryBrandColorDark;
Color? get accentColorDark;
Color? get scaffoldBackgroundColorDark;
Color? get cardBackgroundColorDark;
Color? get appBarColorDark;
// Special features (optional)
LinearGradient? get backgroundGradient;
String? get celebrationBannerImage;
bool get showConfetti;
bool get enableSpecialAnimations;
// Method to get color with dark mode support
Color? getColor(String colorKey, bool isDarkMode) {
// Implementation in concrete classes
}
}
Step 1.3: Dynamic Event Theme from Remote Config
File: lib/theme/event_themes/themes/dynamic_event_theme.dart (NEW)
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/theme/event_themes/models/remote_theme_config.dart';
import 'package:hmg_patient_app_new/theme/event_themes/themes/base_event_theme.dart';
import 'package:hmg_patient_app_new/theme/event_themes/models/event_theme_model.dart';
/// Dynamic event theme created from Remote Config
class DynamicEventTheme extends BaseEventTheme {
final RemoteThemeConfig config;
DynamicEventTheme(this.config);
@override
EventThemeModel get metadata => EventThemeModel(
id: config.id,
type: _parseEventType(config.type),
nameEn: config.nameEn,
nameAr: config.nameAr,
startDate: config.startDate,
endDate: config.endDate,
isActive: config.isActive,
priority: config.priority,
);
EventType _parseEventType(String type) {
switch (type.toLowerCase()) {
case 'nationalday': return EventType.nationalDay;
case 'eid': return EventType.eid;
case 'ramadan': return EventType.ramadan;
case 'newyear': return EventType.newYear;
case 'foundingday': return EventType.foundingDay;
case 'motherday': return EventType.motherDay;
default: return EventType.custom;
}
}
// Color overrides from Remote Config
@override
Color? get primaryBrandColor => config.parseColor(config.primaryColor);
@override
Color? get primaryBrandColorDark => config.parseColor(config.primaryColorDark);
@override
Color? get secondaryBrandColor => config.parseColor(config.secondaryColor);
@override
Color? get secondaryBrandColorDark => config.parseColor(config.secondaryColorDark);
@override
Color? get accentColor => config.parseColor(config.accentColor);
@override
Color? get accentColorDark => config.parseColor(config.accentColorDark);
@override
Color? get scaffoldBackgroundColor => config.parseColor(config.scaffoldBgColor);
@override
Color? get scaffoldBackgroundColorDark => config.parseColor(config.scaffoldBgColorDark);
@override
Color? get cardBackgroundColor => config.parseColor(config.cardBgColor);
@override
Color? get cardBackgroundColorDark => config.parseColor(config.cardBgColorDark);
@override
Color? get appBarColor => config.parseColor(config.appBarColor);
@override
Color? get appBarColorDark => config.parseColor(config.appBarColorDark);
// Special features from Remote Config
@override
LinearGradient? get backgroundGradient => config.gradient;
@override
String? get celebrationBannerImage => config.bannerImageUrl;
@override
bool get showConfetti => config.showConfetti;
@override
bool get enableSpecialAnimations => config.enableAnimations;
@override
Color? getColor(String colorKey, bool isDarkMode) {
switch (colorKey) {
case 'primaryBrand':
return isDarkMode ? primaryBrandColorDark ?? primaryBrandColor : primaryBrandColor;
case 'secondaryBrand':
return isDarkMode ? secondaryBrandColorDark ?? secondaryBrandColor : secondaryBrandColor;
case 'accent':
return isDarkMode ? accentColorDark ?? accentColor : accentColor;
case 'scaffoldBackground':
return isDarkMode ? scaffoldBackgroundColorDark ?? scaffoldBackgroundColor : scaffoldBackgroundColor;
case 'cardBackground':
return isDarkMode ? cardBackgroundColorDark ?? cardBackgroundColor : cardBackgroundColor;
case 'appBar':
return isDarkMode ? appBarColorDark ?? appBarColor : appBarColor;
default:
return null;
}
}
}
Step 1.4: Create Event Theme Manager (Remote Config Integration)
File: lib/theme/event_themes/event_theme_manager.dart
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/theme/event_themes/remote_config_service.dart';
import 'package:hmg_patient_app_new/theme/event_themes/models/remote_theme_config.dart';
import 'package:hmg_patient_app_new/theme/event_themes/themes/base_event_theme.dart';
import 'package:hmg_patient_app_new/theme/event_themes/themes/dynamic_event_theme.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
class EventThemeManager extends ChangeNotifier {
static EventThemeManager? _instance;
factory EventThemeManager() => _instance ??= EventThemeManager._internal();
EventThemeManager._internal();
final RemoteConfigService _remoteConfigService = RemoteConfigService();
final LoggerService _logger = LoggerService();
BaseEventTheme? _activeEventTheme;
List<BaseEventTheme> _availableThemes = [];
bool _isInitialized = false;
BaseEventTheme? get activeEventTheme => _activeEventTheme;
bool get hasActiveEvent => _activeEventTheme != null;
bool get isInitialized => _isInitialized;
/// Initialize with Remote Config
Future<void> initialize() async {
try {
// Initialize Remote Config first
await _remoteConfigService.initialize();
// Listen to Remote Config updates
_remoteConfigService.addListener(_onRemoteConfigUpdated);
// Load themes from Remote Config
await _loadThemesFromRemoteConfig();
_isInitialized = true;
_logger.logInfo('EventThemeManager initialized successfully');
} catch (e) {
_logger.logError('EventThemeManager initialization failed: $e');
_isInitialized = false;
}
}
/// Load themes from Remote Config
Future<void> _loadThemesFromRemoteConfig() async {
try {
// Check if event themes are globally enabled
if (!_remoteConfigService.areEventThemesEnabled()) {
_logger.logInfo('Event themes are disabled via Remote Config');
_availableThemes = [];
_activeEventTheme = null;
notifyListeners();
return;
}
// Check for debug theme override
final debugTheme = _remoteConfigService.getDebugEventTheme();
if (debugTheme != null && debugTheme.isNotEmpty) {
_logger.logInfo('Debug theme active: $debugTheme');
await _activateDebugTheme(debugTheme);
return;
}
// Get event themes configuration
final themesConfig = _remoteConfigService.getEventThemesConfig();
// Convert to RemoteThemeConfig objects
final remoteConfigs = themesConfig
.map((json) => RemoteThemeConfig.fromJson(json))
.toList();
// Create DynamicEventTheme instances
_availableThemes = remoteConfigs
.map((config) => DynamicEventTheme(config))
.toList();
_logger.logInfo('Loaded ${_availableThemes.length} themes from Remote Config');
// Check for active event
_checkForActiveEvent();
} catch (e) {
_logger.logError('Error loading themes from Remote Config: $e');
_availableThemes = [];
_activeEventTheme = null;
}
notifyListeners();
}
/// Handle Remote Config updates
void _onRemoteConfigUpdated() {
_logger.logInfo('Remote Config updated - reloading themes');
_loadThemesFromRemoteConfig();
}
/// Check for active events based on date
void _checkForActiveEvent() {
// Filter themes that should be active
final activeThemes = _availableThemes
.where((theme) => theme.metadata.shouldActivate)
.toList()
..sort((a, b) => b.metadata.priority.compareTo(a.metadata.priority));
final previousTheme = _activeEventTheme;
_activeEventTheme = activeThemes.isNotEmpty ? activeThemes.first : null;
// Log theme change
if (previousTheme?.metadata.id != _activeEventTheme?.metadata.id) {
if (_activeEventTheme != null) {
_logger.logInfo('Event theme activated: ${_activeEventTheme!.metadata.nameEn}');
} else {
_logger.logInfo('Event theme deactivated');
}
}
}
/// Activate debug theme (for testing)
Future<void> _activateDebugTheme(String themeType) async {
final theme = _availableThemes.firstWhere(
(t) => t.metadata.type.toString().contains(themeType),
orElse: () {
// If no matching theme found, create a default one
_logger.logWarning('Debug theme not found: $themeType');
return _availableThemes.isNotEmpty ? _availableThemes.first : _activeEventTheme!;
},
);
_activeEventTheme = theme;
_logger.logInfo('Debug theme activated: $themeType');
notifyListeners();
}
/// Manual activation (for testing or admin override)
void activateTheme(EventType type) {
_activeEventTheme = _availableThemes.firstWhere(
(theme) => theme.metadata.type == type,
orElse: () => _activeEventTheme!,
);
_logger.logInfo('Manual theme activation: ${type.toString()}');
notifyListeners();
}
/// Disable event theme (revert to standard)
void deactivateEventTheme() {
_activeEventTheme = null;
_logger.logInfo('Event theme manually deactivated');
notifyListeners();
}
/// Periodic check (call from app lifecycle)
void checkDaily() {
_logger.logInfo('Performing daily theme check');
_checkForActiveEvent();
notifyListeners();
}
/// Force refresh from Remote Config
Future<void> refresh() async {
_logger.logInfo('Forcing theme refresh from Remote Config');
await _remoteConfigService.refresh();
await _loadThemesFromRemoteConfig();
}
@override
void dispose() {
_remoteConfigService.removeListener(_onRemoteConfigUpdated);
super.dispose();
}
}
Phase 2: Color System Integration (Day 3)
Step 2.1: Extend AppColors Class
File: lib/theme/colors.dart (MODIFICATIONS)
class AppColors {
static bool isDarkMode = false;
static EventThemeManager? _themeManager; // NEW
// NEW: Initialize theme manager
static void initializeEventThemeManager(EventThemeManager manager) {
_themeManager = manager;
}
// NEW: Check if event theme overrides a color
static Color? _getEventColor(String colorKey) {
if (_themeManager?.hasActiveEvent ?? false) {
return _themeManager!.activeEventTheme!.getColor(colorKey, isDarkMode);
}
return null;
}
// MODIFIED: Add event override check
static Color get primaryRedColor {
final eventColor = _getEventColor('primaryBrand');
if (eventColor != null) return eventColor;
return isDarkMode ? dark.primaryRedColor : const Color(0xFFED1C2B);
}
// MODIFIED: Scaffold background with event support
static Color get scaffoldBgColor {
final eventColor = _getEventColor('scaffoldBackground');
if (eventColor != null) return eventColor;
return isDarkMode ? dark.scaffoldBgColor : const Color(0xFFF8F8F8);
}
// NEW: Special event features
static bool get hasEventGradient =>
_themeManager?.activeEventTheme?.backgroundGradient != null;
static LinearGradient? get eventGradient =>
_themeManager?.activeEventTheme?.backgroundGradient;
static String? get eventBannerImage =>
_themeManager?.activeEventTheme?.celebrationBannerImage;
static bool get showConfetti =>
_themeManager?.activeEventTheme?.showConfetti ?? false;
// ... existing code continues
}
Phase 3: ViewModel Integration (Day 4)
Step 3.1: Extend ProfileSettingsViewModel
File: lib/features/profile_settings/profile_settings_view_model.dart (MODIFICATIONS)
class ProfileSettingsViewModel extends ChangeNotifier {
// ... existing code ...
final EventThemeManager _eventThemeManager; // NEW
ProfileSettingsViewModel({
required CacheService cacheService,
required this.profileSettingsRepo,
required this.errorHandlerService,
required EventThemeManager eventThemeManager, // NEW
}) : _cacheService = cacheService,
_eventThemeManager = eventThemeManager {
// Listen to event theme changes
_eventThemeManager.addListener(_onEventThemeChanged);
}
// NEW: Event theme state
BaseEventTheme? get activeEventTheme => _eventThemeManager.activeEventTheme;
bool get hasActiveEvent => _eventThemeManager.hasActiveEvent;
String get eventName => activeEventTheme?.metadata.nameEn ?? '';
// NEW: Handle event theme changes
void _onEventThemeChanged() {
notifyListeners(); // Rebuild UI when event theme changes
}
// NEW: Manual event theme control (for admin/testing)
void activateEventTheme(EventType type) {
_eventThemeManager.activateTheme(type);
}
void deactivateEventTheme() {
_eventThemeManager.deactivateEventTheme();
}
// EXISTING: Dark mode toggle (unchanged)
void toggleDarkMode(bool value) {
_isDarkMode = value;
AppColors.isDarkMode = value;
_cacheService.saveBool(key: _darkModeKey, value: value);
notifyListeners();
}
@override
void dispose() {
_eventThemeManager.removeListener(_onEventThemeChanged);
super.dispose();
}
}
Phase 4: App Initialization (Day 5)
Step 4.1: Update Dependencies
File: lib/core/dependencies.dart (MODIFICATIONS)
Future<void> addDependencies() async {
// ... existing dependencies ...
// NEW: Register Event Theme Manager (Firebase-driven)
getIt.registerLazySingleton<EventThemeManager>(() {
return EventThemeManager();
// Themes are loaded from Firebase Remote Config
// No hardcoded themes needed!
});
// MODIFIED: Inject EventThemeManager into ProfileSettingsViewModel
getIt.registerLazySingleton<ProfileSettingsViewModel>(() =>
ProfileSettingsViewModel(
cacheService: getIt(),
profileSettingsRepo: getIt(),
errorHandlerService: getIt(),
eventThemeManager: getIt(), // NEW
)
);
}
Step 4.2: Update Main App
File: lib/main.dart (MODIFICATIONS)
Future<void> callInitializations() async {
// ... existing code ...
await AppDependencies.addDependencies();
// NEW: Initialize event theme system with Firebase Remote Config
final eventThemeManager = getIt.get<EventThemeManager>();
await eventThemeManager.initialize(); // Fetches from Firebase
AppColors.initializeEventThemeManager(eventThemeManager);
// Existing: Load dark mode
getIt.get<ProfileSettingsViewModel>().loadDarkMode();
// NEW: Check for active events (will use Firebase data)
eventThemeManager.checkDaily();
}
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(
// NEW: Include event theme in key to force rebuild
key: ValueKey('app_theme_${profileVm.isDarkMode}_${profileVm.hasActiveEvent}'),
theme: AppTheme.getTheme(isArabic),
darkTheme: AppTheme.getDarkTheme(isArabic),
themeMode: profileVm.isDarkMode ? ThemeMode.dark : ThemeMode.light,
// ... other properties
);
},
);
}
}
Step 4.3: App Lifecycle Integration
File: lib/services/app_lifecycle_service.dart (MODIFICATIONS)
class AppLifecycleService with WidgetsBindingObserver {
final EventThemeManager _eventThemeManager;
AppLifecycleService({
required EventThemeManager eventThemeManager,
}) : _eventThemeManager = eventThemeManager;
void initialize() {
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
// Refresh Remote Config when app comes to foreground
if (state == AppLifecycleState.resumed) {
_eventThemeManager.refresh();
}
}
void dispose() {
WidgetsBinding.instance.removeObserver(this);
}
}
Phase 5: UI Components & Firebase Console Setup (Day 5-6)
Step 5.1: Event Banner Widget (Optional)
File: lib/widgets/event_banner.dart (NEW)
class EventBanner extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ProfileSettingsViewModel>(
builder: (context, vm, _) {
if (!vm.hasActiveEvent) return SizedBox.shrink();
final theme = vm.activeEventTheme!;
final isArabic = context.locale.languageCode == 'ar';
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: theme.backgroundGradient,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
if (theme.celebrationBannerImage != null)
Image.asset(theme.celebrationBannerImage!, height: 40),
SizedBox(width: 12),
Text(
isArabic ? theme.metadata.nameAr : theme.metadata.nameEn,
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
);
}
}
Step 5.2: Confetti Effect (Optional)
File: lib/widgets/confetti_overlay.dart (NEW)
class ConfettiOverlay extends StatefulWidget {
final Widget child;
const ConfettiOverlay({required this.child});
@override
State<ConfettiOverlay> createState() => _ConfettiOverlayState();
}
class _ConfettiOverlayState extends State<ConfettiOverlay> {
late ConfettiController _controller;
@override
void initState() {
super.initState();
_controller = ConfettiController(duration: Duration(seconds: 3));
if (AppColors.showConfetti) {
_controller.play();
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
if (AppColors.showConfetti)
Align(
alignment: Alignment.topCenter,
child: ConfettiWidget(
confettiController: _controller,
// ... confetti configuration
),
),
],
);
}
}
🔄 Migration Strategy
For Existing Screens
No changes required! All existing code continues to work:
// Existing code - automatically gets event colors
Container(
color: AppColors.primaryRedColor, // Will be green on National Day
)
For New Features
Use event-aware colors:
// Check if event is active
if (context.read<ProfileSettingsViewModel>().hasActiveEvent) {
// Show event-specific UI
}
// Use event banner
EventBanner(),
// Use event gradient
Container(
decoration: BoxDecoration(
gradient: AppColors.eventGradient ?? defaultGradient,
),
)
🧪 Testing Strategy
Firebase Remote Config Testing
Unit Tests
// Test Remote Config parsing
test('Should parse National Day theme from Remote Config JSON', () {
final json = {
'id': 'saudi_national_day_2026',
'type': 'nationalDay',
'primaryColor': '#006C35',
// ... more fields
};
final config = RemoteThemeConfig.fromJson(json);
expect(config.id, 'saudi_national_day_2026');
expect(config.parseColor('#006C35'), Color(0xFF006C35));
});
// Test date-based activation
test('Should activate theme when current date is in range', () async {
final manager = EventThemeManager();
await manager.initialize();
// Mock Remote Config to return active theme
expect(manager.hasActiveEvent, true);
});
// Test priority system
test('Higher priority theme should win when multiple active', () async {
final manager = EventThemeManager();
// Remote Config returns: Eid (priority 15) + National Day (priority 10)
await manager.initialize();
expect(manager.activeEventTheme?.metadata.priority, 15);
});
// Test color override
test('Event color should override base color', () {
AppColors.initializeEventThemeManager(mockManager);
final color = AppColors.primaryRedColor;
expect(color, isNot(Color(0xFFED1C2B))); // Not default red
expect(color, Color(0xFF006C35)); // National Day green
});
Integration Tests
testWidgets('Theme should update when Remote Config changes', (tester) async {
await tester.pumpWidget(MyApp());
// Initial state - no event theme
expect(find.byType(EventBanner), findsNothing);
// Simulate Remote Config update
await mockRemoteConfig.setTheme(nationalDayTheme);
await tester.pumpAndSettle();
// Event banner should appear
expect(find.byType(EventBanner), findsOneWidget);
expect(find.text('Saudi National Day'), findsOneWidget);
});
testWidgets('Dark mode should work with event theme', (tester) async {
await tester.pumpWidget(MyApp());
// Activate National Day theme
final profileVm = tester.read<ProfileSettingsViewModel>();
profileVm.toggleDarkMode(true);
await tester.pumpAndSettle();
// Should use dark variant of event color
final container = tester.widget<Container>(find.byType(Container).first);
expect(container.color, Color(0xFF008A45)); // Dark green, not light green
});
Manual Testing Checklist
Basic Functionality
- Remote Config fetches successfully on app launch
- Theme activates when date range matches
- Theme deactivates when date range ends
- Multiple themes respect priority system
- Debug mode forces specific theme
- Global disable switch works (
enable_event_themes: false)
Color System
- Event colors override base colors correctly
- Dark mode variants display correctly
- Gradient displays when configured
- Invalid hex colors don't crash app
- Missing colors fallback to base theme
Dark/Light Mode Integration
- Toggle dark mode with event active
- Toggle dark mode with event inactive
- Event colors adapt to dark mode
- Existing screens work in both modes
UI Components
- Event banner displays in Arabic/English
- Banner image loads from URL
- Confetti plays when enabled
- Animations work smoothly
- Banner hides when event inactive
Firebase Console
- JSON validation works in console
- Publish changes propagate to app
- Conditional targeting works (region, language)
- A/B testing configuration works
- Rollback to previous config works
Performance
- Remote Config fetch doesn't block UI
- Theme changes are smooth (no jank)
- Color resolution is fast
- App startup time unchanged
- Memory usage normal
Edge Cases
- Invalid JSON doesn't crash app
- Network failure doesn't break app
- Empty theme array works
- Overlapping date ranges handled
- Past event dates ignored
- Future event dates wait
📊 Performance Considerations
Optimization 1: Remote Config Caching
class RemoteConfigService {
// Firebase SDK automatically caches for minimumFetchInterval
// Default: 1 hour (production), 0 seconds (debug)
Future<void> initialize() async {
await _remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(minutes: 1),
minimumFetchInterval: const Duration(hours: 1), // Cache for 1 hour
));
}
}
Optimization 2: Color Caching
class DynamicEventTheme {
final Map<String, Color?> _colorCache = {};
@override
Color? getColor(String colorKey, bool isDarkMode) {
final cacheKey = '${colorKey}_$isDarkMode';
if (_colorCache.containsKey(cacheKey)) {
return _colorCache[cacheKey];
}
final color = _computeColor(colorKey, isDarkMode);
_colorCache[cacheKey] = color;
return color;
}
}
Optimization 3: Lazy Theme Loading
class EventThemeManager {
// Themes are only created when Remote Config has data
// No hardcoded themes = smaller app bundle
Future<void> _loadThemesFromRemoteConfig() async {
// Only parse JSON when needed
final themesConfig = _remoteConfigService.getEventThemesConfig();
_availableThemes = themesConfig
.map((json) => DynamicEventTheme(RemoteThemeConfig.fromJson(json)))
.toList();
}
}
Optimization 4: App Lifecycle Optimization
class AppLifecycleService {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// Only refresh when app comes to foreground
// Not on every frame
_eventThemeManager.refresh();
}
}
}
🔥 Firebase Remote Config Setup Guide
Step 1: Firebase Console Configuration
1.1 Create Remote Config Parameters
Navigate to Firebase Console → Remote Config → Add parameter
Parameter 1: event_themes_config
- Type: JSON
- Description: Array of event theme configurations
- Default Value:
[](empty array)
Parameter 2: enable_event_themes
- Type: Boolean
- Description: Global on/off switch for event themes
- Default Value:
true
Parameter 3: debug_event_theme
- Type: String
- Description: Force specific theme for testing (e.g., "nationalDay")
- Default Value:
""(empty string)
Step 2: Event Theme Configuration Examples
Example 1: Saudi National Day Theme
[
{
"id": "saudi_national_day_2026",
"type": "nationalDay",
"nameEn": "Saudi National Day",
"nameAr": "اليوم الوطني السعودي",
"startDate": "2026-09-20T00:00:00Z",
"endDate": "2026-09-25T23:59:59Z",
"isActive": true,
"priority": 10,
"primaryColor": "#006C35",
"primaryColorDark": "#008A45",
"secondaryColor": "#FFFFFF",
"secondaryColorDark": "#E8E8E8",
"accentColor": "#FFD700",
"accentColorDark": "#FFC700",
"scaffoldBgColor": "#F5FFF5",
"scaffoldBgColorDark": "#0A2617",
"cardBgColor": "#FFFFFF",
"cardBgColorDark": "#1E3A24",
"appBarColor": "#006C35",
"appBarColorDark": "#008A45",
"gradientColors": ["#006C35", "#008A45", "#00A651"],
"gradientStart": "topLeft",
"gradientEnd": "bottomRight",
"bannerImageUrl": "https://yourdomain.com/images/national_day_banner.png",
"showConfetti": true,
"enableAnimations": true
}
]
Example 2: Ramadan Theme
[
{
"id": "ramadan_2026",
"type": "ramadan",
"nameEn": "Ramadan Kareem",
"nameAr": "رمضان كريم",
"startDate": "2026-02-18T00:00:00Z",
"endDate": "2026-03-20T23:59:59Z",
"isActive": true,
"priority": 12,
"primaryColor": "#1C3664",
"primaryColorDark": "#2A4A7F",
"secondaryColor": "#FFD700",
"secondaryColorDark": "#FFC700",
"accentColor": "#F5C842",
"accentColorDark": "#E8B923",
"scaffoldBgColor": "#F0F4FF",
"scaffoldBgColorDark": "#0D1929",
"gradientColors": ["#1C3664", "#2A4A7F", "#3D5FA8"],
"gradientStart": "topCenter",
"gradientEnd": "bottomCenter",
"bannerImageUrl": "https://yourdomain.com/images/ramadan_banner.png",
"showConfetti": false,
"enableAnimations": true
}
]
Example 3: Multiple Active Events (Priority System)
[
{
"id": "eid_al_fitr_2026",
"type": "eid",
"nameEn": "Eid Al-Fitr",
"nameAr": "عيد الفطر",
"startDate": "2026-03-21T00:00:00Z",
"endDate": "2026-03-24T23:59:59Z",
"isActive": true,
"priority": 15,
"primaryColor": "#6B4FA0",
"primaryColorDark": "#8A6BB8",
"secondaryColor": "#FFD700",
"accentColor": "#E8B923",
"showConfetti": true,
"enableAnimations": true
},
{
"id": "saudi_founding_day_2026",
"type": "foundingDay",
"nameEn": "Saudi Founding Day",
"nameAr": "يوم التأسيس السعودي",
"startDate": "2026-02-20T00:00:00Z",
"endDate": "2026-02-24T23:59:59Z",
"isActive": true,
"priority": 8,
"primaryColor": "#8B4513",
"primaryColorDark": "#A0522D",
"accentColor": "#D2691E",
"showConfetti": false
},
{
"id": "new_year_2027",
"type": "newYear",
"nameEn": "Happy New Year 2027",
"nameAr": "سنة جديدة سعيدة ٢٠٢٧",
"startDate": "2026-12-30T00:00:00Z",
"endDate": "2027-01-02T23:59:59Z",
"isActive": true,
"priority": 5,
"primaryColor": "#0F4C81",
"accentColor": "#C0C0C0",
"showConfetti": true
}
]
Step 3: Testing Configurations
Debug Mode Configuration
To test a specific theme without waiting for dates:
Set debug_event_theme to:
"nationalDay"- Force National Day theme"ramadan"- Force Ramadan theme"eid"- Force Eid theme""(empty) - Disable debug mode
Disable All Event Themes
Set enable_event_themes to: false
This will instantly disable all event themes across all users.
Step 4: Conditional Targeting (Advanced)
Firebase Remote Config supports conditional targeting:
Example: Saudi Arabia Only
Condition: Country/Region == SA
Example: Arabic Language Users Only
Condition: Languages includes ar
Example: Beta Users
Condition: User in audience "beta_testers"
Example: A/B Testing
Condition A (50%): Show National Day theme with confetti
Condition B (50%): Show National Day theme without confetti
Step 5: Rollout Strategy
Gradual Rollout
- Day 1: 10% of users
- Day 2: 25% of users
- Day 3: 50% of users
- Day 4: 100% of users
Instant Activation
- Set date range to current date
- Publish configuration
- Users will see theme on next app foreground (auto-refresh)
Emergency Rollback
- Set
enable_event_themestofalse - OR set theme
isActivetofalse - Publish immediately
- Users revert to standard theme within 1 hour (or next app launch)
Step 6: Color Palette Reference
Saudi National Day Colors
Primary Green: #006C35 (Light) / #008A45 (Dark)
Saudi White: #FFFFFF (Light) / #E8E8E8 (Dark)
Gold Accent: #FFD700
Background: #F5FFF5 (Light) / #0A2617 (Dark)
Ramadan Colors
Night Blue: #1C3664 (Light) / #2A4A7F (Dark)
Crescent Gold: #F5C842 (Light) / #E8B923 (Dark)
Background: #F0F4FF (Light) / #0D1929 (Dark)
Eid Colors
Islamic Purple: #6B4FA0 (Light) / #8A6BB8 (Dark)
Festive Gold: #FFD700
Light Gold: #E8B923
New Year Colors
Midnight Blue: #0F4C81
Silver: #C0C0C0
Champagne Gold: #F7E7CE
Step 7: Best Practices
✅ Do's
- Always provide both light and dark color variants
- Use ISO 8601 format for dates:
"2026-09-20T00:00:00Z" - Test theme in both English and Arabic
- Start events 1-2 days before actual date for early celebration
- End events 1 day after to allow for celebrations
- Use priority system: Higher numbers = higher priority
- Test with
debug_event_themebefore production release
❌ Don'ts
- Don't overlap same-priority events (use priority system)
- Don't use invalid hex colors (use # prefix)
- Don't set very long date ranges (affects performance)
- Don't forget to set
isActive: true - Don't use gradients with less than 2 colors
- Don't set all themes to priority 999 (defeats purpose)
Step 8: Monitoring & Analytics
Track these metrics in Firebase Analytics:
// Log when theme is activated
GALogger('event_theme_activated', parameters: {
'theme_id': 'saudi_national_day_2026',
'theme_type': 'nationalDay',
'is_dark_mode': isDarkMode,
'priority': 10,
});
// Log user engagement
GALogger('event_theme_viewed', parameters: {
'theme_id': themeId,
'session_duration': sessionDuration,
'confetti_shown': showConfetti,
});
// Log errors
GALogger('event_theme_error', parameters: {
'error_type': 'invalid_color',
'theme_id': themeId,
'details': errorDetails,
});
Step 9: Quick Reference - Firebase Console Steps
- Open Firebase Console → Your Project
- Navigate to Remote Config (left sidebar)
- Click "Add parameter"
- Enter parameter name:
event_themes_config - Select type: JSON
- Paste theme configuration JSON
- Click "Publish changes"
- Wait ~1-5 minutes for propagation
- Test app (pull down to refresh OR restart app)
- Verify theme is active in your app
Step 10: Troubleshooting
| Issue | Solution |
|---|---|
| Theme not appearing | Check date ranges, ensure isActive: true |
| Wrong theme active | Check priority values (higher = wins) |
| Colors not changing | Verify hex format (#RRGGBB), check dark mode |
| App crashes | Validate JSON syntax in Firebase Console |
| Gradient not showing | Ensure 2+ colors, valid alignment values |
| Confetti not working | Check showConfetti: true in config |
| Debug theme stuck | Set debug_event_theme to empty string "" |
| Remote Config not fetching | Check network, Firebase SDK version |
🌐 Remote Configuration (Optional - Phase 7)
Firebase Remote Config Integration
File: lib/core/remote_config_service.dart (NEW)
class RemoteConfigService {
final FirebaseRemoteConfig _remoteConfig = FirebaseRemoteConfig.instance;
Future<void> initialize() async {
await _remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: Duration(minutes: 1),
minimumFetchInterval: Duration(hours: 1),
));
await _remoteConfig.setDefaults({
'active_event_themes': '[]',
'enable_confetti': false,
});
await _remoteConfig.fetchAndActivate();
}
List<Map<String, dynamic>> getActiveEventThemes() {
final json = _remoteConfig.getString('active_event_themes');
return jsonDecode(json);
}
bool isConfettiEnabled() {
return _remoteConfig.getBool('enable_confetti');
}
}
Remote Theme Activation
{
"active_event_themes": [
{
"type": "nationalDay",
"startDate": "2026-09-20T00:00:00Z",
"endDate": "2026-09-24T23:59:59Z",
"isActive": true,
"priority": 10
}
],
"enable_confetti": true
}
📅 Implementation Timeline
| Phase | Duration | Deliverables |
|---|---|---|
| Phase 1 | 2 days | Base infrastructure, models, manager |
| Phase 2 | 1 day | Concrete event themes (4 themes) |
| Phase 3 | 1 day | Color system integration |
| Phase 4 | 1 day | ViewModel integration |
| Phase 5 | 1 day | App initialization, DI setup |
| Phase 6 | 1 day | UI components (banner, confetti) |
| Phase 7 | 1 day | Remote config (optional) |
| Testing | 2 days | Unit tests, manual testing |
| Total | 10 days | Full implementation + testing |
✅ Benefits Summary
1. Minimal Code Changes
- Zero breaking changes to existing screens
- Only 4 files modified, 8 files added
- Existing dark/light mode logic preserved
2. Scalability
- Add new event themes by creating single class
- Unlimited simultaneous events (priority-based)
- Easy to extend with new color overrides
3. Flexibility
- Auto-activation based on dates
- Manual override for testing/admin
- Remote configuration support
- Per-event dark mode variants
4. Maintainability
- Clear separation of concerns
- Type-safe theme definitions
- Centralized event management
- Easy debugging with event state
5. User Experience
- Seamless theme transitions
- Festive UI without disruption
- Preserves user's dark mode preference
- Optional celebration effects (confetti, banners)
🚀 Quick Start After Approval
- Create base infrastructure (Phase 1)
- Implement 1 event theme as POC (National Day)
- Test with dark mode toggle
- Get stakeholder approval
- Implement remaining themes
- Add UI enhancements
- Deploy with remote config
📝 Notes & Considerations
A. Hijri Calendar Support
For Islamic events (Ramadan, Eid), integrate Hijri calendar:
// Use package: hijri
import 'package:hijri/hijri.dart';
DateTime getRamadanStartDate(int gregorianYear) {
// Convert Hijri date to Gregorian
final hijri = HijriCalendar.fromDate(DateTime(gregorianYear, 1, 1));
// Find Ramadan (month 9)
// ...
}
B. Asset Management
Store event-specific assets:
assets/
└── images/
└── events/
├── national_day_banner.png
├── eid_banner.png
├── ramadan_crescent.png
└── new_year_fireworks.png
C. Analytics Integration
Track event theme engagement:
GALogger('event_theme_viewed', parameters: {
'theme_type': 'national_day',
'is_dark_mode': isDarkMode,
});
🎯 Success Criteria
- National Day theme activates automatically Sept 20-24
- Theme works in both light and dark modes
- Existing screens show event colors without modification
- User can toggle dark mode while event is active
- Multiple events respect priority system
- Theme deactivates automatically after event
- Event banner displays correctly in Arabic/English
- Performance impact < 5ms per frame
- Remote config can enable/disable themes
- Zero crashes related to theme switching
Created: May 15, 2026
Author: AI Architecture Team
Status: Ready for Implementation
Version: 1.0