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
, replace ThemeData(...)
with ThemeData.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.
- 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
or Brightness.dark
) which corresponds to light mode and dark mode. Do this by querying platformBrightness
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:
and themeMode:
, but we'll adjust themeMode:
to use a state field below
- To switch between light / dark modes within the app, we'll swap the
themeMode:
argument between ThemeMode.light
and ThemeMode.dark
and rebuild the MaterialApp
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 a StatefulWidget
and its complementary State
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 our StatefulWidget
makes its State
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 our of()
method: _MyAppState
- we're not getting the
StatefulWidget
, we're getting its State
object: _MyAppState
_MyAppState
will hold the "state" of our ThemeMode
setting (in next step). This is what controls our app's current theme.
- next in our
_MyAppState
class we'll add a ThemeMode
"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
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 our State
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')),
/// //////////////////////////////////////////////////////
],
),
],
),
),
);
}
}
![change theme buttons]()
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.