Flutter:如何在运行时更改MaterialApp主题

zu0ti5jz  于 2023-03-04  发布在  Flutter
关注(0)|答案(9)|浏览(162)

我有一个MaterialApp小部件,它为应用程序中的所有小部件设置theme。我想在运行时从一个没有任何直接引用其父小部件MaterialApp的子小部件更改MaterialApptheme值。
这似乎是可能的,因为ThemeData是由InheritedWidget提供的,但我不知道如何批量更改theme。有人知道如何做到这一点吗?
下面是拥有应用程序其余部分的MaterialApp

new MaterialApp(
    title: 'App Name',
    theme: initialTheme,
    routes: <String, WidgetBuilder>{
      '/' : ...,
    },
),
b5buobof

b5buobof1#

您也可以使用StreamController
只需复制并粘贴此代码。这是一个工作示例。您不需要任何库,而且非常简单

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);
              }),
        ])));
  }
}
0wi1tuuw

0wi1tuuw2#

根据Dan Field的建议,我得出了以下解决方案。如果有人有改进意见,请随时插话:

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

}
7d7tgy0s

7d7tgy0s3#

这是这里回答的问题的一个具体情况:How to force Flutter to rebuild / redraw all widgets?
看看问题中提到的股票样本,特别注意:https://github.com/flutter/flutter/blob/e7b7ebc066c1b2a5aa5c19f8961307427e0142a6/dev/benchmarks/test_apps/stocks/lib/main.darthttps://github.com/flutter/flutter/blob/e7b7ebc066c1b2a5aa5c19f8961307427e0142a6/dev/benchmarks/test_apps/stocks/lib/stock_settings.dart
请注意以下事项:
1.主题由_configuration指定,并由configurationUpdater更新

  1. configurationUpdater将传递给需要它的应用子级
    1.孩子们可以调用configurationUpdater,它反过来在应用程序的根目录设置状态,然后使用指定的主题重新绘制应用程序
vql8enpb

vql8enpb4#

您可以使用**Provider**更改此设置。
1-您必须在pubspec.yaml文件中添加提供程序

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.3.2+2

2-从ChangeNotifier扩展类以更改主题并保留当前主题

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-将ChangeNotifierProvider添加为runApp的子级

void main() {
  runApp(
    ChangeNotifierProvider<ThemeModel>(
      create: (context) => ThemeModel(),
      child: MyApp(),
    ),
  );
}

4-在启动应用程序时获取当前主题

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MyApp',
      initialRoute: '/',
      theme: Provider.of<ThemeModel>(context).currentTheme,
      routes: {
        '/': (context) => FirstPage(),
        '/SecondPage': (context) => SecondPage(),
      },
    );

  }

5-在其他类中切换主题

onTap: () {Provider.of<ThemeModel>(context,listen: false).toggleTheme();},
vq8itlhq

vq8itlhq5#

您可以将provider软件包中的ChangeNotifierProvider/ConsumerChangeNotifier后续产品组合使用。

/// 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;
  },
),
vmjh9lq9

vmjh9lq96#

下面是我使用内置flutter的状态管理解决方案ChangeNotifier的方法,每当Settings类中的数据成员(本例中为ThemeMode)发生变化时,使用AnimatedBuilder构建MaterialApp。

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: 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.headline4,
            ),
          ],
        ),
      ),
    );
  }
}

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

  ThemeMode get getTheme => theme;

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

  void notifyListeners() {
    super.notifyListeners();
  }
}

下面是dartpad演示,尝试https://dartpad.dev/?id=c81eccd13f45568ee10c4d160f1560c9

t3irkdon

t3irkdon7#

简单示例

在运行时更改主题/使用StatefulWidget

此复制/粘贴示例使用StatefulWidget在运行时在亮/暗主题之间更改应用主题。
(This是Android Studio中自动生成的Flutter示例应用,已修改。)

什么改变了

1.将MyAppStatelessWidget更改为StatefulWidgetMyStatefulApp
1.添加到MyStatefulApp的静态of(context)方法(从后代中查找State对象)

  1. changeTheme()方法已添加到State对象
    1.对_incrementCounter的FAB按钮调用将setState重建委托给MyStatefulApp.of(context).changeTheme()。此处无需调用setState
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),
      ),
    );
  }
}

注解

  • 你会看到一个关于一个过时的setter的控制台警告。忽略这个。Flutter team is aware,他们会在有时间的时候修复它。
  • 要在亮/暗模式之间切换,我们应该为MaterialApp提供一个darkThemethemeMode参数,只需在ThemeMode.lightThemeMode.dark之间更改themeMode,而不是每次都更改theme参数。使用themeMode将支持iOS 13/Android 10以上的设备范围暗模式。上面的示例是按原样完成的,目的是尽可能简单/直接地回答问题,但对于此特定用例来说并不理想。
5ktev3wc

5ktev3wc8#

经过各种尝试之后,我使用BLoC模式进行了测试,我不知道这是否是一个好方法,但它似乎没有任何问题:
应用主题模型:

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],
    ),
  ),
];

应用程序BLoC类。这里我使用了RxDart的BehaviorSubject。

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

在应用的设置页面中,我使用_theme流来设置主题列表下拉菜单的当前主题,使用onChanged处理程序,当用户点击主题时,主题将被发送到流:

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();
                    }),

最后,在主页中,使用StreamBuilder,我使用_theme流来设置选定的ThemeData:

StreamBuilder(
          stream: _bloc.outTheme,
          builder: (context, AsyncSnapshot<AppTheme> snapshot) {
            return MaterialApp(
                theme: snapshot.hasData ? _buildThemeData(snapshot.data) : ThemeData(),
                home: HomePage());
          }),

_BuildThemeData方法从主题模型获取ThemeData:

_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
);

}

希望这对你有用。

qlfbtfca

qlfbtfca9#

在@SuperDeclarative Answer之后执行以下操作

制作材料应用程序时位于主省道

MaterialApp(
      builder: (context, child) {
        return new AppTheme(
            child: YourAppWidget())
})

在要更改主题的任何其他类中

setState(() {
    ThemeChanger.of(context).appTheme = appThemeLight;
  });
  • 我的建议:*

1.从其他班级更改主题时保存到共享首选项
1.启动前检查此首选项并打开此主题的应用程序
1.打开后,按照上面的代码从任何其他类

相关问题