【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
    );
  }
}

这里有两个关键点:

  1. Preferences 初始化依赖 UIAbilityContext,因此最适合在 Ability 创建窗口时完成。
  2. 页面只通过 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 结束一次性流程,可以获得稳定且没有闪屏的引导体验。✅

img

img

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