基於 Flutter 以兩種方式實現App主題切換

概述

App主題切換已經成爲了一種流行的用戶體驗,豐富了應用總體UI視覺效果。例如,白天夜間模式切換。實現該功能的思想其實不難,就是將涉及主題的資源文件進行全局替換更新。說到這裏,我想你確定能聯想到一種設計模式:觀察者模式。多種觀察對象(主題資源)來觀察當前主題更新的行爲(被觀察對象),進行主題的更新。今天和你們分享在 Flutter 平臺上如何實現主題更換。
效果

 
實現流程

在 Flutter 項目中,MaterialApp組件爲開發者提供了設置主題的api:

     const MaterialApp({
        ...
        this.theme, // 主題
        ...
      })

經過 theme 屬性,咱們能夠設置在MaterialApp下的主題樣式。theme 是 ThemeData 的對象實例:

    ThemeData({
        
        Brightness brightness,
        MaterialColor primarySwatch,
        Color primaryColor,
        Brightness primaryColorBrightness,
        Color primaryColorLight,
        Color primaryColorDark,
        
        ...
     
      })

ThemeData 中包含了不少主題設置,咱們能夠選擇性的改變其中的顏色,字體等等。因此咱們能夠經過改變 primaryColor 來實現狀態欄的顏色改變。並經過Theme來獲取當前 primaryColor 顏色值,將其賦值到其餘組件上便可。在觸發主題更新行爲時,通知 ThemeData 的 primaryColor改變行對應顏色值。 有了以上思路,接下來咱們經過兩種方式來展現如何實現主題的全局更新。
主題選項

在實例中咱們以一下主題顏色爲主:

    /**
     * 主題選項
     */
    import 'package:flutter/material.dart';
     
    final List<Color> themeList = [
      Colors.black,
      Colors.red,
      Colors.teal,
      Colors.pink,
      Colors.amber,
      Colors.orange,
      Colors.green,
      Colors.blue,
      Colors.lightBlue,
      Colors.purple,
      Colors.deepPurple,
      Colors.indigo,
      Colors.cyan,
      Colors.brown,
      Colors.grey,
      Colors.blueGrey
    ];

EventBus 方式實現

Flutter中EventBus提供了事件總線的功能,以監聽通知的方式進行主體間通訊。咱們能夠在main.dart入口文件下注冊主題修改的監聽,經過EventBus發送通知來動態修改 theme。核心代碼以下:

     @override
      void initState() {
        super.initState();
        Application.eventBus = new EventBus();
        themeColor = ThemeList[widget.themeIndex];
        this.registerThemeEvent();
      }
      
      /**
       * 註冊主題切換監聽
       */
      void registerThemeEvent() {
        Application.eventBus.on<ThemeChangeEvent>().listen((ThemeChangeEvent onData)=> this.changeTheme(onData));
      }
      
      /**
       * 刷新主題樣式
       */
      void changeTheme(ThemeChangeEvent onData) {
        setState(() {
          themeColor = themeList[onData.themeIndex];
        });
      }
     
     
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme:  ThemeData(
            primaryColor: themeColor
          ),
          home: HomePage(),
        );
      }

而後在更新主題行爲的地方來發送通知刷新便可:

      changeTheme() async {
        Application.eventBus.fire(new ThemeChangeEvent(1));
      }

scoped_model 狀態管理方式實現

瞭解 React、 React Naitve 開發的朋友對狀態管理框架確定都不陌生,例如 Redux 、Mobx、 Flux 等等。狀態框架的實現能夠幫助咱們很是輕鬆的控制項目中的狀態邏輯,使得代碼邏輯清晰易維護。Flutter 借鑑了 React 的狀態控制,一樣產生了一些狀態管理框架,例如 flutter_redux、scoped_model、bloc。接下來咱們使用 scoped_model 的方式實現主題的切換。 關於 scoped_model 的使用方式能夠參考pub倉庫提供的文檔:https://pub.dartlang.org/packages/scoped_model

1. 首先定義主題 Model

    /**
     * 主題Model
     * Create by Songlcy
     */
    import 'package:scoped_model/scoped_model.dart';
     
    abstract class ThemeStateModel extends Model {
     
      int _themeIndex;
      get themeIndex => _themeIndex;
     
      void changeTheme(int themeIndex) async {
        _themeIndex = themeIndex;
        notifyListeners();
      }
    }

 在 ThemeStateModel 中,定義了對應的主題下標,changeTheme() 方法爲更改主題,並調用 notifyListeners() 進行全局通知。

2. 注入Model

      @override
      Widget build(BuildContext context) {
        return ScopedModel<MainStateModel>(
          model: MainStateModel(),
          child: ScopedModelDescendant<MainStateModel>(
            builder: (context, child, model) {
              return  MaterialApp(
                theme: ThemeData(
                  primaryColor: themeList[model.themeIndex]
                ),
                home: HomePage(),
              );
            },
          )
        );
      }

3. 修改主題

      changeTheme(int index) async {
        int themeIndex = index;
        MainStateModel().of(context).changeTheme(themeIndex);
      }

能夠看到,使用 scoped_model 的方式一樣比較簡單,思路和 EventBus 相似。以上代碼咱們實現了主題的切換,細心的朋友能夠發現,咱們還須要對主題進行保存,當下次啓動 App 時,要顯示上次切換的主題。Flutter中提供了 shared_preferences 來實現本地持久化存儲。
主題持久化保存

當進行主題更換時,咱們能夠對主題進行持久化本地存儲

      void changeTheme(int themeIndex) async {
        _themeIndex = themeIndex;
        SharedPreferences sp = await SharedPreferences.getInstance();
        sp.setInt("themeIndex", themeIndex);
      }

