HarmonyOS NEXT 企业级记账APP:设置中心开发
设置中心开发
本文是《HarmonyOS NEXT 企业级开发实战:30篇打造智能记账APP》系列的第 24 篇,对应 Git Tag v0.2.4。承接前序开发,本篇开发
SettingView设置中心,支持深色模式切换、数据导出、清空数据与关于页面跳转等能力,底层基于SettingRepository与PreferenceUtil完成偏好持久化。
前言
企业级应用的可配置能力与数据掌控能力决定产品成熟度。一个完善的设置中心,不仅要让用户自定义外观偏好(如深色模式),还要让用户能够导出和清理自己的数据。本文将带你:
- 设计
SettingRepository设置仓库与PreferenceUtil偏好存储工具 - 实现
SettingView设置页面的完整布局与交互 - 封装
ConfirmDialog确认对话框处理危险操作 - 集成
RouterUtil路由工具完成页面跳转 - 遵循 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,通过 PreferenceUtil 的 setString/getString 或 setBoolean/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.ArkData 的 preferences 模块封装的轻量存储工具。它需要在应用启动时调用 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,业务逻辑直接调用 SettingRepository 与 PreferenceUtil,因为设置页面的状态非常简单,无需引入额外的抽象层。
@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格式)');
}
导出方法的执行步骤如下:
- 调用
PreferenceUtil.getString异步读取bills和categories偏好数据 - 定义
ExportData接口约束导出数据结构 - 组装包含账单、分类和导出时间的
ExportData对象 - 使用
JSON.stringify序列化为格式化 JSON 字符串 - 通过
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 路由注册
SettingView 与 AboutView 都需要在 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.ArkUI 的 router 模块,提供 push、pushWithId、back、replace 四个静态方法,统一处理异常捕获。
// 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 定义接口,而非依赖 any。SettingView 的 exportData 方法中定义了 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 时始终返回默认值,数据无法持久化。
原因:PreferenceUtil 的 init 方法未在应用启动时调用,导致 this.pref 为 null。
解决:在 EntryAbility 的 onCreate 中调用初始化:
// 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.json 的 src 数组是否包含 "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 开发的哪个方向?
相关资源
- 本篇源码:GitHub Tag v0.2.4
- HarmonyOS NEXT 开发者文档:developer.harmonyos.com
- ArkUI List 组件:list 组件参考
- ArkUI Toggle 组件:toggle 组件参考
- 鸿蒙数据存储 preferences:data-preferences
- ArkUI 路由管理:router 导航
- ArkTS 类型安全规范:arkts 类型约束
- ArkUI @Prop 装饰器:prop 状态管理
- CSV 格式规范:RFC 4180
更多推荐



所有评论(0)