本篇学习系统主题监听、全局状态管理,实现完整的主题切换功能

图:古今职鉴开源教程封面。本篇围绕「主题适配:深色模式与浅色模式」展开。

学习目标

完成本篇后,你将能够:

  • ✅ 监听系统主题变化
  • ✅ 使用 AppStorage 管理全局状态
  • ✅ 设计符合 WCAG 标准的颜色体系
  • ✅ 实现深色/浅色模式自动切换

预计学习时间

约 90 分钟

---

实战一:获取当前系统主题

第一步:创建 lesson07 目录和文件

products/jiaocheng/src/main/ets/ 下创建 lesson07 文件夹,新建 Lesson07Page.ets

// 文件路径:products/jiaocheng/src/main/ets/lesson07/Lesson07Page.ets

import { ConfigurationConstant } from '@kit.AbilityKit';

@Entry
@Component
struct Lesson07Page {
  @State isDarkMode: boolean = false;

  aboutToAppear(): void {
    // 获取当前系统主题
    const context = getContext(this);
    const colorMode = context.config.colorMode;
    this.isDarkMode = colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
  }

  build() {
    Column() {
      Text(this.isDarkMode ? '当前是深色模式 🌙' : '当前是浅色模式 ☀️')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')
  }
}

第二步:理解 ConfigurationConstant.ColorMode

含义
COLOR_MODE_DARK 深色模式
COLOR_MODE_LIGHT 浅色模式
COLOR_MODE_NOT_SET 未设置(跟随系统)

第三步:运行查看效果

hvigorw assembleHap --no-daemon

预期效果

  • 页面显示当前系统主题状态
  • 背景色和文字颜色根据主题变化

问题:切换系统主题后,应用不会自动更新。下一步解决这个问题。

---

实战二:使用 AppStorage 全局状态

第一步:理解 AppStorage

AppStorage 是应用级别的状态存储:

  • 所有页面都可以访问
  • 支持响应式更新
  • 应用关闭后数据丢失(不持久化)

第二步:在 EntryAbility 中初始化主题状态

修改 products/jiaocheng/src/main/ets/jiaochengability/JiaochengAbility.ets

import { AbilityConstant, ConfigurationConstant, UIAbility, Want, Configuration } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

export default class JiaochengAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 初始化主题状态到 AppStorage
    const isDark = this.context.config.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
    AppStorage.setOrCreate('isDarkMode', isDark);
    console.info(`初始化主题: ${isDark ? '深色' : '浅色'}`);
  }

  // 监听系统配置变化(包括主题切换)
  onConfigurationUpdate(newConfig: Configuration): void {
    const isDark = newConfig.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
    AppStorage.set('isDarkMode', isDark);
    console.info(`主题切换: ${isDark ? '深色' : '浅色'}`);
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        console.error('Failed to load content');
      }
    });
  }
}

第三步:在页面中使用 @StorageProp

修改 Lesson07Page.ets

@Entry
@Component
struct Lesson07Page {
  // 从 AppStorage 读取主题状态(单向绑定)
  @StorageProp('isDarkMode') isDarkMode: boolean = false;