而後在項目啓動時,取出本地存儲的主題下標,設置在theme上便可

    void main() async {
      int themeIndex = await getTheme();
      runApp(App(themeIndex));
    }
     
    Future<int> getTheme() async {
      SharedPreferences sp = await SharedPreferences.getInstance();
      int themeIndex = sp.getInt("themeIndex");
      if(themeIndex != null) {
        return themeIndex;
      }
      return 0;
    }
     
    @override
    Widget build(BuildContext context) {
        return ScopedModel<MainStateModel>(
          model: mainStateModel,
          child: ScopedModelDescendant<MainStateModel>(
            builder: (context, child, model) {
              return  MaterialApp(
                theme: ThemeData(
                  primaryColor: themeList[model.themeIndex != null ? model.themeIndex : widget.themeIndex]
                ),
                home: HomePage(),
              );
            },
          )
        );
    }

以上咱們經過兩種方式來實現了主題的切換,實現思想都是經過通知的方式來觸發組件 build 進行刷新。那麼兩種方式有什麼區別呢?
區別

從 print log 中,能夠發現,當使用 eventbus 事件總線進行切換主題刷新時,_AppState 下的 build方法 和 home指向的組件界面  總體都會從新構建。而使用scoped_model等狀態管理工具,_AppState 下的 build方法不會從新執行,只會刷新使用到了Model的組件,可是home對應的組件依然會從新執行build方法進行構建。因此咱們能夠得出如下結論:

二者方式都會致使 home 組件被重複 build。明顯區別在於使用狀態管理工具的方式能夠避免父組件 build 重構。

源碼已上傳到 Github,詳細代碼能夠查看

EventBus 實現總體代碼:

    import 'package:flutter/material.dart';
    import 'package:event_bus/event_bus.dart';
    import './config/application.dart';
    import './pages/home_page.dart';
    import './events/theme_event.dart';
    import './constants/theme.dart';
    import 'package:shared_preferences/shared_preferences.dart';
     
    void main() async {
      int themeIndex = await getDefaultTheme();
      runApp(App(themeIndex));
    }
     
    Future<int> getDefaultTheme() async {
      // 從shared_preferences中獲取上次切換的主題
      SharedPreferences sp = await SharedPreferences.getInstance();
      int themeIndex = sp.getInt("themeIndex");
      print(themeIndex);
      if(themeIndex != null) {
        return themeIndex;
      }
      return 0;
    }
     
    class App extends StatefulWidget {
     
      int themeIndex;
      App(this.themeIndex);
     
      @override
      State<StatefulWidget> createState() => AppState();
    }
     
    class AppState extends State<App> {
     
      Color themeColor;
     
      @override
      void initState() {
        super.initState();
        Application.eventBus = new EventBus();
        themeColor = ThemeList[widget.themeIndex];
        this.registerThemeEvent();
      }
     
      void registerThemeEvent() {
        Application.eventBus.on<ThemeChangeEvent>().listen((ThemeChangeEvent onData)=> this.changeTheme(onData));
      }
     
      void changeTheme(ThemeChangeEvent onData) {
        setState(() {
          themeColor = ThemeList[onData.themeIndex];
        });
      }
     
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme:  ThemeData(
            primaryColor: themeColor
          ),
          home: HomePage(),
        );
      }
     
      @override
      void dispose() {
        super.dispose();
        Application.eventBus.destroy();
      }
    }

      changeTheme() async {
        SharedPreferences sp = await SharedPreferences.getInstance();
        sp.setInt("themeIndex", 1);
        Application.eventBus.fire(new ThemeChangeEvent(1));
      }

scoped_model 實現總體代碼:

    import 'package:flutter/material.dart';
    import 'package:event_bus/event_bus.dart';
    import 'package:scoped_model/scoped_model.dart';
    import 'package:shared_preferences/shared_preferences.dart';
    import './config/application.dart';
    import './pages/home_page.dart';
    import './constants/theme.dart';
    import './models/state_model/main_model.dart';
     
    void main() async {
      int themeIndex = await getTheme();
      runApp(App(themeIndex));
    }
     
     
    Future<int> getTheme() async {
      SharedPreferences sp = await SharedPreferences.getInstance();
      int themeIndex = sp.getInt("themeIndex");
      if(themeIndex != null) {
        return themeIndex;
      }
      return 0;
    }
     
    class App extends StatefulWidget {
     
      final int themeIndex;
     
      App(this.themeIndex);
     
      @override
      _AppState createState() => _AppState();
    }
     
    class _AppState extends State<App> {
     
      @override
      void initState() {
        super.initState();
        Application.eventBus = new EventBus();
      }
     
      @override
      Widget build(BuildContext context) {
        return ScopedModel<MainStateModel>(
          model: MainStateModel(),
          child: ScopedModelDescendant<MainStateModel>(
            builder: (context, child, model) {
              return  MaterialApp(
                theme: ThemeData(
                  primaryColor: ThemeList[model.themeIndex != null ? model.themeIndex : widget.themeIndex]
                ),
                home: HomePage(),
              );
            },
          )
        );
      }
    }

      changeTheme() async {
        int themeIndex = MainStateModel().of(context).themeIndex == 0 ? 1 : 0;
        SharedPreferences sp = await SharedPreferences.getInstance();
        sp.setInt("themeIndex", themeIndex);
        MainStateModel().of(context).changeTheme(themeIndex);
      } redux

相關文章
相關標籤/搜索