Dark and light mode in Flutter means defining separate ThemeData configurations for light and dark themes in MaterialApp, then controlling which one is active through themeMode.
MaterialApp accepts theme, darkTheme, and themeMode as separate parameters.CupertinoApp does not expose darkTheme or themeMode. It accepts a single CupertinoThemeData through the theme.ThemeMode.system follows the device's current system brightness; ThemeMode.light and ThemeMode.dark force the app to use the light or dark theme respectively.MediaQuery.platformBrightnessOf(context) reads the current system brightness directly, without going through ThemeMode.Theme.of(context).brightness reads the brightness of the app's currently active ThemeData — that is, the resolved result of theme, darkTheme, and themeMode. By contrast, MediaQuery.platformBrightnessOf(context) reads the system brightness.ColorScheme.fromSeed(seedColor: ..., brightness: Brightness.dark) generates a dark color scheme from the same seed color used for the light scheme.ThemeMode.system follows the platform's system theme setting. On platforms that support system dark mode, it reflects the current system theme.shared_preferences.Provide both themes and default to following the system setting with ThemeMode.system.
import 'package:flutter/material.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal)),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
home: const HomeScreen(),
);
}
}Using the same seedColor for both themes keeps the generated color schemes visually related, while the brightness parameter generates a light or dark variant of the scheme.
A ValueNotifier<ThemeMode> combined with a ValueListenableBuilder above MaterialApp is enough for a manual toggle without a full state management package.
final themeModeNotifier = ValueNotifier(ThemeMode.system);
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ThemeMode>(
valueListenable: themeModeNotifier,
builder: (context, mode, _) {
return MaterialApp(
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal)),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
),
),
themeMode: mode,
home: const HomeScreen(),
);
},
);
}
}
// Anywhere in the app:
// themeModeNotifier.value = ThemeMode.dark;Changing themeModeNotifier.value notifies the ValueListenableBuilder, which rebuilds its subtree with the new themeMode. Because MaterialApp is inside that subtree, it rebuilds and applies the corresponding theme across the app.
ThemeMode.system so the app matches the device setting unless the user explicitly overrides it.shared_preferences or similar, so the choice survives an app restart.ColorScheme.fromSeed, using the appropriate brightness for each theme.Theme.of(context).brightness for styling decisions. Use ColorScheme and other theme properties that adapt to the active theme automatically; check Theme.of(context).brightness when you genuinely need to know the active theme's brightness.MaterialApp uses AnimatedTheme to animate changes to the active ThemeData, so changing themeMode already produces an animated theme transition.ThemeMode.system is usually the better default.Theme.of(context).brightness to choose colors manually when the theme already provides the appropriate color. Prefer ColorScheme and other theme properties that adapt to the active theme automatically.
Flutter is known for its seamless UI/UX features and robust design elements. It allowed us to deliver a unique customer experience in the “CA24 Mobile” banking app. This article shares some of the learnings and pain points behind implementing it in this large project.

Starting with Android 15, edge-to-edge becomes the default - bringing a modern, immersive feel to apps. In this article, our Flutter developer shows how to handle system bars in Flutter across Android versions and prepare your UI for Android 16, where edge-to-edge will be mandatory.