设置中心开发

本文是《HarmonyOS NEXT 企业级开发实战:30篇打造智能记账APP》系列的第 24 篇,对应 Git Tag v0.2.4。承接前序开发,本篇开发 SettingView 设置中心,支持深色模式切换、数据导出、清空数据与关于页面跳转等能力,底层基于 SettingRepositoryPreferenceUtil 完成偏好持久化。

前言

企业级应用的可配置能力数据掌控能力决定产品成熟度。一个完善的设置中心,不仅要让用户自定义外观偏好(如深色模式),还要让用户能够导出和清理自己的数据。本文将带你:

  1. 设计 SettingRepository 设置仓库与 PreferenceUtil 偏好存储工具
  2. 实现 SettingView 设置页面的完整布局与交互
  3. 封装 ConfirmDialog 确认对话框处理危险操作
  4. 集成 RouterUtil 路由工具完成页面跳转
  5. 遵循 ArkTS 类型安全规范,杜绝 any 类型

企业级核心原则:功能必须完整、可恢复、类型安全。ArkTS 严格禁止使用 any 类型,所有类名必须使用英文标识符。参考 HarmonyOS NEXT 开发者文档 了解官方约定。


一、需求分析

1.1 功能介绍

设置中心是用户管理应用偏好与数据的统一入口。本篇实现的 SettingView 包含四项核心配置,全部基于 List 组件构建分组列表。

需求项 说明
深色模式 通过 Toggle 开关切换深色模式,持久化到 SettingRepository,重启后生效
导出数据 将账单与分类数据序列化为 JSON 格式,便于用户备份迁移
清空数据 危险操作,通过 ConfirmDialog 二次确认后清空所有偏好数据
关于页面 跳转至 AboutView,展示应用名称、版本号与开源信息

1.2 设置项规划

设置项采用分组列表布局,每项为独立 ListItem,通过 Row 容器横向排列标题与操作控件。

设置项 交互控件 触发动作 危险等级
深色模式 Toggle 开关 saveDarkMode 持久化
导出数据 箭头图标 exportData 序列化 JSON
清空所有数据 红色文字 弹出 ConfirmDialog 确认
关于 箭头图标 RouterUtil.push 跳转

1.3 业务流程

用户进入 SettingView
  ↓
aboutToAppear 调用 loadSettings
  ↓
SettingRepository.loadDarkMode 读取深色模式偏好
  ↓
UI 渲染分组列表(Toggle / 文字 / 箭头)
  ↓
用户交互 → 更新 @State → 持久化偏好 → Toast 反馈
  ↓
清空操作 → ConfirmDialog 二次确认 → PreferenceUtil.clear

二、SettingRepository 设置仓库

2.1 单例模式实现

SettingRepository 是设置数据的统一仓库层,采用单例模式确保全局唯一实例。它封装了对 PreferenceUtil 的调用,对外暴露主题、语言、币种、深色模式等配置的读写方法。

// repository/SettingRepository.ets
import { PreferenceUtil } from '../utils/PreferenceUtil';

export class SettingRepository {
  private static instance: SettingRepository;
  static getInstance(): SettingRepository {
    if (!SettingRepository.instance) {
      SettingRepository.instance = new SettingRepository();
    }
    return SettingRepository.instance;
  }

  async saveTheme(mode: string): Promise<void> {
    await PreferenceUtil.getInstance().setString('setting_theme', mode);
  }

  async loadTheme(): Promise<string> {
    return await PreferenceUtil.getInstance().getString('setting_theme', 'auto');
  }

  async saveLanguage(lang: string): Promise<void> {
    await PreferenceUtil.getInstance().setString('setting_language', lang);
  }

  async saveCurrency(currency: string): Promise<void> {
    await PreferenceUtil.getInstance().setString('setting_currency', currency);
  }

  async saveDarkMode(enabled: boolean): Promise<void> {
    await PreferenceUtil.getInstance().setBoolean('setting_dark_mode', enabled);
  }

  async loadDarkMode(): Promise<boolean> {
    return await PreferenceUtil.getInstance().getBoolean('setting_dark_mode', false);
  }
}

2.2 偏好读写封装

每个配置项对应一个独立的 key,通过 PreferenceUtilsetString/getStringsetBoolean/getBoolean 完成读写。注意所有方法参数与返回值均有明确的类型标注,不使用 any

