【OpenHarmony/HarmonyOs 】首次启动如何分流?用 UIAbility 与 Preferences 实现身份引导
【OpenHarmony/HarmonyOs 】首次启动如何分流?用 UIAbility 与 Preferences 实现身份引导
前言
很多应用都存在“只在第一次出现”的页面,例如隐私说明、兴趣选择、登录引导和功能介绍。最常见的错误是:每次启动都先打开首页,然后在页面里异步查询状态并再次跳转。这会造成界面闪烁、路由栈混乱,甚至出现用户短暂看到不该出现的内容。
本文以 LinkOS 链界为例,实现一条清晰的首次启动链路:初始化本地存储 → 查询身份标记 → 直接决定首屏 → 用户选择后持久化 → 以后直达首页。🧭
一、为什么使用 Preferences
身份 ID、语言、视图模式、访问次数都属于轻量键值数据,数据量小、结构简单,并且需要跨启动保存。ArkData 提供的 Preferences 正适合这类场景。
它适合保存:
user_role_id:当前身份;locale:语言偏好;home_view_mode:宫格或列表;site_visit_count:累计访问次数;- 少量 JSON 字符串,例如快捷入口 ID 集合。
它不适合保存海量记录、复杂关联查询和大文件。随着收藏规模扩大,应考虑 RDB 或 Cloud DB。
二、封装可复用的 StorageUtil
项目将 Preferences 包装成单例,保证全局使用同一个实例:
export class StorageUtil {
private static readonly PREF_NAME = 'linkos_prefs';
private static instance: StorageUtil;
private pref: preferences.Preferences | null = null;
private constructor() {}
public static getInstance(): StorageUtil {
if (!StorageUtil.instance) {
StorageUtil.instance = new StorageUtil();
}
return StorageUtil.instance;
}
async init(context: common.UIAbilityContext): Promise<void> {
this.pref = await preferences.getPreferences(
context,
StorageUtil.PREF_NAME
);
}
}
这里有两个关键点:
- Preferences 初始化依赖
UIAbilityContext,因此最适合在 Ability 创建窗口时完成。 - 页面只通过
getInstance()获取服务,不需要保存 Context,降低生命周期泄漏风险。
三、统一读写并及时 flush
async put(key: string, value: preferences.ValueType): Promise<void> {
if (!this.pref) return;
try {
await this.pref.put(key, value);
await this.pref.flush();
} catch (err) {
console.error(`[StorageUtil] Failed to put ${key}:`, JSON.stringify(err));
}
}
async get(key: string,
defaultValue: preferences.ValueType): Promise<preferences.ValueType> {
if (!this.pref) return defaultValue;
try {
return await this.pref.get(key, defaultValue);
} catch {
return defaultValue;
}
}
put() 修改的是内存中的 Preferences 数据,flush() 才负责持久化到磁盘。对于身份选择这种关键状态,立即 flush 能保证用户刚选择完就退出应用时,数据仍然可靠保存。
建议将 Key 集中定义,避免页面中出现大量魔法字符串:
export class StorageKeys {
static readonly USER_ROLE_ID = 'user_role_id';
static readonly USER_INTERESTS = 'user_interests';
static readonly USAGE_TIME_TODAY = 'usage_time_today';
static readonly SITE_VISIT_COUNT = 'site_visit_count';
static readonly CUSTOM_SITES = 'custom_sites';
static readonly LOCALE = 'locale';
}
四、在 UIAbility 中完成首屏判断
async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
const storage = StorageUtil.getInstance();
await storage.init(this.context);
const hasRole = await storage.has(StorageKeys.USER_ROLE_ID);
const entryPage = hasRole
? 'pages/v2/HomePage'
: 'pages/v2/WelcomePage';
windowStage.loadContent(entryPage, (err) => {
if (err.code) {
hilog.error(DOMAIN, 'LinkOS', 'Failed: %{public}s', JSON.stringify(err));
}
});
}
由于判断发生在 loadContent() 之前,用户看到的第一帧就是正确页面。这种做法也便于未来增加更多状态:
是否同意隐私协议?
否 → PrivacyPage
是 → 是否选择身份?
否 → WelcomePage
是 → HomePage
五、欢迎页保存状态并替换路由
欢迎页使用 @State 保存当前选项,点击角色后立即落盘:
.onClick(async () => {
this.selectedRoleId = role.id;
const storage = StorageUtil.getInstance();
await storage.put(StorageKeys.USER_ROLE_ID, role.id);
router.replaceUrl({ url: 'pages/v2/HomePage' });
})
这里选择 replaceUrl 而不是 pushUrl。身份引导是一次性流程,进入首页后按返回键不应该重新回到欢迎页。替换当前路由正好符合这一交互语义。
六、支持重新选择与清除数据
“首次启动”并不意味着用户永远不能改。在“我的”页面中,可以清空身份并返回引导页:
await storage.put(StorageKeys.USER_ROLE_ID, '');
router.replaceUrl({ url: 'pages/v2/WelcomePage' });
不过这里还隐藏着一个边界:启动代码使用 has(key) 判断,而空字符串仍表示 Key 存在。更严谨的做法是读取值并判断非空:
const roleId = await storage.get(StorageKeys.USER_ROLE_ID, '') as string;
const entryPage = roleId.trim()
? 'pages/v2/HomePage'
: 'pages/v2/WelcomePage';
或者调用 delete(USER_ROLE_ID),从数据语义上表达“身份不存在”。这也是实际开发中值得注意的细节。⚠️
七、异常与体验优化
- 初始化失败时应记录日志,并采用安全默认值进入欢迎页。
- 快速连续点击角色时可设置提交中状态,避免重复路由。
- 首次选择后可同时写入默认网址,保证首页立即有内容。
- 清除全部数据前应使用确认对话框,说明影响范围。
- 若接入云账号,应定义本地身份与云端身份冲突时的优先级。
八、总结
首次启动分流的核心并不是一个布尔值,而是状态初始化、启动时序和路由语义。把 Preferences 初始化放到 Ability,将首屏决策放到 loadContent() 之前,并用 replaceUrl 结束一次性流程,可以获得稳定且没有闪屏的引导体验。✅


更多推荐


所有评论(0)