概述

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. Flutter中文教程-Cookbook

    Flutter中文网的Cookbook中包含了在编写Flutter应用程序时常见问题及示例。设计基础使用主题共享颜色和字体样式Images显示来自网上的图片用占位符淡入图片使用缓存图Lists创建一个基本list创建一个水平list使用长列表创建不同类型子项的List创建一个gridList处理手势处理点击添加Material触摸水波效果实现滑动关闭导航导航到新页面并返回给新页面传值从新页面返回数据给上一个页面网络从网上获取数据进行认证请求使用WebSockets

  2. android-studio – 未配置Dart SDK

    Initializinggradle…

  3. 安卓 – 从一个扑动的应用程序拨打电话

    或者有更好的选择从我的应用程序拨打电话?

  4. android – 如何在Flutter中添加Webview?

    我知道可以将WebView添加为整页,但找不到任何示例代码.我假设你可以使用PageView作为它的基础,但不知道如何调用本机androidWebView并将其添加到PageView.谁能指出我正确的方向?

  5. android – 如何将消息从Flutter传递给Native?

    如果需要与特定的API/硬件组件进行交互,您如何将Flutter的信息传递回Android/Native代码?是否有任何事件频道可以通过其他方式发送信息或类似于回调?

  6. android – 如何在Flutter App中处理onPause / onResume?

    我是否过于复杂的事情?即使我的用例似乎不需要它,我仍然想知道:如何自己处理onPause/onResume事件?

  7. android – 如何使用Flutter构建Augment Reality应用程序?

    我对Android开发有一些基础知识.最近听说过Flutter并且非常有兴趣研究它.我想知道是否有可能使用颤振构建增强现实应用程序以及要实现此目的的方法?请帮忙.解决方法截至目前,颤振不支持3D.Flutter现在专注于2D,团队长期计划为颤振提供优化的3Dapi.你读了常见问题here.

  8. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  9. Flutter StreamBuilder实现局部刷新实例详解

    这篇文章主要为大家介绍了Flutter StreamBuilder实现局部刷新实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. Flutter 首页必用组件NestedScrollView的示例详解

    今天介绍的组件是NestedScrollView,大部分的App首页都会用到这个组件。对Flutter 首页必用组件NestedScrollView的相关知识感兴趣的一起看看吧

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部