Why users abandon apps due to theme issues
Users expect the app to adapt to their preferences: dark theme in the subway, accent color matching the phone wallpaper, at least two modes. If theme switching has a delay or flash, the user leaves. We've seen projects where implementing themes took 3 months and resulted in 5 buggy screens. Over 10 years we've developed an architecture that works on all platforms and doesn't tolerate mistakes. One common problem is the flash of wrong theme: on first launch, the screen flashes light theme, then switches to the saved dark theme. This happens if the theme is read asynchronously. On iOS it's solved by synchronous reading of UserDefaults in AppDelegate, on Android via SplashScreen API. Proper implementation eliminates this effect and retains the user.
Contact us for an estimate of your project — we will analyze your current architecture and propose the optimal solution.
Architecture of the theme system
The key decision is how to store the current theme and how to pass it to components.
iOS (SwiftUI)
We use @Environment + custom EnvironmentKey. Create AppTheme as ObservableObject, publish via environmentObject, all components read via @EnvironmentObject var theme: AppTheme. When theme.colorScheme changes, SwiftUI automatically redraws the entire tree. Switching is instant, without UIApplication.shared.windows hacks.
iOS (UIKit)
More complex: UIAppearance proxy for global settings + traitCollection override. Or a custom ThemeManager via Notification Center: on theme change, all subscribed components call applyTheme(). Downside: need to explicitly unsubscribe, easy to get retain cycles.
Android (Compose)
MaterialTheme(colorScheme = currentColorScheme) in the root of composition. CompositionLocalProvider(LocalAppTheme provides theme). When remember { mutableStateOf(lightColorScheme) } changes, Compose re-composes only the subtrees that read the theme. Very efficient.
React Native
ThemeContext via React Context API + useContext. Or a ready-made solution via styled-components/native with ThemeProvider. On theme change, all components subscribed to the context re-render. To prevent unnecessary renders: React.memo + useMemo for the theme object.
Flutter
MaterialApp(theme: lightTheme, darkTheme: darkTheme, themeMode: themeMode). Custom themes — ThemeExtension<T>. ThemeMode.system / .light / .dark managed via setState or Provider/Riverpod.
How to ensure theme persistence without restart?
The chosen theme needs to be saved. iOS: UserDefaults + @AppStorage in SwiftUI. Android: DataStore<Preferences> (recommended over SharedPreferences). React Native: AsyncStorage or MMKV for synchronous access. Flutter: SharedPreferences or Hive.
Critical point: on first launch, the theme must be applied before the user sees the first frame. Otherwise there will be a flash of wrong theme — the screen flashes from the default theme to the saved one. On iOS it's solved by synchronous reading from UserDefaults in AppDelegate / @main before the window is drawn. On Android — via SplashScreen API with the correct background color. We guarantee the absence of this effect.
How to avoid memory leaks when switching themes?
With active theme switching, it's easy to create many subscriptions and listeners that are not cleaned up. On iOS we use Combine with AnyCancellable and store(in: &cancellables), on Android — Flow with collect in lifecycleScope. We test on 10+ devices, checking for leaks via Instruments and Android Profiler. The savings on debugging can be significant — preventing bugs early pays off the investment.
How to implement dynamic themes: step-by-step plan
- Audit the current UI: identify all places where colors, fonts, icons are used. Determine which components need adaptation.
- Design the palette: define a set of colors (primary, secondary, background, surface, error) for light and dark schemes. If a custom accent is needed, plan generation from a seed color.
- Choose the storage mechanism: based on the platform, select persistent storage (UserDefaults, DataStore, AsyncStorage, SharedPreferences). Ensure reading happens synchronously before the first frame renders.
- Implement on the platform: integrate the theme system according to best practices (Environment, Context, ThemeProvider). Write tests to verify correct theme switching without flashing.
- Test on 10+ devices: check on different OS versions, with different settings (System theme, accessibility). Use Instruments / Android Profiler for memory leaks.
How to test theme switching?
Test that reveals most issues: open a screen with complex UI → switch theme 5–10 times quickly → ensure no flash, no memory leaks (Instruments / Android Profiler), and all colors are applied correctly. Special attention: UIAlertController, UIActivityViewController, system components on iOS — they don't always react to custom themes and require separate handling.
Testing checklist for dynamic themes:
- Check switching light/dark theme on the main screen and all child screens.
- Switch theme rapidly 10 times in a row — no memory leaks, no flickering.
- Ensure system components (ActionSheet, ShareSheet) use the correct theme.
- Verify theme persistence after app restart.
- Test with Material You (Android 12+) — should pick up colors from wallpapers.
- On iOS — check with Dark Mode active in the system and without.
Custom accent color
Android 12+ supports Material You — a dynamic palette is generated from the user's wallpaper via DynamicColors.applyToActivitiesIfAvailable(this). Result: the app automatically adapts colors to the phone's personalization. Palette generation takes less than 5 ms, which is 10 times faster than custom color processing.
For a custom color picker inside the app: you need to generate a full ColorScheme from the selected seed color. On Android this is dynamicDarkColorScheme / dynamicLightColorScheme (API 31+) or the material-color-utilities library for API <31. On iOS — manual calculation of derived colors via HSL.
Common mistakes when implementing themes
| Mistake | Consequence | Solution |
|---|---|---|
| Asynchronous theme reading | Flash of wrong theme | Synchronous reading before first frame |
| Ignoring system theme | No automatic switching | Use ThemeMode.system |
| Hardcoding colors | Difficult maintenance | Externalize all colors into a palette |
| No testing on tablets | Uneven color changes | Add to checklist |
What's included in dynamic theme development?
- Analysis of current app architecture
- Design of the theme system (palette, storage and propagation methods)
- Implementation on the chosen stack (iOS/Android/Flutter/RN) following best practices
- Integration of theme saving and restoration
- Testing on 10+ physical devices and emulators
- Documentation for maintenance and further development
- 30 days of free support after delivery
| Scope of work | Duration |
|---|---|
| Only dark/light switching | 1–2 days |
| Multiple predefined themes | 2–3 days |
| Dynamic accent color | 3–5 days |
| Material You (Android) + full system | 4–6 days |
Developing dynamic themes is an investment that quickly pays off through increased user retention. Order dynamic theme implementation — and we'll ensure a seamless transition. Get 30 days of free support and consultations. Our team has 10+ years of experience in mobile development and Apple and Google certifications.
Additional information: Human Interface Guidelines — official Apple recommendations for working with color.