2.3 字段说明

方法 参数类型 返回类型 存储键 用途
saveTheme string Promise<void> setting_theme 保存主题模式
loadTheme Promise<string> setting_theme 读取主题模式
saveLanguage string Promise<void> setting_language 保存语言设置
saveCurrency string Promise<void> setting_currency 保存币种设置
saveDarkMode boolean Promise<void> setting_dark_mode 保存深色模式开关
loadDarkMode Promise<boolean> setting_dark_mode 读取深色模式开关

三、PreferenceUtil 偏好存储工具

3.1 初始化与单例

PreferenceUtil 是基于 @kit.ArkDatapreferences 模块封装的轻量存储工具。它需要在应用启动时调用 init 方法传入 Context 完成初始化,之后即可全局调用。

// utils/PreferenceUtil.ets
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

const PREF_NAME = 'harmonyledger';

export class PreferenceUtil {
  private pref: preferences.Preferences | null = null;
  private static instance: PreferenceUtil | null = null;

  static getInstance(): PreferenceUtil {
    if (PreferenceUtil.instance === null) {
      PreferenceUtil.instance = new PreferenceUtil();
    }
    return PreferenceUtil.instance;
  }

  async init(context: common.Context): Promise<void> {
    this.pref = await preferences.getPreferences(context, PREF_NAME);
  }
}

3.2 基本类型读写

PreferenceUtil 提供了字符串、布尔、数字三种基本类型的读写方法,每次写入后自动调用 flush 持久化到磁盘。

async setString(key: string, value: string): Promise<void> {
  if (this.pref === null) return;
  await this.pref.put(key, value);
  await this.pref.flush();
}

async getString(key: string, defaultValue: string = ''): Promise<string> {
  if (this.pref === null) return defaultValue;
  let result: ESObject = await this.pref.get(key, defaultValue);
  return '' + result;
}

async setBoolean(key: string, value: boolean): Promise<void> {
  if (this.pref === null) return;
  await this.pref.put(key, value);
  await this.pref.flush();
}

async getBoolean(key: string, defaultValue: boolean = false): Promise<boolean> {
  if (this.pref === null) return defaultValue;
  let result: ESObject = await this.pref.get(key, defaultValue);
  return result === true || result === 'true';
}

3.3 对象读写与清空

除基本类型外,PreferenceUtil 还支持泛型对象的序列化存储,以及 delete 单项删除和 clear 全量清空。SettingView 的清空数据功能正是调用 clear 方法。

async setObject<T>(key: string, value: T): Promise<void> {
  if (this.pref === null) return;
  const json = JSON.stringify(value);
  await this.pref.put(key, json);
  await this.pref.flush();
}

async delete(key: string): Promise<void> {
  if (this.pref === null) return;
  await this.pref.delete(key);
  await this.pref.flush();
}

async clear(): Promise<void> {
  if (this.pref === null) return;
  await this.pref.clear();
  await this.pref.flush();
}

3.4 API 说明

方法 参数 返回值 说明
init context: common.Context Promise<void> 初始化偏好实例
setString key, value Promise<void> 写入字符串
getString key, defaultValue Promise<string> 读取字符串
setBoolean key, value Promise<void> 写入布尔值
getBoolean key, defaultValue Promise<boolean> 读取布尔值
setObject<T> key, value: T Promise<void> 写入泛型对象
clear Promise<void> 清空所有偏好

四、SettingView 页面实现

4.1 页面状态设计

SettingView 使用两个 @State 状态变量管理页面数据。没有使用独立的 ViewModel,业务逻辑直接调用 SettingRepositoryPreferenceUtil,因为设置页面的状态非常简单,无需引入额外的抽象层。

@State darkMode: boolean = false;        // 深色模式开关状态
@State showClearConfirm: boolean = false; // 清空确认对话框显示状态

4.2 完整页面代码

以下是 SettingView 的完整实现,包含顶栏、分组列表、确认对话框三部分。所有代码与实际源码完全一致。

// pages/SettingView.ets
import { AppColors } from '../theme/Colors';
import { AppFontSize } from '../theme/Typography';
import { AppSpace } from '../theme/Spacing';
import { RouterUtil } from '../utils/RouterUtil';
import { SettingRepository } from '../repository/SettingRepository';
import { PreferenceUtil } from '../utils/PreferenceUtil';
import { ToastUtil } from '../utils/ToastUtil';
import { ConfirmDialog } from '../components/dialog/ConfirmDialog';

