


在健康类应用中,用户数据的敏感性远高于普通工具类 App。作为一款专注于个人健康管理的「小V健身助手」,我们必须在产品设计之初就将用户隐私保护置于核心位置。根据《个人信息保护法》及主流应用市场的审核要求,任何涉及用户数据采集的应用都必须在首次启动时明确展示隐私政策,并获得用户的主动同意。
本文将基于 HarmonyOS 的 ArkTS 语言与 Stage 模型,通过实际代码详解如何在应用启动页实现一个合规、轻量且用户体验友好的隐私协议授权流程。整个方案包含三个关键环节:
我们使用以下 HarmonyOS 能力完成该功能:
@ohos.data.preferences:轻量级键值对存储,用于保存用户授权状态;CustomDialogController + @CustomDialog:构建自定义弹窗;router.replaceUrl:页面路由控制;UIAbilityContext:获取 Ability 上下文,用于调用系统 API。所有逻辑集中在两个文件中:
Index.ets:应用入口页面,负责判断授权状态并控制流程;UserPrivacyDialog.ets:自定义隐私协议弹窗组件。首先,定义两个常量,用于标识首选项文件名和存储键:
const H_STORE: string = 'V_health'
const IS_PRIVACY: string = 'isPrivacy'这里使用 V_health 作为首选项文件名,便于后续扩展其他健康相关配置;isPrivacy 则专门记录用户是否已同意隐私协议。
在 Index 页面中,通过 getContext(this) 获取当前 Ability 的上下文:
contest: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;该上下文是调用 data_preferences.getPreferences() 所必需的。
我们使用 @CustomDialog 装饰器创建 UserPrivacyDialog 组件:
@CustomDialog
export default struct UserPrivacyDialog {
cancel: Function = () => {}
confirm: Function = () => {}
build() {
Column({ space: 10 }) {
Text('欢迎使用小V健身')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Button('同意')
.fontColor(Color.White)
.backgroundColor('#ff06ae27')
.width(150)
.onClick(() => {
this.confirm()
this.controller.close()
})
Button('不同意')
.fontColor(Color.Gray)
.backgroundColor('#c8fcd0')
.width(150)
.onClick(() => {
this.cancel()
this.controller.close()
})
}
.width('80%')
.height('75%')
.justifyContent(FlexAlign.Center)
}
}confirm 和 cancel 回调将用户操作传递回父组件;controller.close() 确保点击后关闭弹窗。💡 注意:
controller实例由父组件传入,子组件无需重新创建。
aboutToAppear页面加载时,通过 aboutToAppear 生命周期钩子判断用户是否已授权:
aboutToAppear(): void {
let preferences = data_preferences.getPreferences(this.contest, H_STORE)
preferences.then((res) => {
res.get(IS_PRIVACY, false).then((isPrivate) => {
if (isPrivate === true) {
this.jumpToMain()
} else {
this.dialogController.open()
}
})
})
}false,确保首次安装时弹窗必现;isPrivate === true),则跳转主界面;当用户点击“同意”按钮,触发 onConfirm 方法:
onConfirm() {
let preferences = data_preferences.getPreferences(this.contest, H_STORE)
preferences.then((res) => {
res.put(IS_PRIVACY, true).then(() => {
res.flush(); // 强制写入磁盘
console.log('Index', 'isPrivacy记录成功');
}).catch((err: Error) => {
console.log('Index', 'isPrivacy记录失败,原因' + err);
})
})
}put 写入 true 值;flush() 确保数据立即落盘,避免因应用意外退出导致状态丢失;若用户拒绝授权,我们选择立即终止当前 Ability,符合隐私合规的最佳实践:
exitAPP() {
this.contest.terminateSelf()
}terminateSelf() 会关闭当前应用进程;授权成功后,通过 jumpToMain 跳转至首页:
jumpToMain() {
setTimeout(() => {
router.replaceUrl({ url: '' })
}, 2000)
}replaceUrl 替换当前页面,防止用户通过返回键回到启动页;url: '' 表示跳转到主页面(需在 main_pages.json 中配置为默认路由);setTimeout 实现即时跳转。⚠️ 提示:启动页背景图通过
.backgroundImage($r('app.media.backgroundBegin'))设置,提升首次启动的视觉体验。
本方案在满足法律合规的同时,兼顾了用户体验:
场景 | 行为 | 合规性 |
|---|---|---|
首次安装启动 | 弹出隐私协议弹窗 | ✅ 明示告知 + 主动同意 |
用户同意 | 记录状态,跳转主界面 | ✅ 授权后才启用功能 |
用户拒绝 | 立即退出,不收集任何数据 | ✅ 尊重用户选择 |
已授权用户再次启动 | 直接进入主界面 | ✅ 无重复打扰 |
这里的应用logo和昵称可以自己改一下

