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>{
'/' : ...,
},
),
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