@Entry
@Component
struct SettingView {
  @State darkMode: boolean = false;
  @State showClearConfirm: boolean = false;

  aboutToAppear(): void {
    this.loadSettings();
  }

  private async loadSettings(): Promise<void> {
    this.darkMode = await SettingRepository.getInstance().loadDarkMode();
  }

  build() {
    Column() {
      Row() {
        Image($r('app.media.icon_back')).width(24).height(24).fillColor(AppColors.PrimaryText)
          .onClick(() => { RouterUtil.back(); })
        Text('设置').fontSize(AppFontSize.XL).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)
      }.width('100%').height(56).alignItems(VerticalAlign.Center)

      List({ space: AppSpace.SM }) {
        // 深色模式
        ListItem() {
          Row() {
            Text('深色模式').fontSize(AppFontSize.MD).fontColor(AppColors.PrimaryText).layoutWeight(1)
            Toggle({ type: ToggleType.Switch, isOn: this.darkMode })
              .onChange((isOn: boolean) => {
                this.darkMode = isOn;
                SettingRepository.getInstance().saveDarkMode(isOn);
                ToastUtil.show('重启应用后生效');
              })
          }
          .width('100%').height(56)
          .padding({ left: AppSpace.MD, right: AppSpace.MD })
          .backgroundColor(AppColors.CardBackground).borderRadius(AppSpace.CardRadius)
        }

        // 数据导出
        ListItem() {
          Row() {
            Text('导出数据').fontSize(AppFontSize.MD).fontColor(AppColors.PrimaryText).layoutWeight(1)
            Image($r('app.media.icon_arrow_right')).width(20).height(20).fillColor(AppColors.SecondaryText)
          }
          .width('100%').height(56)
          .padding({ left: AppSpace.MD, right: AppSpace.MD })
          .backgroundColor(AppColors.CardBackground).borderRadius(AppSpace.CardRadius)
          .onClick(() => { this.exportData(); })
        }

        // 清空数据
        ListItem() {
          Row() {
            Text('清空所有数据').fontSize(AppFontSize.MD).fontColor(AppColors.Expense).layoutWeight(1)
          }
          .width('100%').height(56)
          .padding({ left: AppSpace.MD, right: AppSpace.MD })
          .backgroundColor(AppColors.CardBackground).borderRadius(AppSpace.CardRadius)
          .onClick(() => { this.showClearConfirm = true; })
        }

        // 关于
        ListItem() {
          Row() {
            Text('关于').fontSize(AppFontSize.MD).fontColor(AppColors.PrimaryText).layoutWeight(1)
            Image($r('app.media.icon_arrow_right')).width(20).height(20).fillColor(AppColors.SecondaryText)
          }
          .width('100%').height(56)
          .padding({ left: AppSpace.MD, right: AppSpace.MD })
          .backgroundColor(AppColors.CardBackground).borderRadius(AppSpace.CardRadius)
          .onClick(() => { RouterUtil.push('pages/AboutView'); })
        }
      }
      .layoutWeight(1).margin({ top: AppSpace.MD })

      if (this.showClearConfirm) {
        ConfirmDialog({
          title: '确认清空',
          message: '清空后将删除所有账单、分类和预算数据,此操作不可恢复!',
          confirmText: '清空',
          confirmColor: AppColors.Expense,
          onConfirm: () => { this.doClear(); },
          onCancel: () => { this.showClearConfirm = false; }
        })
      }
    }
    .height('100%').padding({ left: AppSpace.XL, right: AppSpace.XL, top: AppSpace.MD })
    .backgroundColor(AppColors.Background)
  }

  private async exportData(): Promise<void> {
    const billsJson = await PreferenceUtil.getInstance().getString('bills', '[]');
    const categoriesJson = await PreferenceUtil.getInstance().getString('categories', '[]');
    interface ExportData {
      bills: string;
      categories: string;
      exportTime: string;
    }
    const data: ExportData = {
      bills: billsJson,
      categories: categoriesJson,
      exportTime: new Date().toISOString()
    };
    const json = JSON.stringify(data, null, 2);
    ToastUtil.show('数据已准备(JSON格式)');
  }

  private async doClear(): Promise<void> {
    await PreferenceUtil.getInstance().clear();
    this.showClearConfirm = false;
    ToastUtil.show('数据已清空');
  }
}

