概述

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

基于 Flutter 以两种方式实现App主题切换的更多相关文章

  1. SpringBoot集成Mybatis实现多表查询的两种方式(基于xml)

     下面将在用户和账户进行一对一查询的基础上进行介绍SpringBoot集成Mybatis实现多表查询的基于xml的两种方式.   首先我们先创建两个数据库表,分别是user用户表和account账户表 ...

  2. 在基于MVC的Web项目中使用Web API和直接连接两种方式混合式接入

    在我之前介绍的混合式开发框架中,其界面是基于Winform的实现方式,后台使用Web API.WCF服务以及直接连接数据库的几种方式混合式接入,在Web项目中我们也可以采用这种方式实现混合式的接入方式 ...

  3. 五 Mybatis一对一关联查询的两种方式(基于resultType&基于resultMap)

    关联查询: 一个用户对应多个订单,一个订单只有一个用户 订单关联用户:两种方式 一:基于resultTYpe,一个与表关系一样的pojo实现 主表订单,从表用户 首先要有一个与关联查询表关系一样的po ...

  4. 基于Maven的SpringBoot项目实现热部署的两种方式

    转载:http://blog.csdn.net/tengxing007/article/details/72675168 前言 JRebel是JavaEE中比较流行的热部署插件,可快速实现热部署,节省 ...

  5. 《连载 | 物联网框架ServerSuperIO教程》- 10.持续传输大块数据流的两种方式(如:文件)

    1.C#跨平台物联网通讯框架ServerSuperIO(SSIO)介绍 <连载 | 物联网框架ServerSuperIO教程>1.4种通讯模式机制. <连载 | 物联网框架Serve ...

  6. k8s创建资源的两种方式

    命令 vs 配置文件 Kubernetes 支持两种方式创建资源: 1. 用 kubectl 命令直接创建 kubectl run nginx-deployment --image=nginx: -- ...

  7. egg.js 通过 form 和 ajax 两种方式上传文件并自定义目录和文件名

    egg.js 通过 form 和 ajax 两种方式上传文件并自定义目录和文件名 评论:10 · 阅读:8437· 喜欢:0 一.需求 二.CSRF 校验 三.通过 form 表单上传文件 四.通过 ...

  8. Struts2实现ajax的两种方式

    基于Struts2框架下实现Ajax有两种方式,第一种是原声的方式,另外一种是struts2自带的一个插件. js部分调用方式是一样的: JS代码: function testAjax() { var ...

  9. CSharpGL(18)分别处理glDrawArrays()和glDrawElements()两种方式下的拾取(ColorCodedPicking)

    CSharpGL(18)分别处理glDrawArrays()和glDrawElements()两种方式下的拾取(ColorCodedPicking) 我在(Modern OpenGL用Shader拾取 ...

随机推荐

  1. ES6(简)

    一. let.const 和 var let和const只在当前块级作用域中有效const用来声明常量var是全局作用域有效的 constants.js 模块export const A = 1;ex ...

  2. 剑指offer3

    输入一个链表,从尾到头打印链表每个节点的值. 思路:首先借助一个栈,遍历链表中的每一个值,然后存储到栈中,利用栈的先进后出特点,然后添加到数组中返回. package demo3; import ja ...

  3. sql server数字转字符串出现科学计数法

    在从excel往sql server导入数据,电话.编号等数字呈现float类型,然后向b表中insert后(phone为nvarchar)出现科学计数法,解决方法:需将float等数据类型转为标准的 ...

  4. SQL Server 运行状况监控SQL语句

    Microsoft SQL Server 2005 提供了一些工具来监控数据库.方法之一是动态管理视图.动态管理视图 (DMV) 和动态管理函数 (DMF) 返回的服务器状态信息可用于监控服务器实例的 ...

  5. 710 Random Pick with Blacklist

    1. 问题 给定一个黑名单,包含[0, N)的一些数,从[0, N)之间的非黑名单数中随机采样一个值. 2. 思路 字典映射 (1)计算黑名单数的长度,记作B,因为已经排除掉了B个元素,所以最后是从N ...

  6. OS X 下动态库的引用

    foo.c #include <stdio.h> void foo(void) { printf("foo.\n"); } main.c #include <st ...

  7. python练习-生成一个1到50的大字符串每个数字之间有个空格

    #-*-encoding:UTF-8-*- string=[] for i in range(1,51): string.append(str(i)) print string#打印一下string ...

  8. spoj1825 Free tour II

    题目链接 一道神奇的点分治 貌似有很多做法,我觉得BIT要好些一些(雾 要求经过黑点数<k就用BIT区间查询前缀 对于每个点用  BIT[0,k-经过黑点数]的最大值+路径长度 使用点分治做到O ...

  9. 【翻唱】【你的名字MAD】爱你等于爱自己

    http://video.yingtu.co/0/8f606e9b-9694-4d35-a0bf-730391a3ee12.mp4 [你的名字MAD]爱你等于爱自己-原唱翻唱 http://video ...

  10. ELK之elasticsearch6.5集群

    前面介绍并初试了es6.5系列的单节点的操作,现在搭建es6.5系列的集群: 环境:三节点:master-172.16.23.128.node1-172.16.23.129.node2-172.16. ...