Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: How to change the MaterialApp theme at runtime

Tags:

flutter

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>{
      '/' : ...,
    },
),
like image 426
SuperDeclarative Avatar asked Mar 08 '18 02:03

SuperDeclarative


5 Answers

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.
like image 195
Baker Avatar answered Sep 23 '22 00:09

Baker


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);
              }),
        ])));
  }
}
like image 25
erluxman Avatar answered Nov 19 '22 22:11

erluxman


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

}
like image 8
SuperDeclarative Avatar answered Nov 19 '22 22:11

SuperDeclarative


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
like image 5
Dan Field Avatar answered Nov 19 '22 23:11

Dan Field


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();},
like image 3
Milad Ahmadi Avatar answered Nov 20 '22 00:11

Milad Ahmadi