4.3 设置项列表解析

页面主体使用 List({ space: AppSpace.SM }) 构建分组列表,每个 ListItem 内部是一个 Row 容器:

  • 深色模式:左侧 Text 标题占据 layoutWeight(1),右侧 Toggle 开关,onChange 回调中同步状态并持久化
  • 导出数据:左侧标题 + 右侧箭头图标,点击触发 exportData 方法
  • 清空数据:文字使用 AppColors.Expense 红色警示,点击弹出确认对话框
  • 关于:标准箭头布局,点击跳转 pages/AboutView

五、核心功能实现

5.1 深色模式切换

深色模式通过 Toggle 组件实现开关交互,状态变化时立即调用 SettingRepository.saveDarkMode 持久化,并通过 ToastUtil 提示用户重启生效。

Toggle({ type: ToggleType.Switch, isOn: this.darkMode })
  .onChange((isOn: boolean) => {
    this.darkMode = isOn;
    SettingRepository.getInstance().saveDarkMode(isOn);
    ToastUtil.show('重启应用后生效');
  })

注意onChange 回调参数 isOn 的类型明确标注为 boolean,不允许使用 any。这是 ArkTS 类型安全的强制要求。

5.2 数据导出

exportData 方法从 PreferenceUtil 读取账单与分类的 JSON 字符串,组装为 ExportData 接口对象后序列化输出。关于数据导出的完整设计,将在下一篇详细展开。

private async exportData(): Promise<void> {
  const billsJson = await PreferenceUtil.getInstance().getString('bills', '[]');
  const categoriesJson = await PreferenceUtil.getInstance().getString('categories', '[]');
  interface ExportData {
    bills: string;
    categories: string;
    exportTime: string;
  }
  const data: ExportData = {
    bills: billsJson,
    categories: categoriesJson,
    exportTime: new Date().toISOString()
  };
  const json = JSON.stringify(data, null, 2);
  ToastUtil.show('数据已准备(JSON格式)');
}

导出方法的执行步骤如下:

  1. 调用 PreferenceUtil.getString 异步读取 billscategories 偏好数据
  2. 定义 ExportData 接口约束导出数据结构
  3. 组装包含账单、分类和导出时间的 ExportData 对象
  4. 使用 JSON.stringify 序列化为格式化 JSON 字符串
  5. 通过 ToastUtil.show 提示用户导出完成

5.3 清空数据

清空数据是高危操作,必须经过 ConfirmDialog 二次确认。确认后调用 PreferenceUtil.getInstance().clear() 清空所有偏好数据。

private async doClear(): Promise<void> {
  await PreferenceUtil.getInstance().clear();
  this.showClearConfirm = false;
  ToastUtil.show('数据已清空');
}

5.4 关于页面跳转

关于页面通过 RouterUtil.push 跳转,传递路由路径字符串 pages/AboutView

.onClick(() => { RouterUtil.push('pages/AboutView'); })

六、ConfirmDialog 确认对话框

6.1 组件设计

ConfirmDialog 是通用的确认对话框组件,通过 @Prop 接收标题、消息、按钮文案与颜色,通过回调函数 onConfirm/onCancel 处理用户操作。

// components/dialog/ConfirmDialog.ets
import { AppColors } from '../../theme/Colors';
import { AppFontSize, AppFontWeight } from '../../theme/Typography';
import { AppSpace } from '../../theme/Spacing';

@Component
export struct ConfirmDialog {
  @Prop title: string = '确认';
  @Prop message: string = '';
  @Prop confirmText: string = '确认';
  @Prop cancelText: string = '取消';
  @Prop confirmColor: string = AppColors.Budget;
  onConfirm: () => void = () => {};
  onCancel: () => void = () => {};