  build() {
    Column() {
      Text(this.isDarkMode ? '当前是深色模式 🌙' : '当前是浅色模式 ☀️')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')

      Text('切换系统主题试试看')
        .fontSize(14)
        .fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')
        .margin({ top: 16 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')
  }
}

第四步:理解 @StorageProp 和 @StorageLink

装饰器 数据流向 适用场景
@StorageProp 单向(只读) 只需要显示,不修改
@StorageLink 双向 需要修改全局状态
// 单向绑定:只读取,不写回
@StorageProp('isDarkMode') isDarkMode: boolean = false;

// 双向绑定:修改会同步到 AppStorage
@StorageLink('isDarkMode') isDarkMode: boolean = false;

第五步:运行验证

hvigorw assembleHap --no-daemon

预期效果

  • 切换系统主题后,应用界面自动更新
  • 无需重启应用

---

实战三:WCAG 色彩对比度规范

第一步:理解对比度要求

根据华为应用市场上架要求和 WCAG 标准:

场景 最低对比度
图标或标题文字与背景色 > 3:1
正文文字与背景色 > 4.5:1

第二步:深色模式推荐颜色

背景色 #221210 下的推荐颜色:

用途 颜色值 对比度 是否合规
主要文字 Color.White 15.8:1
次要文字 #9ca3af 5.2:1
提示文字 #6b7280 3.1:1 ⚠️ 仅标题可用

重要:深色模式下避免使用 #6b7280 作为正文颜色!

第三步:浅色模式推荐颜色

背景色 #f8f6f5 下的推荐颜色:

用途 颜色值 对比度 是否合规
主要文字 #1e293b 12.5:1
次要文字 #64748b 4.8:1
提示文字 #94a3b8 3.2:1 ⚠️ 仅标题可用

第四步:创建颜色速查表

在代码中使用这些颜色:

// 主要文字
.fontColor(this.isDarkMode ? Color.White : '#1e293b')

// 次要文字(满足 4.5:1)
.fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')

// 页面背景
.backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')

// 卡片背景
.backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)

---

实战四:完整主题适配页面

第一步:定义数据接口

interface CardItem {
  id: number;
  title: string;
  subtitle: string;
}

第二步:创建完整页面

// 文件路径:products/jiaocheng/src/main/ets/lesson07/Lesson07Page.ets

interface CardItem {
  id: number;
  title: string;
  subtitle: string;
}

@Entry
@Component
struct Lesson07Page {
  @StorageProp('isDarkMode') isDarkMode: boolean = false;

  private items: CardItem[] = [
    { id: 1, title: '丞相', subtitle: '百官之长,辅佐皇帝处理政务' },
    { id: 2, title: '太尉', subtitle: '掌管全国军事' },
    { id: 3, title: '御史大夫', subtitle: '监察百官,掌管图籍' }
  ];

  build() {
    Column() {
      this.PageHeader()
      this.ThemeStatus()

      List() {
        ForEach(this.items, (item: CardItem) => {
          ListItem() {
            this.ItemCard(item)
          }
        }, (item: CardItem) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 16, right: 16 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')
  }

  @Builder
  PageHeader() {
    Row() {
      Image($r('app.media.ic_back'))
        .width(24)
        .height(24)
        .fillColor(this.isDarkMode ? '#9ca3af' : '#1e293b')

      Text('主题适配演示')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')
        .margin({ left: 12 })

      Blank()
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
  }

  @Builder
  ThemeStatus() {
    Row() {
      Text('当前主题:')
        .fontSize(14)
        .fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')

      Text(this.isDarkMode ? '深色模式 🌙' : '浅色模式 ☀️')
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')
    }
    .width('100%')
    .padding(16)
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  ItemCard(item: CardItem) {
    Column() {
      Text(item.title)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')

      Text(item.subtitle)
        .fontSize(13)
        .fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')
        .margin({ top: 8 })
    }
    .width('100%')
    .padding(16)
    .margin({ bottom: 12 })
    .alignItems(HorizontalAlign.Start)
    .backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
    .borderRadius(12)
    .shadow({
      radius: 4,
      color: this.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.05)',
      offsetX: 0,
      offsetY: 2
    })
  }
}

@Builder
export function Lesson07PageBuilder() {
  Lesson07Page()
}

第三步:运行验证

hvigorw assembleHap --no-daemon

预期效果

  • 页面头部、卡片、文字颜色都随主题变化
  • 切换系统主题后自动更新
  • 所有文字对比度满足 WCAG 标准

---

实战五:创建主题工具类

第一步:创建 ThemeUtil.ets

// 文件路径:products/jiaocheng/src/main/ets/lesson07/ThemeUtil.ets

export class ThemeUtil {
  // ===== 背景色 =====
  static getBgPage(isDark: boolean): string {
    return isDark ? '#221210' : '#f8f6f5';
  }

  static getBgCard(isDark: boolean): string {
    return isDark ? '#2d1f1d' : '#ffffff';
  }

  static getBgInput(isDark: boolean): string {
    return isDark ? '#3d2f2d' : '#f0f0f0';
  }

  // ===== 文字颜色 =====
  static getTextPrimary(isDark: boolean): ResourceColor {
    return isDark ? Color.White : '#1e293b';
  }

  static getTextSecondary(isDark: boolean): string {
    return isDark ? '#9ca3af' : '#64748b';
  }

  // ===== 图标颜色 =====
  static getIconColor(isDark: boolean): string {
    return isDark ? '#9ca3af' : '#64748b';
  }

  // ===== 强调色(不随主题变化)=====
  static readonly primaryColor = '#c41e3a';
}

第二步:使用工具类简化代码

import { ThemeUtil } from './ThemeUtil';

// 使用前
.fontColor(this.isDarkMode ? Color.White : '#1e293b')

// 使用后
.fontColor(ThemeUtil.getTextPrimary(this.isDarkMode))

第三步:工具类的优势

  • 颜色值集中管理,修改方便
  • 代码更简洁,可读性更好
  • 避免颜色值写错

---

完整代码

// 文件路径:products/jiaocheng/src/main/ets/lesson07/Lesson07Page.ets

interface CardItem {
  id: number;
  title: string;
  subtitle: string;
}

@Entry
@Component
struct Lesson07Page {
  @StorageProp('isDarkMode') isDarkMode: boolean = false;

  private items: CardItem[] = [
    { id: 1, title: '丞相', subtitle: '百官之长,辅佐皇帝处理政务' },
    { id: 2, title: '太尉', subtitle: '掌管全国军事' },
    { id: 3, title: '御史大夫', subtitle: '监察百官,掌管图籍' }
  ];

  build() {
    Column() {
      this.PageHeader()
      this.ThemeStatus()

      List() {
        ForEach(this.items, (item: CardItem) => {
          ListItem() {
            this.ItemCard(item)
          }
        }, (item: CardItem) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 16, right: 16 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')
  }

  @Builder
  PageHeader() {
    Row() {
      Image($r('app.media.ic_back'))
        .width(24)
        .height(24)
        .fillColor(this.isDarkMode ? '#9ca3af' : '#1e293b')

      Text('主题适配演示')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')
        .margin({ left: 12 })

      Blank()
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
  }

  @Builder
  ThemeStatus() {
    Row() {
      Text('当前主题:')
        .fontSize(14)
        .fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')

      Text(this.isDarkMode ? '深色模式 🌙' : '浅色模式 ☀️')
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')
    }
    .width('100%')
    .padding(16)
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  ItemCard(item: CardItem) {
    Column() {
      Text(item.title)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')

      Text(item.subtitle)
        .fontSize(13)
        .fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')
        .margin({ top: 8 })
    }
    .width('100%')
    .padding(16)
    .margin({ bottom: 12 })
    .alignItems(HorizontalAlign.Start)
    .backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
    .borderRadius(12)
    .shadow({
      radius: 4,
      color: this.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.05)',
      offsetX: 0,
      offsetY: 2
    })
  }
}

@Builder
export function Lesson07PageBuilder() {
  Lesson07Page()
}

---

本课小结

核心知识点

知识点 说明
ConfigurationConstant.ColorMode 获取系统主题
onConfigurationUpdate 监听主题变化
AppStorage 应用级全局状态
@StorageProp 单向绑定(只读)
@StorageLink 双向绑定
WCAG 对比度 正文 > 4.5:1,标题 > 3:1

颜色速查表

模式 用途 颜色值
深色 页面背景 #221210
深色 卡片背景 #2d1f1d
深色 主要文字 Color.White
深色 次要文字 #9ca3af
浅色 页面背景 #f8f6f5
浅色 卡片背景 #ffffff
浅色 主要文字 #1e293b
浅色 次要文字 #64748b

---

课后练习

练习1:添加手动切换开关

使用 @StorageLink 实现手动切换主题:

@StorageLink('isDarkMode') isDarkMode: boolean = false;

Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
  .onChange((isOn: boolean) => {
    this.isDarkMode = isOn;
  })

练习2:为输入框添加主题适配

TextInput({ placeholder: '请输入' })
  .backgroundColor(this.isDarkMode ? '#3d2f2d' : '#f0f0f0')
  .fontColor(this.isDarkMode ? Color.White : '#1e293b')
  .placeholderColor(this.isDarkMode ? '#6b7280' : '#94a3b8')

---

下一课预告

第8课我们将学习动画与交互,包括:

  • 属性动画 animateTo
  • 转场动画 transition
  • 手势识别系统
  • 实现卡片弹出动画

项目开源地址

https://gitcode.com/daleishen/gujinzhijian

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