这里声明部分没有怎么做,就先占个位 首次进入应用才会提示

点击同意的时候就会消失,除非再次安装才显示

通过不到 100 行核心代码,我们为「小V健身助手」构建了一个轻量、可靠、合规的隐私授权机制。这不仅是法律的要求,更是赢得用户长期信任的第一步。

UserPrivacyDialog
@CustomDialog
export default struct UserPrivacyDialog{
controller: CustomDialogController = new CustomDialogController({
builder:''
})
cancel:Function = () =>{} // 不同意
confirm:Function = () =>{} // 同意
build() {
Column({space:10}){
Text('欢迎使用小V健身')
Button('同意')
.fontColor(Color.White)
.backgroundColor('#ff06ae27')
.width(150)
.onClick(()=>{
this.confirm()
this.controller.close()
})
Button('不同意')
.fontColor(Color.Gray)
.backgroundColor('#c8fcd0')
.width(150)
.onClick(()=>{
this.cancel()
this.controller.close()
})
}
.width('80%')
.height('75%')
}
}Index
import UserPrivacyDialog from '../dialog/UserPrivacyDialog'
import { common } from '@kit.AbilityKit'
import data_preferences from '@ohos.data.preferences'
import { router } from '@kit.ArkUI'
// 定义常量存储首选项中的键
const H_STORE:string = 'V_health'
const IS_PRIVACY:string = 'isPrivacy'
@Entry
@Component
struct Index {
// 生命周期
contest: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
dialogController: CustomDialogController = new CustomDialogController({
builder:UserPrivacyDialog({
cancel:()=>{this.exitAPP()},
confirm:()=>{this.onConfirm()}
})
})
// 点击同意后的逻辑
onConfirm(){
// 定义首选项
let preferences = data_preferences.getPreferences(this.contest,H_STORE)
// 异步处理首选项中的数据
preferences.then((res)=>{
res.put(IS_PRIVACY,true).then(()=>{
res.flush();
// 记录日志
console.log('Index','isPrivacy记录成功');
}).catch((err:Error)=>{
console.log('Index','isPrivacy记录失败,原因'+err);
})
})
}
// 点击不同意时的逻辑
exitAPP(){
this.contest.terminateSelf()
}
// 页面加载开始执行逻辑
aboutToAppear(): void {
let preferences = data_preferences.getPreferences(this.contest,H_STORE)
preferences.then((res)=>{
res.get(IS_PRIVACY,false).then((isPrivate)=>{
// 判断传入的参数
if(isPrivate==true){
// 点击同意跳转到首页
this.jumpToMain()
}
else{
this.dialogController.open()
}
})
})
}
// 页面结束时的执行逻辑
aboutToDisappear(): void {
clearTimeout()
}
// 跳转到首页
jumpToMain(){
setTimeout(()=>{
router.replaceUrl({url:''})
},2000)
}
build() {
Column(){
}
.width('100%')
.height('100%')
.backgroundImage($r('app.media.backgroundBegin'))
.backgroundImageSize({width:'100%',height:'100%'})
}
}