  build() {
    Column() {
      Column()
        .width('100%').height('100%').backgroundColor('#80000000')
        .onClick(() => { this.onCancel(); })

      Column() {
        Text(this.title).fontSize(AppFontSize.LG).fontWeight(AppFontWeight.Bold).margin({ bottom: AppSpace.MD })
        Text(this.message).fontSize(AppFontSize.MD).fontColor(AppColors.SecondaryText)
          .textAlign(TextAlign.Center).margin({ bottom: AppSpace.LG })
        Row({ space: AppSpace.MD }) {
          Text(this.cancelText).layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 12, bottom: 12 }).backgroundColor(AppColors.Background).borderRadius(24)
            .onClick(() => { this.onCancel(); })
          Text(this.confirmText).layoutWeight(1).textAlign(TextAlign.Center).fontColor('#FFFFFF')
            .padding({ top: 12, bottom: 12 }).backgroundColor(this.confirmColor).borderRadius(24)
            .onClick(() => { this.onConfirm(); })
        }.width('100%')
      }
      .width('80%').padding(AppSpace.LG)
      .backgroundColor(AppColors.CardBackground).borderRadius(16)
    }
    .width('100%').height('100%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
  }
}

6.2 在 SettingView 中的使用

SettingView 中,通过条件渲染 if (this.showClearConfirm) 控制对话框的显示与隐藏,确认按钮使用红色 AppColors.Expense 突出危险操作。

if (this.showClearConfirm) {
  ConfirmDialog({
    title: '确认清空',
    message: '清空后将删除所有账单、分类和预算数据,此操作不可恢复!',
    confirmText: '清空',
    confirmColor: AppColors.Expense,
    onConfirm: () => { this.doClear(); },
    onCancel: () => { this.showClearConfirm = false; }
  })
}

七、路由配置与页面注册

7.1 main_pages.json 路由注册

SettingViewAboutView 都需要在 main_pages.json 中注册路由路径,否则 RouterUtil.push 跳转时会报错。

// entry/src/main/resources/base/profile/main_pages.json
{
  "src": [
    "pages/MainView",
    "pages/AddBillView",
    "pages/EditBillView",
    "pages/SearchView",
    "pages/CategoryManageView",
    "pages/SettingView",
    "pages/AboutView"
  ]
}

7.2 RouterUtil 路由工具

RouterUtil 封装了 @kit.ArkUIrouter 模块,提供 pushpushWithIdbackreplace 四个静态方法,统一处理异常捕获。

// utils/RouterUtil.ets
import { router } from '@kit.ArkUI';

export interface RouteParams {
  id: string;
}

export class RouterUtil {
  static push(url: string): void {
    router.pushUrl({ url: url }).catch((err: Error) => {
      console.error('Router push failed:', JSON.stringify(err));
    });
  }

  static pushWithId(url: string, id: string): void {
    let params: RouteParams = { id: id };
    router.pushUrl({ url: url, params: params }).catch((err: Error) => {
      console.error('Router push failed:', JSON.stringify(err));
    });
  }

  static back(): void {
    try {
      router.back();
    } catch (e) {
      console.error('Router back failed:', JSON.stringify(e));
    }
  }

  static replace(url: string): void {
    router.replaceUrl({ url: url }).catch((err: Error) => {
      console.error('Router replace failed:', JSON.stringify(err));
    });
  }
}

7.3 AboutView 关于页面

AboutView 是设置中心跳转的目标页面,展示应用图标、名称、版本号与开源信息。

// pages/AboutView.ets
import { AppColors } from '../theme/Colors';
import { AppFontSize } from '../theme/Typography';
import { AppSpace } from '../theme/Spacing';
import { RouterUtil } from '../utils/RouterUtil';

@Entry
@Component
struct AboutView {
  build() {
    Column() {
      Row() {
        Image($r('app.media.icon_back')).width(24).height(24).fillColor(AppColors.PrimaryText)
          .onClick(() => { RouterUtil.back(); })
        Text('关于').fontSize(AppFontSize.XL).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)
      }.width('100%').height(56).alignItems(VerticalAlign.Center)

      Column() {
        Image($r('app.media.icon_tab_user')).width(80).height(80).fillColor(AppColors.Budget)
          .margin({ top: 40 })
        Text('HarmonyLedger').fontSize(24).fontColor(AppColors.PrimaryText).fontWeight(FontWeight.Bold)
          .margin({ top: AppSpace.LG })
        Text('鸿蒙记账').fontSize(16).fontColor(AppColors.SecondaryText)
          .margin({ top: AppSpace.XS })
        Text('v1.0.0').fontSize(14).fontColor(AppColors.SecondaryText)
          .margin({ top: AppSpace.SM })
        Text('HarmonyOS NEXT 企业级记账 APP').fontSize(14).fontColor(AppColors.SecondaryText)
          .margin({ top: AppSpace.XL })
        Text('基于 ArkTS + ArkUI 开发').fontSize(14).fontColor(AppColors.SecondaryText)
          .margin({ top: AppSpace.XS })
        Text('GitHub 开源项目').fontSize(14).fontColor(AppColors.Budget)
          .margin({ top: AppSpace.XL })
      }
      .width('100%')
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
    }
    .height('100%').padding({ left: AppSpace.XL, right: AppSpace.XL, top: AppSpace.MD })
    .backgroundColor(AppColors.Background)
  }
}

