概述
App主题切换已经成为了一种流行的用户体验,丰富了应用整体UI视觉效果。例如,白天夜间模式切换。实现该功能的思想其实不难,就是将涉及主题的资源文件进行全局替换更新。说到这里,我想你肯定能联想到一种设计模式:观察者模式。多种观察对象(主题资源)来观察当前主题更新的行为(被观察对象),进行主题的更新。今天和大家分享在 Flutter 平台上如何实现主题更换。
效果
实现流程
在 Flutter 项目中,MaterialApp组件为开发者提供了设置主题的api:
1
2
3
4
5
|
const MaterialApp({ ... this .theme, // 主题 ... }) |
通过 theme 属性,我们可以设置在MaterialApp下的主题样式。theme 是 ThemeData 的对象实例:
1
2
3
4
5
6
7
8
9
10
11
12
|
ThemeData({ Brightness brightness, MaterialColor primarySwatch, Color primaryColor, Brightness primaryColorBrightness, Color primaryColorLight, Color primaryColorDark, ... }) |
ThemeData 中包含了很多主题设置,我们可以选择性的改变其中的颜色,字体等等。所以我们可以通过改变 primaryColor 来实现状态栏的颜色改变。并通过Theme来获取当前 primaryColor 颜色值,将其赋值到其他组件上即可。在触发主题更新行为时,通知 ThemeData 的 primaryColor改变行对应颜色值。 有了以上思路,接下来我们通过两种方式来展示如何实现主题的全局更新。
主题选项
在实例中我们以一下主题颜色为主:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
/** * 主题选项 */ 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。核心代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@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(), ); } |
然后在更新主题行为的地方来发送通知刷新即可:
1
2
3
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/** * 主题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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@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. 修改主题
1
2
3
4
|
changeTheme( int index) async { int themeIndex = index; MainStateModel().of(context).changeTheme(themeIndex); } |
可以看到,使用 scoped_model
的方式同样比较简单,思路和 EventBus 类似。以上代码我们实现了主题的切换,细心的朋友可以发现,我们还需要对主题进行保存,当下次启动 App 时,要显示上次切换的主题。Flutter中提供了 shared_preferences
来实现本地持久化存储。
主题持久化保存
当进行主题更换时,我们可以对主题进行持久化本地存储
1
2
3
4
5
|
void changeTheme( int themeIndex) async { _themeIndex = themeIndex; SharedPreferences sp = await SharedPreferences.getInstance(); sp.setInt( "themeIndex" , themeIndex); } |
然后在项目启动时,取出本地存储的主题下标,设置在theme上即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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 实现整体代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
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 实现整体代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
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); } |