Hey, I'm Ekaksh.
In this post I'm going to walk you through how I set up dark mode and light mode in Flutter using nothing besides ValueNotifier and ValueListenableBuilder.
This is exactly the same setup I showed in my YouTube video.
why this pattern
I use this in basically every Flutter app I build because it's clean, simple, and works everywhere in the app without dragging in state-management packages just for toggling a theme.
Let's jump into it.
Create a new project and install dependencies (optional)
Create a new Flutter project:
flutter create theme_1
cd theme_1Add shared_preferences if you want to persist the theme choice:
flutter pub add shared_preferencesNow, just remove the boilerplate from main.dart, create your usual app.dart and home.dart, and get a basic MaterialApp running.
Create the Theme Notifier (this is the core)
We need something to tell the entire app:
"Theme mode changed — rebuild yourself."
Flutter already gives us ValueNotifier, so we'll just use that. Let's create a theme_notifier.dart file and write the following code:
class ThemeNotifier {
static final ThemeNotifier _instance = ThemeNotifier._internal();
factory ThemeNotifier() => _instance;
ThemeNotifier._internal();
final ValueNotifier<ThemeMode> themeModeNotifier = ValueNotifier<ThemeMode>(
ThemeMode.system,
);
Future<void> loadThemeMode() async {
// TODO: Load the saved theme mode from local storage
}
void toggleTheme() {
// TODO: Toggle between light and dark theme modes and save the updated preference
}
void saveThemeMode(ThemeMode mode) {
// TODO: Save the selected theme mode to local storage and update the notifier
// Updating the notifier to rebuild the listeners
}
}why a singleton?
Because I want to access this from anywhere in the app without passing BuildContext around like it's hot potato.
Persist the theme with SharedPreferences (optional)
If someone switches to dark mode, closes the app, opens it again — the app should stay in dark mode.
For this, we can use the shared_preferences package we added earlier. Let's create a class called LocalStorage in a file called local_storage.dart to handle saving and retrieving things from local storage:
class LocalStorage {
static SharedPreferences? _prefs;
static SharedPreferences? get prefs => _prefs;
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
static Future<void> set(String key, dynamic value) async {
if (prefs == null) await init();
if (value is String) {
await _prefs?.setString(key, value);
} else if (value is int) {
await _prefs?.setInt(key, value);
} else if (value is bool) {
await _prefs?.setBool(key, value);
} else if (value is double) {
await _prefs?.setDouble(key, value);
} else if (value is List<String>) {
await _prefs?.setStringList(key, value);
} else {
throw Exception("Unsupported value type");
}
}
static T? get<T>(String key, {T? defaultValue}) {
if (prefs == null) return defaultValue;
if (T == String) {
return _prefs?.getString(key) as T? ?? defaultValue;
} else if (T == int) {
return _prefs?.getInt(key) as T? ?? defaultValue;
} else if (T == bool) {
return _prefs?.getBool(key) as T? ?? defaultValue;
} else if (T == double) {
return _prefs?.getDouble(key) as T? ?? defaultValue;
} else if (T == List<String>) {
return _prefs?.getStringList(key) as T? ?? defaultValue;
} else {
throw Exception("Unsupported value type");
}
}
static Future<void> remove(String key) async {
if (prefs == null) await init();
await _prefs?.remove(key);
}
static Future<void> clear() async {
if (prefs == null) await init();
await _prefs?.clear();
}
}Complete the theme notifier with persistence
Now let's complete the theme notifier by implementing loadThemeMode, saveThemeMode, and toggleTheme:
class ThemeNotifier {
static final ThemeNotifier _instance = ThemeNotifier._internal();
factory ThemeNotifier() => _instance;
ThemeNotifier._internal();
final ValueNotifier<ThemeMode> themeModeNotifier = ValueNotifier<ThemeMode>(
ThemeMode.system,
);
Future<void> loadThemeMode() async {
final savedTheme = LocalStorage.get<String>(
"theme_mode",
defaultValue: "system",
);
if (savedTheme == null) {
themeModeNotifier.value = ThemeMode.system;
return;
}
if (savedTheme == ThemeMode.light.name) {
themeModeNotifier.value = .light;
} else if (savedTheme == ThemeMode.dark.name) {
themeModeNotifier.value = .dark;
} else {
themeModeNotifier.value = .system;
}
}
void toggleTheme() {
if (themeModeNotifier.value == ThemeMode.light) {
saveThemeMode(.dark);
} else {
saveThemeMode(.light);
}
}
void saveThemeMode(ThemeMode mode) {
themeModeNotifier.value = mode;
LocalStorage.set("theme_mode", mode.name);
}
}Setup the material app to listen to theme changes
In your app.dart, wrap your MaterialApp with a ValueListenableBuilder that listens to themeModeNotifier:
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: ThemeNotifier().themeModeNotifier,
builder: (_, themeMode, _) {
return MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: themeMode,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: const Home(),
);
},
);
}
}Finally in your main.dart, initialize LocalStorage and call loadThemeMode before running the app:
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
await bootStrap();
runApp(const App());
}
Future<void> bootStrap() async {
await LocalStorage.init();
await ThemeNotifier().loadThemeMode();
}Setup your home.dart
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Theme Notifier Example"),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Home Screen"),
ElevatedButton(
onPressed: () => ThemeNotifier().toggleTheme(),
child: const Text("Toggle Theme"),
),
],
),
),
);
}
}that's it
You now have a working dark mode and light mode toggle, with persistence via local storage and zero external state-management packages. Just simple ValueNotifiers doing their job.
Here is the full source code: Theme Notifier Tutorial.
Thanks for reading — hope this helps you keep your theming clean.