八、主题常量体系

8.1 AppColors 颜色常量

AppColors 使用 static readonly 定义所有颜色 Token,包括语义颜色(收入绿、支出红、预算蓝、统计紫)与中性颜色(背景、卡片、文字)。

// theme/Colors.ets
export class AppColors {
  // Base Token
  static readonly Green500: string = '#34C759';
  static readonly Red500: string = '#FF3B30';
  static readonly Blue500: string = '#007AFF';
  static readonly Purple500: string = '#AF52DE';

  // Semantic Token
  static readonly Income: string = AppColors.Green500;
  static readonly Expense: string = AppColors.Red500;
  static readonly Budget: string = AppColors.Blue500;
  static readonly Statistic: string = AppColors.Purple500;

  // Neutral
  static readonly Background: string = '#F2F2F7';
  static readonly CardBackground: string = '#FFFFFF';
  static readonly PrimaryText: string = '#1C1C1E';
  static readonly SecondaryText: string = '#8E8E93';
  static readonly Separator: string = '#E5E5EA';
}

8.2 AppFontSize 字体常量

// theme/Typography.ets
export class AppFontSize {
  static readonly XS: number = 12;
  static readonly SM: number = 14;
  static readonly MD: number = 16;
  static readonly LG: number = 18;
  static readonly XL: number = 22;
  static readonly XXL: number = 28;
  static readonly Display: number = 36;
}

8.3 AppSpace 间距常量

// theme/Spacing.ets
export class AppSpace {
  static readonly XS: number = 4;
  static readonly SM: number = 8;
  static readonly MD: number = 16;
  static readonly LG: number = 20;
  static readonly XL: number = 24;
  static readonly XXL: number = 32;
  static readonly PagePadding: number = 20;
  static readonly CardPadding: number = 16;
  static readonly CardRadius: number = 16;
}

8.4 主题常量对照表

常量类 代表含义 示例值 使用场景
AppColors.Expense 支出红 #FF3B30 清空数据警示文字
AppColors.Budget 预算蓝 #007AFF 关于页图标
AppFontSize.XL 大标题 22 顶栏标题
AppFontSize.MD 正文 16 列表项文字
AppSpace.CardRadius 卡片圆角 16 ListItem 圆角
AppSpace.XL 大间距 24 页面左右边距

九、ArkTS 类型安全规范

9.1 禁用 any 类型

ArkTS 是基于 TypeScript 的强类型语言,严格禁止使用 any 类型any 类型会绕过编译期类型检查,导致运行时错误难以定位。在 SettingView 的实现中,所有变量、参数、返回值都有明确的类型标注。

9.2 接口定义代替 any

当需要描述复杂对象结构时,应使用 interface 定义接口,而非依赖 anySettingViewexportData 方法中定义了 ExportData 接口来约束导出数据结构。

错误写法(禁止)

// 违反 ArkTS 类型安全,禁止使用
private async exportData(): Promise<void> {
  const data: any = {
    bills: '',
    categories: '',
    exportTime: ''
  };
}

正确写法(推荐)

// 使用 interface 定义明确的类型约束
interface ExportData {
  bills: string;
  categories: string;
  exportTime: string;
}

const data: ExportData = {
  bills: billsJson,
  categories: categoriesJson,
  exportTime: new Date().toISOString()
};

9.3 类名必须使用英文

ArkTS 中类名、结构体名、接口名必须使用英文标识符。中文标识符在编译时会报错。

场景 错误写法 正确写法
仓库类名 中文类名(编译报错) SettingRepository
页面结构体名 中文结构体名(编译报错) SettingView
方法参数类型 item: any item: ExportData
列表数据类型 data: Array<any> data: Array<ExportData>
ForEach 键值函数 (item: any) => item.id (item: ExportData) => item.exportTime

9.4 异步方法返回类型

所有 async 方法必须显式标注返回类型为 Promise<T>,不能省略。SettingView 中的三个异步方法均遵循此规范:

