簡體   English   中英

Flutter:如何在運行時更改 MaterialApp 主題

[英]Flutter: How to change the MaterialApp theme at runtime

我有一個MaterialApp小部件,它為應用程序中的所有小部件設置theme 我想在運行時從沒有任何直接引用其父MaterialApp的子 Widget 更改MaterialApptheme值。

看起來這應該是可能的,因為ThemeData是由InheritedWidget提供的,但我不知道如何批量更改theme 有誰知道如何做到這一點?

這是擁有應用程序其余部分的MaterialApp

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

根據丹菲爾德的建議,我得出了以下解決方案。 如果有人有改進,請隨時加入:

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

}

這是此處回答的問題的一個具體案例: Force Flutter to redraw all widgets

看看那個問題中提到的股票樣本,特別注意: https : //github.com/flutter/flutter/blob/master/examples/stocks/lib/main.dart https://github.com/顫振/顫振/blob/master/examples/stocks/lib/stock_settings.dart

請注意以下事項:

  1. 主題由_configuration指定,由configurationUpdater更新
  2. configurationUpdater傳遞給需要它的應用程序的子級
  3. 孩子們可以調用那個 configurationUpdater,它反過來在應用程序的根設置狀態,然后使用指定的主題重繪應用程序

您還可以使用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);
              }),
        ])));
  }
}

您可以使用提供程序包中的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;
  },
),

經過各種嘗試,我用 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
);

}

我希望這對你有用。

您可以只使用 MaterialApp 小部件的 themeMode 參數,如下所示:

theme: yourLightTheme,
darkTheme: yourDarkTheme,
themeMode: settings.useSystemTheme
     ? ThemeMode.system
     : settings.useDarkTheme ? ThemeMode.dark : ThemeMode.light,
home: Home(),

它很干凈,適用於 Android 和 iOS。 你只需要在你所在州的某個地方擁有這兩個布爾值,useSystemTheme 和 useDarkTheme,就是這樣。

在遵循@SuperDeclarative Answer 之后執行此操作

在 main.dart 制作材料應用程序時

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

在您想要更改主題的任何其他課程中

setState(() {
    ThemeChanger.of(context).appTheme = appThemeLight;
  });

我的提示:

  1. 保存到共享首選項。 在從其他班級改變主題的時候
  2. 在啟動之前檢查此首選項並使用此主題打開應用程序
  3. 打開后按照上面的任何其他類的代碼

您可以使用Provider來更改它。

1- 您必須在 pubspec.yaml 文件中添加 Provider

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

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

簡單示例

在運行時更改主題 /w StatefulWidget

此復制/粘貼示例在運行時使用StatefulWidget在淺色/深色主題之間更改應用主題。

(這是從 Android Studio 自動生成的 Flutter 示例應用程序,已修改。)

發生了什么變化

  1. MyAppStatelessWidget更改為StatefulWidget ( MyStatefulApp )
  2. 添加到MyStatefulApp static of(context)方法(從后代中查找我們的State對象)
  3. changeTheme()方法添加到我們的State對象
  4. _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 團隊知道,他們會在有時間時修復它。
  • 光/暗模式之間的交換,我們真的應該提供一個darkThemethemeMode參數傳遞給MaterialApp ,只是改變themeMode之間ThemeMode.lightThemeMode.dark改變,而不是theme每次ARG。 從 iOS 13 / Android 10 開始,使用themeMode將支持設備范圍的暗模式。 上面的示例按原樣完成,以盡可能簡單/直接地回答問題,但對於此特定用例並不理想。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM