基于 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);
}
基于 Flutter 以两种方式实现App主题切换的更多相关文章
- SpringBoot集成Mybatis实现多表查询的两种方式(基于xml)
下面将在用户和账户进行一对一查询的基础上进行介绍SpringBoot集成Mybatis实现多表查询的基于xml的两种方式. 首先我们先创建两个数据库表,分别是user用户表和account账户表 ...
- 在基于MVC的Web项目中使用Web API和直接连接两种方式混合式接入
在我之前介绍的混合式开发框架中,其界面是基于Winform的实现方式,后台使用Web API.WCF服务以及直接连接数据库的几种方式混合式接入,在Web项目中我们也可以采用这种方式实现混合式的接入方式 ...
- 五 Mybatis一对一关联查询的两种方式(基于resultType&基于resultMap)
关联查询: 一个用户对应多个订单,一个订单只有一个用户 订单关联用户:两种方式 一:基于resultTYpe,一个与表关系一样的pojo实现 主表订单,从表用户 首先要有一个与关联查询表关系一样的po ...
- 基于Maven的SpringBoot项目实现热部署的两种方式
转载:http://blog.csdn.net/tengxing007/article/details/72675168 前言 JRebel是JavaEE中比较流行的热部署插件,可快速实现热部署,节省 ...
- 《连载 | 物联网框架ServerSuperIO教程》- 10.持续传输大块数据流的两种方式(如:文件)
1.C#跨平台物联网通讯框架ServerSuperIO(SSIO)介绍 <连载 | 物联网框架ServerSuperIO教程>1.4种通讯模式机制. <连载 | 物联网框架Serve ...
- k8s创建资源的两种方式
命令 vs 配置文件 Kubernetes 支持两种方式创建资源: 1. 用 kubectl 命令直接创建 kubectl run nginx-deployment --image=nginx: -- ...
- egg.js 通过 form 和 ajax 两种方式上传文件并自定义目录和文件名
egg.js 通过 form 和 ajax 两种方式上传文件并自定义目录和文件名 评论:10 · 阅读:8437· 喜欢:0 一.需求 二.CSRF 校验 三.通过 form 表单上传文件 四.通过 ...
- Struts2实现ajax的两种方式
基于Struts2框架下实现Ajax有两种方式,第一种是原声的方式,另外一种是struts2自带的一个插件. js部分调用方式是一样的: JS代码: function testAjax() { var ...
- CSharpGL(18)分别处理glDrawArrays()和glDrawElements()两种方式下的拾取(ColorCodedPicking)
CSharpGL(18)分别处理glDrawArrays()和glDrawElements()两种方式下的拾取(ColorCodedPicking) 我在(Modern OpenGL用Shader拾取 ...
随机推荐
- 安卓备份 To Do(待办事项)的数据库
真正路径:/data/data/com.mediatek.todos/databases/todos.db 使用过链接的路径:/data/user/0/com.mediatek.todos/datab ...
- 关于在ASP.NET中使用JavaScript的建议
一个很恼人的情况,就是当你使用JS在一个ASP,NET应用程序中引用一个在模板页初始化的服务器控件的时候: 比如,我们在模板页有一个TextBox的服务器控件,而且我们想要去获取他的Text:如果你使 ...
- linux常用命令:watch 命令
watch是一个非常实用的命令,基本所有的Linux发行版都带有这个小工具,如同名字一样,watch可以帮你监测一个命令的运行结果,省得你一遍遍的手动运行.在Linux下,watch是周期性的执行下个 ...
- IO(字符流)
1. 由于Java采用16位的Unicode字符,因此需要基于字符的输入/输出操作.从Java1.1版开始,加入了专门处理字符流的抽象类Reader和Writer,前者用于处理输入,后者用于处 ...
- pm2 观察报错时 pm2 start app.js --watch
pm2 start app.js --watch[PM2][ERROR] Script already launched, add -f option to force re-execution
- Linux基础命令---mktemp
mktemp 创建临时文件或者目录,这样的创建方式是安全的.此命令的适用范围:RedHat.RHEL.Ubuntu.CentOS.SUSE.openSUSE.Fedora. 1.语法 mk ...
- IEEE发布2017年编程语言排行榜:Python高居首位,java第三,php第八
2017年7月18日,IEEE Spectrum 发布了第四届顶级编程语言交互排行榜.因为有各种不同语言的排行,所以 IEEE Spectrum 依据不同的变量对流行度进行了排行.据 IEEE Spe ...
- C/C++之内存对齐
数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍.DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽.X86 CPU能直接访问对齐的数据,当它试图访问一个未对齐的数据 ...
- 【运维技术】Maven + Gogs + Nexus 实现版本管理 + 代码模块开发管理
Gogs:能够实现fork + 代码提交 + 代码框架 Nexus:进行jar包的版本管理,私服下载jar包共享jar包 Maven:在客户端进行模块管理和批量操作 1. 本地maven仓库配置配置s ...
- OpenCV实现SfM(三):多目三维重建
http://lib.csdn.net/article/opencv/24548 注意:本文中的代码必须使用OpenCV3.0或以上版本进行编译,因为很多函数是3.0以后才加入的. 目录: 问题简化 ...