'Flutter: How to change the MaterialApp theme at runtime
I have a MaterialApp Widget that sets the theme for all Widgets within the app. I'd like to change the MaterialApps theme value at runtime from a child Widget that doesn't have any direct reference to its parent MaterialApp.
It seems like this should be possible because the ThemeData is provided by an InheritedWidget, but I can't figure out how to change the theme wholesale. Does anyone know how to do this?
Here is the MaterialApp that owns the rest of the app:
new MaterialApp(
title: 'App Name',
theme: initialTheme,
routes: <String, WidgetBuilder>{
'/' : ...,
},
),
Solution 1:[1]
Based on Dan Field's recommendation I came to the following solution. If anyone has improvements feel free to chime in:
// How to use: Any Widget in the app can access the ThemeChanger
// because it is an InheritedWidget. Then the Widget can call
// themeChanger.theme = [blah] to change the theme. The ThemeChanger
// then accesses AppThemeState by using the _themeGlobalKey, and
// the ThemeChanger switches out the old ThemeData for the new
// ThemeData in the AppThemeState (which causes a re-render).
final _themeGlobalKey = new GlobalKey(debugLabel: 'app_theme');
class AppTheme extends StatefulWidget {
final child;
AppTheme({
this.child,
}) : super(key: _themeGlobalKey);
@override
AppThemeState createState() => new AppThemeState();
}
class AppThemeState extends State<AppTheme> {
ThemeData _theme = DEV_THEME;
set theme(newTheme) {
if (newTheme != _theme) {
setState(() => _theme = newTheme);
}
}
@override
Widget build(BuildContext context) {
return new ThemeChanger(
appThemeKey: _themeGlobalKey,
child: new Theme(
data: _theme,
child: widget.child,
),
);
}
}
class ThemeChanger extends InheritedWidget {
static ThemeChanger of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ThemeChanger);
}
final ThemeData theme;
final GlobalKey _appThemeKey;
ThemeChanger({
appThemeKey,
this.theme,
child
}) : _appThemeKey = appThemeKey, super(child: child);
set appTheme(AppThemeOption theme) {
switch (theme) {
case AppThemeOption.experimental:
(_appThemeKey.currentState as AppThemeState)?.theme = EXPERIMENT_THEME;
break;
case AppThemeOption.dev:
(_appThemeKey.currentState as AppThemeState)?.theme = DEV_THEME;
break;
}
}
@override
bool updateShouldNotify(ThemeChanger oldWidget) {
return oldWidget.theme == theme;
}
}
Solution 2:[2]
You can also use StreamController.
Just copy and paste this code. It's a working sample. You don't need any library and it's super simple
import 'dart:async';
import 'package:flutter/material.dart';
StreamController<bool> isLightTheme = StreamController();
main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
initialData: true,
stream: isLightTheme.stream,
builder: (context, snapshot) {
return MaterialApp(
theme: snapshot.data ? ThemeData.light() : ThemeData.dark(),
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: Text("Dynamic Theme")),
body: SettingPage()));
});
}
}
class SettingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <
Widget>[
RaisedButton(
color: Colors.blue,
child: Text("Light Theme", style: TextStyle(color: Colors.white)),
onPressed: () {
isLightTheme.add(true);
}),
RaisedButton(
color: Colors.black,
child: Text("Dark Theme", style: TextStyle(color: Colors.white)),
onPressed: () {
isLightTheme.add(false);
}),
])));
}
}
Solution 3:[3]
This is a specific case of the question answered here: Force Flutter to redraw all widgets
Take a look at the Stocks sample mentioned in that question, taking note especially of: https://github.com/flutter/flutter/blob/master/examples/stocks/lib/main.dart https://github.com/flutter/flutter/blob/master/examples/stocks/lib/stock_settings.dart
Take note of the following:
- Theme is specified from
_configuration, which is updated byconfigurationUpdater configurationUpdateris passed on to children of the app that need it- Children can call that configurationUpdater, which in turn sets state at the root of the app, which in turn redraws the app using the specified theme
Solution 4:[4]
You may use ChangeNotifierProvider/Consumer from provider package with combination of ChangeNotifier successor.
/// Theme manager
class ThemeManager extends ChangeNotifier {
ThemeManager([ThemeData initialTheme]) : _themeData = initialTheme ?? lightTheme;
ThemeData _themeData;
/// Returns the current theme
ThemeData get themeData => _themeData;
/// Sets the current theme
set themeData(ThemeData value) {
_themeData = value;
notifyListeners();
}
/// Dark mode theme
static ThemeData lightTheme = ThemeData();
/// Light mode theme
static ThemeData darkTheme = ThemeData();
}
/// Application
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ThemeManager(),
child: Consumer<ThemeManager>(
builder: (_, manager, __) {
return MaterialApp(
title: 'Flutter Demo',
theme: manager.themeData,
home: HomePage(),
);
},
),
);
}
}
// Somewhere in GUI
FlatButton(
child: Text(isDarkMode ? 'Light Mode' : 'Dark Mode'),
onPressed() {
Provider.of<ThemeManager>(context, listen:false)
.themeData = isDarkMode ? ThemeManager.darkTheme : ThemeManager.lightTheme;
},
),
Solution 5:[5]
You can use Provider to change that .
1- You have to add Provider in pubspec.yaml file
dependencies:
flutter:
sdk: flutter
provider: ^4.3.2+2
2- Extend a class from ChangeNotifier to change theme and hold current theme
import 'package:flutter/material.dart';
var darkTheme = ThemeData.dark();
var lightTheme= ThemeData.light();
enum ThemeType { Light, Dark }
class ThemeModel extends ChangeNotifier {
ThemeData currentTheme = darkTheme;
ThemeType _themeType = ThemeType.Dark;
toggleTheme() {
if (_themeType == ThemeType.Dark) {
currentTheme = lightTheme;
_themeType = ThemeType.Light;
}
else if (_themeType == ThemeType.Light) {
currentTheme = darkTheme;
_themeType = ThemeType.Dark;
}
return notifyListeners();
}
}
3- Add ChangeNotifierProvider as child of runApp
void main() {
runApp(
ChangeNotifierProvider<ThemeModel>(
create: (context) => ThemeModel(),
child: MyApp(),
),
);
}
4- get current theme on starting app
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MyApp',
initialRoute: '/',
theme: Provider.of<ThemeModel>(context).currentTheme,
routes: {
'/': (context) => FirstPage(),
'/SecondPage': (context) => SecondPage(),
},
);
}
5- Toggle your Theme in Other class
onTap: () {Provider.of<ThemeModel>(context,listen: false).toggleTheme();},
Solution 6:[6]
Simple Example
Change Themes at Runtime /w StatefulWidget
This copy/paste example changes the app theme between light/dark themes at runtime using StatefulWidget.
(This is the auto-generated Flutter example app from Android Studio, modified.)
What's changed
MyAppchanged fromStatelessWidgettoStatefulWidget(MyStatefulApp)- static
of(context)method added toMyStatefulApp(to find ourStateobject from descendants) changeTheme()method added to ourStateobject- FAB button call to
_incrementCounterdelegatessetStaterebuild toMyStatefulApp.of(context).changeTheme(). No need to callsetStatehere.
import 'package:flutter/material.dart';
void main() {
runApp(MyStatefulApp());
}
/// Change MyApp from StatelessWidget to StatefulWidget
class MyStatefulApp extends StatefulWidget {
@override
_MyStatefulAppState createState() => _MyStatefulAppState();
/// Add an InheritedWidget-style static accessor so we can
/// find our State object from any descendant & call changeTheme
/// from anywhere.
static _MyStatefulAppState of(BuildContext context) =>
context.findAncestorStateOfType<_MyStatefulAppState>();
}
class _MyStatefulAppState extends State<MyStatefulApp> {
// define a state field for theme
ThemeData _theme = ThemeData();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App Themes',
theme: _theme, // use theme field here
home: MyHomePage(title: 'Change App Theme at Runtime'),
);
}
/// Call changeTheme to rebuild app with a new theme
void changeTheme({ThemeData theme}) {
setState(() {
_theme = theme;
});
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
_counter++;
// alternate light / dark themes with each FAB press, for illustration
ThemeData _theme = _counter.isOdd ? ThemeData.dark() : ThemeData();
/// Find the State object and change the theme, can be done anywhere with
/// a context
MyStatefulApp.of(context).changeTheme(theme: _theme);
// we're rebuilding with changeTheme, so don't duplicate setState call
/*setState(() {
_counter++;
});*/
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You switched themes this many times, happy yet?:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Notes
- You'll see a console warning about a deprecated setter. Ignore this. The Flutter team is aware and they'll fix it when they get time.
- to swap between light / dark modes we should really provide a
darkThemeandthemeModeargs toMaterialAppand just changethemeModebetweenThemeMode.lightandThemeMode.darkinstead of changing thethemearg each time. UsingthemeModewould support device-wide dark mode from iOS 13 / Android 10 onwards. The above example was done as-is to answer the question as simply/directly as possible, but isn't ideal for this particular use case.
Solution 7:[7]
after various attempts I did it with the BLoC pattern, I don't know if it is a good method but it seems to work with no problems:
App theme models:
class MyTheme {
Brightness brightness;
Color backgroundColor;
Color scaffoldBackgroundColor;
Color primaryColor;
Brightness primaryColorBrightness;
Color accentColor;
MyTheme({
this.brightness,
this.backgroundColor,
this.scaffoldBackgroundColor,
this.primaryColor,
this.primaryColorBrightness,
this.accentColor
});
}
class AppTheme {
String name;
MyTheme theme;
AppTheme(this.name, this.theme);
}
List<AppTheme> myThemes = [
AppTheme(
'Default',
MyTheme(
brightness: Brightness.light,
backgroundColor: Colors.blue[50],
scaffoldBackgroundColor: Colors.blue[50],
primaryColor: Colors.blue,
primaryColorBrightness: Brightness.dark,
accentColor: Colors.blue[50],
)),
AppTheme(
'Teal',
MyTheme(
brightness: Brightness.light,
backgroundColor: Colors.teal[50],
scaffoldBackgroundColor: Colors.teal[50],
primaryColor: Colors.teal[600],
primaryColorBrightness: Brightness.dark,
accentColor: Colors.teal[50],
),
),
];
App BLoC class. Here I used a BehaviorSubject of RxDart.
class AppBloc {
final _theme = BehaviorSubject<AppTheme>();
Function(AppTheme) get inTheme => _theme.sink.add;
Stream<AppTheme> get outTheme => _theme.stream;
AppBloc() {
print('-------APP BLOC INIT--------');
// Send to stream the initial theme
inTheme(myThemes[0]);
}
dispose() {
print('---------APP BLOC DISPOSE-----------');
_theme.close();
}
}
In the settings page of the app I use the _theme stream to set the current theme of a dropdown menu with the themes list. With the onChanged handler, when a user clicks on the theme it is sent to stream:
StreamBuilder(
stream: widget.bloc.outTheme,
builder: (context, AsyncSnapshot<AppTheme> snapshot) {
return snapshot.hasData
? DropdownButton<AppTheme>(
hint: Text("Status"),
value: snapshot.data,
items: myThemes.map((AppTheme appTheme) {
return DropdownMenuItem<AppTheme>(
value: appTheme,
child: Text(appTheme.name),
);
}).toList(),
onChanged: widget.bloc.inTheme,
)
: Container();
}),
And finally in the homepage, with a StreamBuilder I use the _theme stream to set the selected ThemeData:
StreamBuilder(
stream: _bloc.outTheme,
builder: (context, AsyncSnapshot<AppTheme> snapshot) {
return MaterialApp(
theme: snapshot.hasData ? _buildThemeData(snapshot.data) : ThemeData(),
home: HomePage());
}),
_BuildThemeData method to get the ThemeData from the theme model:
_buildThemeData(AppTheme appTheme) {
return ThemeData(
brightness: appTheme.theme.brightness,
backgroundColor: appTheme.theme.backgroundColor,
scaffoldBackgroundColor: appTheme.theme.scaffoldBackgroundColor,
primaryColor: appTheme.theme.primaryColor,
primaryColorBrightness: appTheme.theme.primaryColorBrightness,
accentColor: appTheme.theme.accentColor
);
}
I hope this is useful to you.
Solution 8:[8]
After following @SuperDeclarative Answer do this
At main.dart while making material app
MaterialApp(
builder: (context, child) {
return new AppTheme(
child: YourAppWidget())
})
In any other class where you want to change theme
setState(() {
ThemeChanger.of(context).appTheme = appThemeLight;
});
My Tip:
- Save to shared pref. at the time changing theme from other class
- Before launching check this preference and open the app with this theme
- After opening follow the above code from any other class
Solution 9:[9]
Here's my approach using the built-in flutter's state management solution ChangeNotifier. Which uses AnimatedBuilder to build the MaterialApp whenever the data members (ThemeMode in this case) in the Settings class changes.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
Settings appSettings = Settings();
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: appSettings,
builder: (context, snapshot) {
return MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: appSettings.getTheme,
home: MyAwesomeApp(title: "Dark Theme Sample"),
);
});
}
}
class MyAwesomeApp extends StatefulWidget {
const MyAwesomeApp({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyAwesomeApp> createState() => _MyAwesomeAppState();
}
class _MyAwesomeAppState extends State<MyAwesomeApp> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Drag the button to change the Theme',
),
Switch(
value: appSettings.getTheme == ThemeMode.dark,
onChanged: (isDark) {
if (isDark) {
appSettings.setTheme(ThemeMode.dark);
} else {
appSettings.setTheme(ThemeMode.light);
}
}),
Text(
appSettings.getTheme == ThemeMode.dark ? 'Dark' : 'Light',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
);
}
}
class Settings extends ChangeNotifier {
ThemeMode theme = ThemeMode.light;
ThemeMode get getTheme => theme;
void setTheme(ThemeMode theme) {
this.theme = theme;
notifyListeners();
}
void notifyListeners() {
super.notifyListeners();
}
}
Heres the dartpad demo to try it out https://dartpad.dev/?id=c81eccd13f45568ee10c4d160f1560c9
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | SuperDeclarative |
| Solution 2 | erluxman |
| Solution 3 | Dan Field |
| Solution 4 | BambinoUA |
| Solution 5 | |
| Solution 6 | Baker |
| Solution 7 | |
| Solution 8 | Shreyash Jain |
| Solution 9 | Mahesh Jamdade |
