Flutter Dark Mode: Light/Dark Theme Switching

Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter Dark Mode: Light/Dark Theme Switching, with theme, darktheme, and thememode, thememode.system as an honest default, and manual switching with valuenotifier or provider.
Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter Dark Mode: Light/Dark Theme Switching, with theme, darktheme, and thememode, thememode.system as an honest default, and manual switching with valuenotifier or provider.

A video restart exposes theme flashes that are easy to miss in isolated screenshots.

Subscribe on YouTube@codingliquids

Supply theme darkTheme and themeMode

MaterialApp selects theme or darkTheme according to themeMode and platform brightness. Define both schemes instead of applying a dark background to a light component palette.

Omitting darkTheme makes ThemeMode.dark fall back to a generic dark theme rather than the brand design.

flutter pub add shared_preferences

Start with ThemeMode.system

ThemeMode.system follows the user device setting and updates when platform brightness changes. It is a respectful default until the user explicitly chooses an override.

Reading platformBrightness once at startup misses later system changes.

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: any

An Instagram dark-mode audit covers surfaces, artwork, charts, system chrome, and saved preference.

Follow me on Instagram@sagnikteaches

Derive matching schemes with brightness

Create light and dark ColorScheme.fromSeed calls with the same seed and different Brightness values. The generated tonal roles preserve identity while adapting surface and on-colour contrast.

Inverting raw colours manually rarely produces a complete accessible dark palette.

The Complete Flutter Guide course thumbnail

Build production-ready Dark Mode features

The Complete Flutter Guide turns dark mode into maintainable app architecture, polished UI, and testable production code.

Enrol now

Shared preferences enters only at the persistence step, leaving ThemeMode switching independent of storage.

import 'package:shared_preferences/shared_preferences.dart';

Switch modes through ValueNotifier

A ValueNotifier and ValueListenableBuilder can rebuild MaterialApp when the setting changes. Offer system, light, and dark choices rather than a boolean if users can return to automatic mode.

Creating the notifier inside build resets the selection on every rebuild.

final themeMode = ValueNotifier(ThemeMode.system);

ValueListenableBuilder<ThemeMode>(
  valueListenable: themeMode,
  builder: (_, mode, __) => MaterialApp(
    theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal)),
    darkTheme: ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal, brightness: Brightness.dark),
    ),
    themeMode: mode,
    home: SettingsPage(onModeChanged: (value) => themeMode.value = value),
  ),
);

Persist the selected ThemeMode

Store mode.name with shared_preferences and parse it during startup, falling back to system for missing or unknown values. Write only after a user choice and keep the notifier value in sync with the successful preference update.

Storing an enum index is fragile when enum order changes.

Future<ThemeMode> loadThemeMode() async {
  final prefs = await SharedPreferences.getInstance();
  final saved = prefs.getString('themeMode');
  return ThemeMode.values.where((mode) => mode.name == saved).firstOrNull
      ?? ThemeMode.system;
}

Future<void> saveThemeMode(ThemeMode mode) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('themeMode', mode.name);
}

The LinkedIn theming discussion separates platform brightness from user intent and effective appearance.

Connect on LinkedInSagnik Bhattacharya
The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Adapt individual artwork to brightness

Theme.of(context).brightness can select assets, shadows, overlays, and system UI treatments that cannot come from ColorScheme alone. Keep branches small and semantic, using theme roles for ordinary colours.

Checking MediaQuery platform brightness ignores an explicit in-app override.

final brightness = Theme.of(context).brightness;
final logoAsset = brightness == Brightness.dark
    ? 'assets/logo-light.png'
    : 'assets/logo-dark.png';

AnnotatedRegion<SystemUiOverlayStyle>(
  value: brightness == Brightness.dark
      ? SystemUiOverlayStyle.light
      : SystemUiOverlayStyle.dark,
  child: Image.asset(logoAsset),
)

Make brightness a complete product state

Separate the user’s preference from the effective platform brightness. A three-way choice—system, light, or dark—preserves intent when the operating system changes later. Persist that enum rather than saving only the currently resolved boolean. At startup, load it early enough to avoid a bright flash, or provide a neutral launch surface while preferences initialise.

Dark mode is not a mechanical inversion. Build a dedicated dark ColorScheme, inspect foreground contrast on every surface, and reduce large bright areas that cause glare. Images, illustrations, charts, code blocks, dividers, selection colours, and disabled states all need review. Elevation may be communicated with tonal separation or overlays rather than the same dark shadow used in the light design.

System chrome should follow the surface behind it. Update status-bar and navigation-bar icon brightness through an annotated region or app-bar configuration, then inspect Android and iOS devices because their system areas differ. Modal routes, splash screens, native pickers, and embedded web content can expose an unthemed flash even when ordinary Flutter routes are correct. Do not infer a person’s choice from time of day when the application already offers an explicit setting.

