20

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>{
      '/' : ...,
    },
),
SuperDeclarative
  • 1,609
  • 4
  • 18
  • 32

9 Answers9

11

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);
              }),
        ])));
  }
}
erluxman
  • 18,155
  • 20
  • 92
  • 126
8

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;
  }

}
SuperDeclarative
  • 1,609
  • 4
  • 18
  • 32
5

This is a specific case of the question answered here: How to force Flutter to rebuild / redraw all widgets?

Take a look at the Stocks sample mentioned in that question, taking note especially of: https://github.com/flutter/flutter/blob/e7b7ebc066c1b2a5aa5c19f8961307427e0142a6/dev/benchmarks/test_apps/stocks/lib/main.dart https://github.com/flutter/flutter/blob/e7b7ebc066c1b2a5aa5c19f8961307427e0142a6/dev/benchmarks/test_apps/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
Shady Mohamed Sherif
  • 15,003
  • 4
  • 45
  • 54
Dan Field
  • 20,885
  • 5
  • 55
  • 71
4

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();},
Milad Ahmadi
  • 1,019
  • 12
  • 19
2

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;
  },
),
BambinoUA
  • 6,126
  • 5
  • 35
  • 51
2

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: const 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(
              'Toggle the switch 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.headlineMedium,
            ),
          ],
        ),
      ),
    );
  }
}

class Settings extends ChangeNotifier {
  ThemeMode theme = ThemeMode.light;

  ThemeMode get getTheme => theme;

  void setTheme(ThemeMode theme) {
    this.theme = theme;
    notifyListeners();
  }
  
  @override
  void notifyListeners() {
    super.notifyListeners();
  }
}

Here's the Dartpad demo to try it out https://dartpad.dev/?id=c81eccd13f45568ee10c4d160f1560c9

Mahesh Jamdade
  • 17,235
  • 8
  • 110
  • 131
1

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

  1. MyApp changed from StatelessWidget to StatefulWidget (MyStatefulApp)
  2. static of(context) method added to MyStatefulApp (to find our State object from descendants)
  3. changeTheme() method added to our State object
  4. FAB button call to _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),
      ),
    );
  }
}

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 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.
Baker
  • 24,730
  • 11
  • 100
  • 106
0

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.

Francesco Mineo
  • 137
  • 1
  • 6
0

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:

  1. Save to shared pref. at the time changing theme from other class
  2. Before launching check this preference and open the app with this theme
  3. After opening follow the above code from any other class
Shreyash Jain
  • 112
  • 1
  • 2
  • I tried @erluxman solution and that works OK and is much simpler than SuperDeclarative way. Is there any downside to that? – Sohail Nov 07 '20 at 17:49