Flutter: How to change the MaterialApp theme at runtime
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);
}),
])));
}
}
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;
},
),
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:
- Theme is specified from
_configuration
, which is updated byconfigurationUpdater
configurationUpdater
is 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