I have a MaterialApp
Widget that sets the theme
for all Widgets within the app. I'd like to change the MaterialApp
s 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>{
'/' : ...,
},
),
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.)
MyApp
changed from StatelessWidget
to StatefulWidget
(MyStatefulApp
)of(context)
method added to MyStatefulApp
(to find our State
object from descendants)changeTheme()
method added to our State
object_incrementCounter
delegates setState
rebuild to MyStatefulApp.of(context).changeTheme()
. No need to call setState
here.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),
),
);
}
}
darkTheme
and themeMode
args to MaterialApp
and just change themeMode
between ThemeMode.light
and ThemeMode.dark
instead of changing the theme
arg each time. Using themeMode
would 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.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);
}),
])));
}
}
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;
}
}
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:
_configuration
, which is updated by configurationUpdater
configurationUpdater
is passed on to children of the app that need itYou 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();},
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With