private async loadSettings(): Promise<void> { ... }
private async exportData(): Promise<void> { ... }
private async doClear(): Promise<void> { ... }

十、运行验证

10.1 构建命令

hvigorw assembleHap --mode module -p product=default

10.2 验证清单

验证项 操作步骤 预期结果
进入设置页 从主页点击设置入口 展示四项设置列表
深色模式切换 点击 Toggle 开关 开关状态变化,Toast 提示"重启应用后生效"
重启后保持 切换后重启应用 深色模式状态与切换前一致
导出数据 点击"导出数据"项 Toast 提示"数据已准备(JSON格式)"
清空确认弹窗 点击"清空所有数据" 弹出 ConfirmDialog 确认框
取消清空 在确认框点击"取消" 对话框关闭,数据不变
确认清空 在确认框点击"清空" 数据清空,Toast 提示"数据已清空"
关于跳转 点击"关于"项 跳转至 AboutView 页面

验证要点:深色模式持久化需重启验证;清空操作需确认数据确实被清除(重新进入账单页应为空)。


十一、常见问题

11.1 PreferenceUtil 未初始化

现象:调用 getString/getBoolean 时始终返回默认值,数据无法持久化。

原因PreferenceUtilinit 方法未在应用启动时调用,导致 this.prefnull

解决:在 EntryAbilityonCreate 中调用初始化:

// EntryAbility.ets
export default class EntryAbility extends UIAbility {
  async onCreate(want, launchParam): Promise<void> {
    await PreferenceUtil.getInstance().init(this.context);
  }
}

11.2 深色模式切换不生效

现象:Toggle 开关切换后,UI 颜色没有立即变化。

原因:当前实现仅持久化深色模式开关,实际主题切换需重启应用后由 ThemeManager 读取偏好并应用。

解决:Toast 已提示"重启应用后生效",若需即时切换,需在 onChange 中同时调用 ThemeManager.switch 并通过 AppStorage 全局刷新。

11.3 清空数据误操作

现象:用户误触清空,数据全部丢失。

原因PreferenceUtil.clear() 会清空所有偏好数据,不可恢复。

解决:已通过 ConfirmDialog 二次确认防护。建议在确认对话框的文案中明确说明后果,当前文案为"清空后将删除所有账单、分类和预算数据,此操作不可恢复!"。

11.4 路由跳转失败

现象:点击"关于"项后页面无反应或报错。

原因main_pages.json 中未注册 pages/AboutView 路由。

解决:检查 main_pages.jsonsrc 数组是否包含 "pages/AboutView"


十二、Git 提交

12.1 提交命令

git add .
git commit -m "feat(setting): 设置中心开发

- 新增 SettingRepository 设置仓库(单例模式)
- 新增 SettingView 设置页面(深色模式/导出/清空/关于)
- 集成 PreferenceUtil 完成偏好持久化
- 封装 ConfirmDialog 确认对话框处理危险操作
- 注册 SettingView 与 AboutView 路由"

12.2 变更记录

## [v0.2.4] - 2026-07-28
### Added
- repository/SettingRepository.ets
- pages/SettingView.ets
- pages/AboutView.ets
- components/dialog/ConfirmDialog.ets
### Changed
- main_pages.json 新增 SettingView、AboutView 路由

附录:运行效果截图

在这里插入图片描述


总结

本文完整介绍了 SettingView 设置中心 的开发过程,涵盖 SettingRepository 设置仓库、PreferenceUtil 偏好存储工具、SettingView 页面实现、ConfirmDialog 确认对话框、RouterUtil 路由跳转与主题常量体系。通过本篇你可以:

  • 设计基于单例模式的设置仓库与偏好存储工具
  • 实现深色模式切换并持久化到本地偏好
  • 封装确认对话框处理高危清空操作
  • 集成路由工具完成页面跳转
  • 遵循 ArkTS 类型安全规范,使用 interface 代替 any 类型

下一篇预告:第 25 篇将深入讲解数据导出、备份与恢复的完整方案,基于 SettingView 中的 exportData 方法扩展为支持 JSON 导出、本地备份与数据恢复的全链路设计。


如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!欢迎在评论区留言讨论,或参与下方投票,告诉我你最想了解 HarmonyOS 开发的哪个方向?


相关资源

Logo

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

更多推荐