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:

  1. Theme is specified from _configuration, which is updated by configurationUpdater
  2. configurationUpdater is passed on to children of the app that need it
  3. 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

Tags:

Flutter