how to implement dark mode in flutter
I want to create a flutter app that has 2 light and dark mode themes that change by a switch in-app and the default theme is default android theme.
I need to pass some custom color to the fellow widget and I don't want to just config material theme.
- how to detect the user device default theme?
- the secend question is how to provide a theme to the whole app?
- third is how change the theme with a simple switch in running time?
Solution 1:
Using Material App
MaterialApp(
title: 'App Title',
theme: ThemeData(
brightness: Brightness.light,
/* light theme settings */
),
darkTheme: ThemeData(
brightness: Brightness.dark,
/* dark theme settings */
),
themeMode: ThemeMode.dark,
/* ThemeMode.system to follow system theme,
ThemeMode.light for light theme,
ThemeMode.dark for dark theme
*/
debugShowCheckedModeBanner: false,
home: YourAppHomepage(),
);
Using CupertinoApp
-
Detect the dark mode using,
WidgetsBinding.instance?.window.platformBrightness
-
You may also have to listen for the brightness changes from the system in order to update in real-time using
WidgetsBindingObserver
, and overriding,didChangePlatformBrightness();
CupertinoApp Example:
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
Brightness? _brightness;
@override
void initState() {
WidgetsBinding.instance?.addObserver(this);
_brightness = WidgetsBinding.instance?.window.platformBrightness;
super.initState();
}
@override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
super.dispose();
}
@override
void didChangePlatformBrightness() {
if (mounted) {
setState(() {
_brightness = WidgetsBinding.instance?.window.platformBrightness;
});
}
super.didChangePlatformBrightness();
}
CupertinoThemeData get _lightTheme =>
CupertinoThemeData(brightness: Brightness.light, /* light theme settings */);
CupertinoThemeData get _darkTheme => CupertinoThemeData(
brightness: Brightness.dark, /* dark theme settings */,
);
@override
Widget build(BuildContext context) {
return CupertinoApp(
title: 'Demo App',
theme: _brightness == Brightness.dark ? _darkTheme : _lightTheme,
home: MyHomePage(title: 'Demo Home Page'),
);
}
}
You can use scoped_model, provider, bloc or get for seamless experience.
Solution 2:
The easiest way in my opinion is by using provider to manage the state of your app and shared_preferences to save your theme preference on file system. By following this procedure you can save your theme so the user doesn't have to switch theme every time.
Output
You can easily store your theme preference in form of a string and then at the start of your app check if there is value stored on file system, if so apply that theme as shown below.
StorageManager.dart
import 'package:shared_preferences/shared_preferences.dart';
class StorageManager {
static void saveData(String key, dynamic value) async {
final prefs = await SharedPreferences.getInstance();
if (value is int) {
prefs.setInt(key, value);
} else if (value is String) {
prefs.setString(key, value);
} else if (value is bool) {
prefs.setBool(key, value);
} else {
print("Invalid Type");
}
}
static Future<dynamic> readData(String key) async {
final prefs = await SharedPreferences.getInstance();
dynamic obj = prefs.get(key);
return obj;
}
static Future<bool> deleteData(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.remove(key);
}
}
Define your theme properties in a theme variable like below and initialize your _themedata variable on the basis of value inside storage.
ThemeManager.dart
import 'package:flutter/material.dart';
import '../services/storage_manager.dart';
class ThemeNotifier with ChangeNotifier {
final darkTheme = ThemeData(
primarySwatch: Colors.grey,
primaryColor: Colors.black,
brightness: Brightness.dark,
backgroundColor: const Color(0xFF212121),
accentColor: Colors.white,
accentIconTheme: IconThemeData(color: Colors.black),
dividerColor: Colors.black12,
);
final lightTheme = ThemeData(
primarySwatch: Colors.grey,
primaryColor: Colors.white,
brightness: Brightness.light,
backgroundColor: const Color(0xFFE5E5E5),
accentColor: Colors.black,
accentIconTheme: IconThemeData(color: Colors.white),
dividerColor: Colors.white54,
);
ThemeData _themeData;
ThemeData getTheme() => _themeData;
ThemeNotifier() {
StorageManager.readData('themeMode').then((value) {
print('value read from storage: ' + value.toString());
var themeMode = value ?? 'light';
if (themeMode == 'light') {
_themeData = lightTheme;
} else {
print('setting dark theme');
_themeData = darkTheme;
}
notifyListeners();
});
}
void setDarkMode() async {
_themeData = darkTheme;
StorageManager.saveData('themeMode', 'dark');
notifyListeners();
}
void setLightMode() async {
_themeData = lightTheme;
StorageManager.saveData('themeMode', 'light');
notifyListeners();
}
}
Wrap your app with themeProvider and then apply theme using consumer. By doing so whenever you change the value of theme and call notify listeners widgets rebuild to sync changes.
Main.dart
void main() {
return runApp(ChangeNotifierProvider<ThemeNotifier>(
create: (_) => new ThemeNotifier(),
child: MyApp(),
));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ThemeNotifier>(
builder: (context, theme, _) => MaterialApp(
theme: theme.getTheme(),
home: Scaffold(
appBar: AppBar(
title: Text('Hybrid Theme'),
),
body: Row(
children: [
Container(
child: FlatButton(
onPressed: () => {
print('Set Light Theme'),
theme.setLightMode(),
},
child: Text('Set Light Theme'),
),
),
Container(
child: FlatButton(
onPressed: () => {
print('Set Dark theme'),
theme.setDarkMode(),
},
child: Text('Set Dark theme'),
),
),
],
),
),
),
);
}
}
Here is the link to github repository.
Solution 3:
Below are three ways to implement Dark Mode:
- always Dark mode
- device/platform controlled dark mode
- app controlled, runtime switchable dark mode
Always Dark Mode
To run your app only in Dark Mode:
- in
MaterialApp
, replaceThemeData(...)
withThemeData.dark()
- restart your app. It will now be running in Dark Mode using the colors defined in
ThemeData.dark()
OLD
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
NEW
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.dark(), // default dark theme replaces default light theme
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
Device Controlled Dark Mode
- works only on Android 10+, iOS 13+ (when dark mode was introduced)
- to let the device/platform set the theme,
MaterialApp
needs 3 args:theme: ThemeData()
darkTheme: ThemeData().dark
themeMode: ThemeMode.system
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(),
darkTheme: ThemeData.dark(), // standard dark theme
themeMode: ThemeMode.system, // device controls theme
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
- (you can use custom themes. Above are defaults for simplicity)
-
themeMode: ThemeMode.system
tells Flutter to use the device/platform theme setting - with the above settings on Android 10+ or iOS 13+, toggling Dark mode via Device Settings will now switch your app between light and dark modes.
- on Android: drag down from top of screen and click the Dark theme toggle button.
- iOS physical device: Settings > Display & Brightness > Light or Dark.
- iOS: add Dark mode switch to Control Center for ease of testing
- iOS simulator: Settings > Developer > Dark Appearance.
- any time the device theme changes, your app will immediately reflect the chosen device theme
- to get the current device theme mode programmatically, we can check device brightness (
Brightness.light
orBrightness.dark
) which corresponds to light mode and dark mode. Do this by queryingplatformBrightness
with:MediaQuery.of(context).platformBrightness
App Controlled Dark Mode
- our app can run in either light or dark mode, controlled by user and switched freely at runtime inside the app and completely ignore the device's theme setting
- as before, supply all three theme arguments to
MaterialApp
:theme:
,darkTheme:
andthemeMode:
, but we'll adjustthemeMode:
to use a state field below - To switch between light / dark modes within the app, we'll swap the
themeMode:
argument betweenThemeMode.light
andThemeMode.dark
and rebuild theMaterialApp
widget.
How to Rebuild MaterialApp widget
- to switch our app theme from anywhere, we need to access
MaterialApp
from anywhere in our app - we can do this without any package using just
StatefulWidget
, or we can use a state management package - example of runtime theme switching anywhere in app using StatefulWidget below
Before - Stateless
- we started with this, but we'll replace it with a
StatefulWidget
next
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(),
darkTheme: ThemeData.dark(), // standard dark theme
themeMode: ThemeMode.system, // device controls theme
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
After - Stateful
- here we've replaced
MyApp
StatelessWidget
with aStatefulWidget
and its complementaryState
class,_MyAppState
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(),
darkTheme: ThemeData.dark(), // standard dark theme
themeMode: ThemeMode.system, // device controls theme
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
Add Static Accessor to StatefulWidget
- adding this static
of()
method to ourStatefulWidget
makes itsState
object accessible for any descendant widget
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
/// ↓↓ ADDED
/// InheritedWidget style accessor to our State object.
static _MyAppState of(BuildContext context) =>
context.findAncestorStateOfType<_MyAppState>();
}
/// State object hidden ↓. Focusing on ↑ StatefulWidget here.
- note the return
Type
of ourof()
method:_MyAppState
- we're not getting the
StatefulWidget
, we're getting itsState
object:_MyAppState
-
_MyAppState
will hold the "state" of ourThemeMode
setting (in next step). This is what controls our app's current theme. - next in our
_MyAppState
class we'll add aThemeMode
"state" field and a method to change theme & rebuild our app
_MyAppState
- below is our
State
class modified with:- a "state" field
_themeMode
-
MaterialApp
themeMode:
arg using_themeMode
state field value -
changeTheme
method
- a "state" field
class _MyAppState extends State<MyApp> {
/// 1) our themeMode "state" field
ThemeMode _themeMode = ThemeMode.system;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(),
darkTheme: ThemeData.dark(),
themeMode: _themeMode, // 2) ← ← ← use "state" field here //////////////
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
/// 3) Call this to change theme from any context using "of" accessor
/// e.g.:
/// MyApp.of(context).changeTheme(ThemeMode.dark);
void changeTheme(ThemeMode themeMode) {
setState(() {
_themeMode = themeMode;
});
}
}
- next, we'll show how to access
changeTheme()
to change our theme & rebuild the app
Change Theme & Rebuild
- below is an example of using the
of()
accessor method to find ourState
object and call its changeTheme method from the two buttons below which call:MyApp.of(context).changeTheme(ThemeMode.light)
MyApp.of(context).changeTheme(ThemeMode.dark)
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Choose your theme:',
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
/// //////////////////////////////////////////////////////
/// Change theme & rebuild to show it using these buttons
ElevatedButton(
onPressed: () => MyApp.of(context).changeTheme(ThemeMode.light),
child: Text('Light')),
ElevatedButton(
onPressed: () => MyApp.of(context).changeTheme(ThemeMode.dark),
child: Text('Dark')),
/// //////////////////////////////////////////////////////
],
),
],
),
),
);
}
}
To return theme control back to the device's Dark mode setting, create a third button that makes a call to set themeMode:
to ThemeMode.system
:
MyApp.of(context).changeTheme(ThemeMode.system)
Running this method will delegate control of the app's theme back to whatever Dark mode setting the device is currently using.
Code: Complete copy-paste code available in this gist.
Solution 4:
Letting the system handle themes:
runApp(
MaterialApp(
theme: ThemeData.light(), // Provide light theme
darkTheme: ThemeData.dark(), // Provide dark theme
home: HomePage(),
),
);
Handling the themes yourself:
Use provider to set the theme programmatically. Full code:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<ThemeModel>(
create: (_) => ThemeModel(),
child: Consumer<ThemeModel>(
builder: (_, model, __) {
return MaterialApp(
theme: ThemeData.light(), // Provide light theme.
darkTheme: ThemeData.dark(), // Provide dark theme.
themeMode: model.mode, // Decides which theme to show.
home: Scaffold(
appBar: AppBar(title: Text('Light/Dark Theme')),
body: ElevatedButton(
onPressed: () => model.toggleMode(),
child: Text('Toggle Theme'),
),
),
);
},
),
);
}
}
class ThemeModel with ChangeNotifier {
ThemeMode _mode;
ThemeMode get mode => _mode;
ThemeModel({ThemeMode mode = ThemeMode.light}) : _mode = mode;
void toggleMode() {
_mode = _mode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
notifyListeners();
}
}
Answering OP questions:
-
Current theme can be found using:
bool isDarkMode = MediaQuery.of(context).platformBrightness == Brightness.dark;
or
bool isDarkMode = SchedulerBinding.instance.window.platformBrightness == Brightness.dark;
-
You can provide theme to your whole app using
theme
for default themes,darkTheme
for Dark themes (if dark mode is enabled by the system or by you usingthemeMode
) -
You can make use of provider package as shown in the code above.
Solution 5:
MaterialApp(
theme: ThemeData.light(),
/// theme: ThemeData.dark(),
)
Down the widget tree, you can access ThemeData simply by writing Theme.of(context)
. If you want to access the current ThemeData and provide your own styling for certain field, you can do for an instance:
Widget build(BuildContext context) {
var themeData = Theme.of(context).copyWith(scaffoldBackgroundColor: darkBlue)
return Scaffold(
backgroundColor = themeData.scaffoldBackgroundColor,
);
}
But to handle the ThemeData state (changing its value), you need to implement proper state management.