Test system mode while changing operating-system brightness, all three saved choices after process death, increased contrast, large text, and every brand illustration. A widget test can inject platform brightness and verify the effective theme without depending on the test machine. Analytics should record only the coarse preference if it is genuinely useful and disclosed. Animating a theme change can help preserve context, but respect reduced motion and ensure custom theme extensions interpolate. The switch itself must announce its current value and remain visible against both surfaces before the rest of the screen is considered finished.

Cached HTML, maps, charts, and remote images can defeat an otherwise complete dark theme. Pass an explicit appearance to embedded content where supported and provide a neutral surrounding surface while it loads. For data visualisation, choose series colours against both backgrounds and test overlapping lines, tooltips, and print or export output. Screenshots and shared images may need a fixed presentation independent of the viewer’s mode. State restoration should preserve the selected option, while “system” continues following live platform changes; these are separate behaviours and deserve separate tests.

Automated contrast tools are useful, but translucent layers and gradients still need checks in their composed state. Sample the rendered result and validate critical text by eye with assistive settings enabled.

Launch surfaces deserve special attention because they appear before the Flutter theme is available. Configure native splash colours for both brightness variants where the platform permits, and avoid a white asset background around a transparent dark-mode logo. If the saved preference cannot be read before startup, a neutral system-matched surface is less jarring than painting light mode and switching a moment later. Test a terminated launch, not only hot restart.

Pure black is not automatically the best dark surface. Slight tonal differences can express hierarchy, reduce halation around bright text, and keep cards distinct without heavy shadows. OLED power savings may matter for large areas, but readability and brand requirements still need evidence. Review screenshots, charts, maps, syntax highlighting, web views, and user-generated images at low display brightness. A contrast ratio measured from token values can miss translucent overlays, so sample the composed pixels for critical labels. Finally, confirm that scheduled notifications, exported images, and shared documents use an intentional appearance rather than inheriting an unavailable build context.

Desktop window chrome and browser theme-colour metadata should also follow the active surface where the host platform exposes them. Otherwise a finished dark route can still sit inside a bright outer frame. Verify these details after packaging, because development runners do not always match the installed application.

Common mistakes

  • Supply theme darkTheme and themeMode: In Flutter dark mode, omitting darkTheme makes ThemeMode.dark fall back to a generic dark theme rather than the brand design; inspect this Flutter dark mode cause before changing another Flutter dark mode widget.
  • Start with ThemeMode.system: In Flutter dark mode, reading platformBrightness once at startup misses later system changes; inspect this Flutter dark mode cause before changing another Flutter dark mode widget.
  • Derive matching schemes with brightness: In Flutter dark mode, inverting raw colours manually rarely produces a complete accessible dark palette; inspect this Flutter dark mode cause before changing another Flutter dark mode widget.
  • Switch modes through ValueNotifier: In Flutter dark mode, creating the notifier inside build resets the selection on every rebuild; inspect this Flutter dark mode cause before changing another Flutter dark mode widget.
  • Persist the selected ThemeMode: In Flutter dark mode, storing an enum index is fragile when enum order changes; inspect this Flutter dark mode cause before changing another Flutter dark mode widget.
  • Adapt individual artwork to brightness: In Flutter dark mode, checking MediaQuery platform brightness ignores an explicit in-app override; inspect this Flutter dark mode cause before changing another Flutter dark mode widget.

Frequently asked questions

How does supply theme darkTheme and themeMode work in Flutter dark mode?

For Flutter dark mode, the starting rule is that materialApp selects theme or darkTheme according to themeMode and platform brightness. Apply this Flutter dark mode rule first because supply theme darkTheme and themeMode determines whether the Flutter dark mode pattern fits.

Why does derive matching schemes with brightness matter for Flutter dark mode?

In Flutter dark mode, the generated tonal roles preserve identity while adapting surface and on-colour contrast. Keeping derive matching schemes with brightness at the Flutter dark mode call site exposes the Flutter dark mode return value directly.

What failure should I test first in Flutter dark mode?

First reproduce the Flutter dark mode case where storing an enum index is fragile when enum order changes. The persistence test saves a stable ThemeMode name, survives process death, and still follows platform brightness when the stored choice is system.

How can I verify Flutter dark mode before release?

Exercise adapt individual artwork to brightness with real Flutter dark mode inputs on every shipped platform. Inspect the final Flutter dark mode callback or output; a successful Flutter dark mode button tap alone is not proof.

Further reads

Connect Flutter dark mode to the surrounding Flutter stack through these published tutorials:

Sources: Flutter framework and Dart API documentation; Flutter dark mode examples verified against current stable Flutter.