1. 程式人生 > 程式設計 >Flutter以兩種方式實現App主題切換的程式碼

Flutter以兩種方式實現App主題切換的程式碼

概述

App主題切換已經成為了一種流行的使用者體驗,豐富了應用整體UI視覺效果。例如,白天夜間模式切換。實現該功能的思想其實不難,就是將涉及主題的資原始檔進行全域性替換更新。說到這裡,我想你肯定能聯想到一種設計模式:觀察者模式。多種觀察物件(主題資源)來觀察當前主題更新的行為(被觀察物件),進行主題的更新。今天和大家分享在Flutter 平臺上如何實現主題更換。

效果

Flutter以兩種方式實現App主題切換的程式碼

實現流程

在 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]
   ),);
  },)
 );
 }

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,model) {
   return MaterialApp(
   theme: ThemeData(
    primaryColor: themeList[model.themeIndex != null ? model.themeIndex : widget.themeIndex]
   ),)
 );
}

以上我們通過兩種方式來實現了主題的切換,實現思想都是通過通知的方式來觸發元件 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
  ),);
 }
 
 @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(),model) {
   return MaterialApp(
   theme: ThemeData(
    primaryColor: ThemeList[model.themeIndex != null ? model.themeIndex : widget.themeIndex]
   ),)
 );
 }
}
 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);
 }

總結