【时光清单|12】HarmonyOS ArkTS 单页复杂状态实战:拆分 Index 中的导航、弹层和编辑草稿

HarmonyOS 应用的入口页很容易变成“总控页面”:既维护 NavPathStack,又切换底部 Tab,还保存编辑表单、控制弹层、处理安全区、读取业务数据。功能刚开始时写在一个 Index.ets 里很直接,几轮迭代后却会出现状态互相影响:返回二级页时表单被重建,关闭弹层误清空草稿,切换 Tab 触发重复加载,主题变化又把业务组件全部刷新。

时光清单 的当前真实源码恰好展示了拆分后的结果:Index.ets 保持精简,只持有全局 NavPathStack 和主题背景,构建根 Navigation;底部 Tab 的选择与懒加载由 MainTabShell.ets 管理;二级页面映射集中在 RouteMap.ets;新建事项的类型、标题、日期、备注和成功状态全部留在 AddView.ets。当前 Index 中没有业务弹层,也没有编辑草稿字段,这是已经完成职责下沉的证据。

本文基于当前工程中的这些文件以及 DetailView.etsAppViewModel.ets 复核状态所有权,不把未核验的平台版本、构建结果或历史重构过程写成事实。重点不是假设源码里仍有巨型入口页,而是从当前结构倒推:导航、条件面板和编辑草稿为什么不能由同一组件统管;如果未来增加 Sheet、Dialog、跨 Tab 草稿和深链恢复,状态该落在哪一层。

单页复杂状态拆分主题封面

本文将完成这些工程分析:

  1. 还原 Index -> MainTabShell -> RouteMap 的导航链。
  2. 区分 @State@StorageLink@StorageProp 的真实用途。
  3. 解释 Tab 懒加载为什么用 visitedTabs
  4. 说明编辑草稿为何留在 AddView,而不是提升到入口页。
  5. 给出未来弹层状态的归属判断和状态机设计。
  6. 分析返回恢复、保存成功、数据刷新与多设备窗口适配。

本文唯一标记:CSDN-SERIES:ALL-163208359

一、先看真实 Index:入口页只负责导航宿主

当前 Index.ets

@Entry
@Component
struct Index {
  @StorageLink(StateKeys.NAV_STACK)
  pathStack: NavPathStack = new NavPathStack();

  @StorageLink(StateKeys.THEME_BG)
  themeBg: string = '#F5F0E8';

  build() {
    Navigation(this.pathStack) {
      MainTabShell()
    }
    .navDestination(appRouter)
    .hideTitleBar(true)
    .hideToolBar(true)
    .mode(NavigationMode.Stack)
    .backgroundColor(this.themeBg);
  }
}

它只拥有两类入口级状态:

状态 原因 使用范围
NAV_STACK 所有页面需要共享同一导航栈 全应用路由
THEME_BG 根容器背景需跟随主题 根 Navigation

Index 不读取纪念日仓库,不保存新建表单,也不判断当前 Tab。入口组件只承担“容器与集成”职责,业务变化就不会频繁触碰应用根节点。

这也说明标题中的“拆分 Index 中的导航、弹层和编辑草稿”应理解为架构目标与结果:当前源码已经把 Tab、路由和草稿拆走,不能再把不存在的弹层字段写成现状。

历史证据:当前拆分可见,旧 Index 形态不可虚构

当前源码能够证明的是“现在怎样分工”:Index 是根导航宿主,MainTabShell 管理主 Tab,RouteMap 解析二级目的地,创建草稿和详情编辑状态分别留在业务页面。它不能反向证明旧版本一定存在一个同时保存导航、弹层与草稿的巨型 Index。本轮对相关文件执行了聚焦的 Git 历史检查,但没有找到足以还原那段重构过程的日期、提交和旧代码,因此本文不会把常见的演进路径包装成该项目已经发生过的历史。

可核对的历史记录来自 PROJECT_ERRORS.md。2026 年 5 月 20 日记录过编辑纪念日后列表没有立即刷新,需要切页才看到新值;当时的处理是在列表页观察 DATA_VERSION,在详情保存、删除和置顶后递增版本,并把 updatedAt 加入列表渲染键。记录还写明当时 assembleHap 通过,但那只是那次修复的历史证据,不能替代本文精修时的当前构建结果。

这条记录支持一个有限结论:跨页数据新鲜度不应依赖 Index、Tab 切换或单一生命周期回调,而应由明确的失效信号和仓库读取协作完成。至于旧入口页是否曾拥有业务弹层、又如何逐步拆出这些文件,目前没有可靠材料,后文凡是状态机、脏草稿确认与持久化策略,都会明确作为建议实现讨论。

二、三类状态先按生命周期分类

页面复杂并不等于所有状态都要全局化。可以先按生命周期分类:

应用级
  NavPathStack、主题、安全区、数据版本

页面壳级
  当前 Tab、哪些 Tab 已访问

业务页面级
  表单标题、日期、备注、保存成功态

瞬时覆盖层
  Sheet 是否显示、Dialog 类型、待确认操作

状态提升的判断不是“多个组件可能用到”,而是“多个拥有不同生命周期的组件必须共同修改”。NavPathStack 跨页面共享,适合 AppStoragecurrentIndex 只影响底部壳,使用本地 @Statetitle 只属于新建页面,继续留在 AddView

页面命令到草稿校验、保存和跨页刷新的状态流

把状态放得过高,会扩大刷新范围和耦合;放得过低,则可能在组件重建时丢失。正确位置是“能完整覆盖它所需生命周期的最低共同拥有者”。

三、MainTabShell:Tab 状态不进入 Index

MainTabShell 自己维护:

@State currentIndex: number = 0;
@State visitedTabs: boolean[] =
  [true, false, false, false, false];

currentIndex 决定当前 Tab 与选中样式,visitedTabs 决定某个页面是否已经创建。它们不参与二级路由,也不需要被详情页修改,因此没必要提升到 IndexAppStorage

.onChange((index: number) => {
  this.currentIndex = index;
  if (!this.visitedTabs[index]) {
    this.visitedTabs[index] = true;
    this.visitedTabs = [...this.visitedTabs];
  }
})

这里重新赋值数组,向 ArkUI 发出明确状态变化。首次进入 Tab 时创建页面,离开后仍保留已访问标记。这样既避免启动时一次性构建五个业务页面,也减少反复切换造成的初始化抖动。

四、visitedTabs 的真实收益与边界

未访问的 Tab 使用条件构建:

TabContent() {
  if (this.visitedTabs[2]) {
    AddView();
  }
}
.tabBar(this.tabBar(2));

这对包含仓库读取、复杂列表或大量资源的页面很有意义。首页默认创建,其余页面按需创建。

策略 首屏成本 切换恢复 内存占用
五页全部立即创建
每次切换重新创建
首次访问后保留

当前实现采用第三种。它同时影响编辑草稿:用户进入“新建”后输入内容,切去首页再回来,AddView 通常不会因为 visitedTabs 变回 false 而被主动销毁,因此本地 @State 可以保留。

不过,这种恢复依赖 Tabs 组件的生命周期行为和页面没有被系统回收。需要跨进程或任务恢复的草稿,仍应有专门持久化策略,不能把内存保留当作可靠保存。

五、RouteMap:二级页面映射不塞进入口 build

根 Navigation 通过:

.navDestination(appRouter)

绑定统一 Builder。RouteMap.ets 根据路由名创建目的页:

@Builder
export function appRouter(name: string, param: Object) {
  NavDestination() {
    if (name === RouteNames.DETAIL) {
      DetailView({ itemParam: param as string });
    } else if (name === RouteNames.COUPLE) {
      CoupleView();
    } else if (name === RouteNames.DIARY) {
      DiaryView();
    }
  }
  .hideTitleBar(true);
}

如果这些分支都写进 Index.build(),入口页会随着每个业务页面增长。现在 Index 只知道“路由由 appRouter 解析”,不依赖详情页的参数字段和业务实现。

RouteNames 又把字符串集中为常量,减少页面散落 'detail''couple' 等魔法值。

多个业务页面通过同一个键获取导航栈:

@StorageLink(StateKeys.NAV_STACK)
pathStack: NavPathStack =
  new NavPathStack();

首页可以:

this.pathStack.pushPath({
  name: RouteNames.DETAIL,
  param: JSON.stringify(item)
});

二级页返回时:

this.pathStack.pop();

@StorageLink 是双向链接:页面对栈的修改会影响共享状态。相比逐层传递回调,它适合导航基础设施。主题背景也需要全局响应变化,所以使用 StorageLink;安全区在 MainTabShell 中只读取,使用 StorageProp 更贴近单向消费。

AppStore、Index、MainTabShell、RouteMap 与业务页面的状态所有权结构

共享并不意味着任意业务数据都应放进 AppStorage。导航栈是跨页面基础设施,编辑标题不是。

七、编辑草稿属于 AddView,而不是 Index

AddView 真实草稿状态:

@State selectedType: AnniversaryType = 'countdown';
@State title: string = '';
@State targetDate: string = '';
@State remark: string = '';
@State showSuccess: boolean = false;

这些字段只参与新建事项:

  • selectedType 控制模板选择。
  • titletargetDateremark 绑定输入控件。
  • showSuccess 控制保存反馈。

如果把它们提升到 Index,入口组件就必须理解纪念日类型、日期格式和保存结果;主题切换、导航栈变化也可能与表单状态交织。当前源码让 AddView 自己拥有草稿,AppViewModel 只接收完整 AnniversarySaveData,边界更清晰。

八、表单草稿不是业务实体

用户输入中的 targetDate 是字符串:

@State targetDate: string = '';

保存时才转换:

const dateMs = this.targetDate
  ? new Date(this.targetDate).getTime()
  : Date.now() + 86400000 * 30;

let data: AnniversarySaveData = {
  type: this.selectedType,
  title: this.title.trim(),
  targetDate: dateMs,
  remark:
    remarkText.length > 0
      ? remarkText : undefined,
};

这说明草稿模型和持久化实体不同。输入控件需要保留用户正在编辑的文本,包括暂时不完整的日期;仓库只应接收验证后的毫秒时间。

可以显式定义草稿:

interface AnniversaryDraft {
  type: AnniversaryType;
  titleText: string;
  targetDateText: string;
  remarkText: string;
}

草稿允许“不完整但可继续编辑”,业务实体要求“完整且可保存”。不要让页面直接构造半成品 Anniversary

九、日期校验是当前表单的薄弱点

当前代码只判断 targetDate 是否为空,没有检查:

new Date(this.targetDate).getTime()

是否得到 NaN。错误日期会进入 AnniversarySaveData,后续倒计时计算就可能显示异常。

可加入显式解析结果:

interface DateParseResult {
  valid: boolean;
  value?: number;
  message?: string;
}

function parseTargetDate(
  value: string
): DateParseResult {
  const text = value.trim();
  if (text.length === 0) {
    return {
      valid: false,
      message: '请选择目标日期'
    };
  }

  const timestamp =
    new Date(text).getTime();
  if (!Number.isFinite(timestamp)) {
    return {
      valid: false,
      message: '日期格式无效'
    };
  }
  return { valid: true, value: timestamp };
}

解析函数属于页面表单或专门校验器,不属于 Index。入口页不应知道日期格式。

十、保存流程:业务动作结束后再清空草稿

真实 handleSave() 先保存仓库,再处理默认桌面组件项,随后递增数据版本并清空表单:

const saved =
  await this.viewModel
    .saveAnniversary(data);

const selectedWidgetId =
  await this.store.getJson<string>(
    DataKeys.WIDGET_ANNIVERSARY_ID,
    ''
  );

if (selectedWidgetId.length === 0) {
  await this.store.putJson(
    DataKeys.WIDGET_ANNIVERSARY_ID,
    saved.id
  );
}

const ver =
  AppStorage.get<number>(
    StateKeys.DATA_VERSION
  ) ?? 0;
AppStorage.set<number>(
  StateKeys.DATA_VERSION,
  ver + 1
);

成功后才执行:

this.showSuccess = true;
this.title = '';
this.targetDate = '';
this.remark = '';

这个顺序很重要。若先清空再保存,写入失败时用户输入已丢失。当前仓库是否向上抛错还要结合实现,但页面层的正确原则是:只有收到明确成功结果,才清空草稿。

十一、DATA_VERSION 是刷新信号,不是业务数据

保存后递增 StateKeys.DATA_VERSION,首页通过 @Watch 重新加载:

@StorageLink(StateKeys.DATA_VERSION)
@Watch('onDataChange')
dataVersion: number = 0;

onDataChange(): void {
  this.loadData();
}

这是一个轻量失效信号:它不保存事项数量,也不代表数据库版本。任何业务页都不应从这个数字推导实体内容。

当写操作增多时,可考虑事件总线或仓库订阅,但当前版本号方案简单直观。要避免的问题包括:

  • 保存失败仍递增版本。
  • 多次连续写入触发重复全量加载。
  • 业务页面把版本号当成计数器展示。
  • 页面卸载后监听没有按生命周期释放。

信号只负责“数据可能变化,请重新读取真源”。

当前源码边界:条件面板不是完整状态机

谈“弹层拆分”前,必须先说明当前界面的真实形态。AddViewshowSuccess 控制的是页面内条件渲染的成功反馈,DetailViewshowShareCard 控制的也是 Scroll 内容中的分享卡片。检查到的代码没有用 DialogCustomDialog 承载这两块内容,所以不能仅凭视觉上出现了一块覆盖区域,就把它们描述成系统弹窗或自定义对话框。

详情页还有另一组本地状态:isEditingeditTitleeditTargetDateeditRemark。进入编辑时,页面把当前事项的标题、格式化日期和备注复制到编辑字段;取消时只把 isEditing 设为 false;再次进入时再从当前事项复制。保存路径会校验标题和日期,调用 AppViewModel.saveAnniversary,更新本地事项,退出编辑,递增 DATA_VERSION 并显示提示。这些都是当前源码可以逐项对应的事实。

边界也很清楚。isEditingshowShareCard 是彼此独立的布尔值,当前字段本身并不保证“编辑”和“分享预览”互斥;取消编辑没有脏草稿比较,也没有返回确认。AddView 同样没有原始快照、isDirty、离开 Tab 确认和持久化恢复;它只校验标题非空,非空但不可解析的日期也没有显式 isNaNNumber.isFinite 分支。这里列出的不是已经发生的故障统计,而是从状态组合与校验分支中直接识别出的待验证风险。

如果产品要求分享预览、编辑模式和删除确认互斥,可以把详情页状态收敛为一个视图模式;如果产品允许分享卡片在编辑区旁同时存在,则保留独立状态也可以,但要把这种并存写成明确交互规则。状态机不是越多越好,它只应该用来消除产品不允许出现的组合。

十二、未来弹层状态应该放在哪里

当前 Index 没有 Sheet、Dialog 或业务遮罩。未来新增时先按触发范围判断:

弹层 所有者 理由
新建页日期选择器 AddView 只服务表单
详情页删除确认 DetailView 只服务当前实体
全局隐私确认 应用入口或专门协调器 跨页面阻断
Tab 级快捷菜单 MainTabShell 与底部壳绑定
保存失败提示 发起保存的页面 拥有重试上下文

不要因为 Sheet 在视觉上覆盖整个屏幕,就把它的状态放到 Index。视觉层级与状态所有权不是同一件事。

如果一个页面有多个互斥弹层,多个布尔值会产生非法组合:

@State showDatePicker = false;
@State showDeleteDialog = false;
@State showSuccessSheet = false;

更适合使用枚举状态:

type OverlayState =
  | 'none'
  | 'datePicker'
  | 'deleteConfirm'
  | 'saveSuccess';

@State overlay:
  OverlayState = 'none';

这样一次只能处于一个覆盖层状态,关闭时统一回到 none

建议把转换动作也收窄为 beginEditcancelEditsaveEditopenSharePreviewcloseSharePreview。每个命令只负责一次可命名的状态变化,并在入口统一处理不兼容状态。例如 beginEdit 可以先关闭分享预览,再从当前实体建立新草稿;cancelEdit 可以比较原始快照与当前草稿,只有确实修改过时才进入放弃确认。这里是建议实现,不代表当前文件已经存在这些方法。

十三、草稿跨 Tab 保留与跨重启恢复是两种需求

当前 visitedTabs 可以支持内存中的 Tab 切换恢复,但应用进程退出后 @State 不保证保留。产品需要先定义草稿承诺:

承诺 存储方式
仅当前页面内 @State
切换 Tab 后保留 Tab 页面保持或 ViewModel
导航离开后恢复 页面级草稿仓库
应用重启后恢复 Preferences 草稿
多设备继续编辑 带版本的同步草稿

对时光清单的新建表单,持久化草稿可以保存类型、原始文本和更新时间:

interface SavedDraft {
  schemaVersion: number;
  type: AnniversaryType;
  titleText: string;
  targetDateText: string;
  remarkText: string;
  updatedAt: number;
}

草稿与已保存事项必须使用不同键。用户点击保存成功后删除草稿;用户主动“放弃编辑”也应给出清晰确认。

十四、AppViewModel 保持业务入口,不保存 UI 草稿

AppViewModel 当前持有仓库引用并暴露意图方法:

async saveAnniversary(
  data: AnniversarySaveData
): Promise<Anniversary> {
  return this.anniversaryRepo.save(data);
}

它没有 showDialogcurrentTabtitleText 等 UI 字段,这是合理的。ViewModel 负责协调业务动作和仓库,页面负责输入草稿与控件状态。

若表单校验、自动保存和错误恢复变复杂,可以创建专门的 AnniversaryEditorViewModel,但不要把所有页面能力继续塞进已有 AppViewModel。当前 AppViewModel 同时协调纪念日、语录和主题,已经接近应用级门面;继续增长时应按功能拆分。

十五、返回与恢复:Navigation 栈不应携带整个可变页面

RouteMap 的详情参数使用 JSON 字符串:

DetailView({
  itemParam: param as string
});

首页 push 时序列化当前事项。这个参数适合页面打开时定位或展示,但如果详情页长时间停留,底层实体可能已变化。更稳的导航契约是传稳定 ID,由目的页从仓库读取最新数据。

interface DetailRouteParam {
  id: string;
}

导航栈应保存“去哪里”和“定位哪个实体”,不应成为业务对象缓存。返回主 Tab 后,DATA_VERSION 或页面生命周期再触发真源刷新。

十六、多窗口与安全区:壳层负责容器适配

MainTabShell 读取底部安全区:

@StorageProp(StateKeys.SAFE_AREA_BOTTOM)
private safeBottom: number = 0;

底栏高度:

.barHeight(
  56 + px2vp(this.safeBottom)
)

根壳处理系统导航区域,业务表单只需要在自身内容底部留出适当空间。若每个页面都重复读取并计算底部避让,容易出现双重 padding 或遗漏。

在手机横屏、平板和 2in1 窗口中,还需要验证:

  • 五个 Tab 标签不截断。
  • 新建表单可滚动,键盘弹出后保存按钮可达。
  • 二级页底部 padding 不与系统导航区重叠。
  • Navigation 返回手势和页面返回按钮一致。
  • 深浅色切换时根背景、Tab 背景和页面背景连贯。

十七、把大页面拆开后仍需防止“共享状态回流”

文件拆分只是第一步。如果所有页面仍通过大量 AppStorage 键互相修改,逻辑耦合仍然存在。

建议把共享键限定为:

  • 导航栈。
  • 主题和系统安全区。
  • 必要的数据失效信号。
  • 确实跨页面的用户设置。

不应全局化:

  • 输入框实时文本。
  • 某个页面的加载中状态。
  • 局部 Dialog 是否显示。
  • 当前列表临时筛选。
  • 保存按钮禁用状态。

每增加一个 StateKeys,都应回答:哪些页面读,哪些页面写,生命周期多长,是否有持久化含义,默认值由谁初始化。

分阶段改造:先定义所有权,再定义转换

如果要继续完善这个页面族,第一阶段只画所有权表,不急着新增全局键。把 NAV_STACK、主题、安全区和 DATA_VERSION 保持在应用层;把 currentIndexvisitedTabs 留给 Tab 壳;把创建草稿交给 AddView,把详情编辑和分享状态交给 DetailView。此时验收点是任意一个字段都能说清谁创建、谁修改、何时销毁,而不是文件数量是否增加。

第二阶段再引入类型化草稿。草稿保存原始文本,进入编辑时同时记录原始快照,通过字段比较得到 isDirty。保存命令先解析和校验,再构造仓库需要的业务数据;失败时保留草稿和错误信息,成功后才更新实体、清理草稿并发送失效信号。这样可以把“输入尚未完成”和“业务实体无效”区分开,也能让取消、返回和切 Tab 的策略有统一依据。

第三阶段只在互斥关系真实存在时引入枚举或联合类型。例如详情页可以使用 view | edit | share | deleteConfirm,每次转换都关闭旧模式;创建页的成功反馈若只是普通行内提示,则不必为了形式统一强行塞入弹层状态机。最后再依据产品承诺决定是否持久化:只保证当前页面内保留可继续使用 @State,要求进程重启恢复才考虑 Preferences,并为草稿设置独立键、结构版本、更新时间和清理时机。

这一顺序能避免两种常见反弹:一是为了防止草稿丢失,把所有输入都提升到 AppStorage;二是为了“状态机化”,把彼此可以并存的界面状态硬绑成单一枚举。改造结果应由返回、失败、重复进入和恢复场景验证,而不是由抽象层数证明。

十八、测试矩阵:状态拆分要验证“回来以后”

当前未验证项

下面的矩阵是后续验证方案,不是本轮已经跑出的测试报告。本轮没有执行当前源码构建,没有在模拟器或真机验证 Tab 切换、系统返回、键盘遮挡、进程重建,也没有通过仓库故障注入证明保存失败时的页面表现。历史记录中的一次 assembleHap 成功只对应 2026 年 5 月 20 日的数据刷新修复,不能挪作当前版本的构建证明。

因此表中的“期望”表示实现或精修后的验收条件。执行时应记录环境、操作步骤、可见结果和失败日志;任何一项未运行都应保持“未验证”,不能因为源码路径看起来合理就写成已经通过。

场景 操作 期望
首次启动 进入首页 只创建必要 Tab
首次进入新建 切到 Tab 2 AddView 创建
草稿切换 输入后切首页再返回 内存草稿按设计保留
保存成功 输入合法数据 清空草稿并显示成功
保存失败 模拟仓库失败 不清空输入
日期非法 输入错误日期 阻止保存并提示
二级导航 首页打开详情 栈 push 正确
返回 详情页 pop 回到原 Tab
数据刷新 新建成功后回首页 首页重新读取仓库
深链参数异常 非法 route param 目的页安全兜底
小窗键盘 表单聚焦 保存操作仍可达
系统返回 二级页返回 与可见返回按钮一致

测试时尤其关注“返回以后”的状态,而不只看首次截图。复杂状态问题常发生在第二次进入、快速切换和失败恢复。

十九、常见问题与定位

现象 常见根因 修复方向
切 Tab 草稿消失 页面每次重建 首次访问后保留或草稿仓库
保存失败仍清空 先清表单后写仓库 成功后清空
返回详情到了错误 Tab 导航栈和 Tab 状态混用 分离两层状态
任一状态变化全页刷新 草稿提升到入口 下沉到业务页面
多个弹层同时出现 多布尔值非法组合 使用枚举状态机
新建后首页没更新 未发送失效信号 成功后递增 DATA_VERSION
首页重复加载 多次信号无节流 合并刷新或订阅仓库
底部按钮被手势区遮挡 壳层未统一安全区 统一 bottom avoid area

定位时先画出“状态所有者、写入者、读取者、销毁时机”四列,比继续添加布尔变量有效。

二十、发布前核对清单

  • [ ] Index 只构建根 Navigation 和 MainTabShell。
  • [ ] 文中没有虚构当前不存在的 Index 弹层。
  • [ ] currentIndexvisitedTabs 归 MainTabShell。
  • [ ] 新建草稿归 AddView,不进入全局状态。
  • [ ] 路由名集中在 RouteNames,映射集中在 RouteMap
  • [ ] 二级页面能通过共享 NavPathStack 返回。
  • [ ] 保存成功后才清空草稿并发送刷新信号。
  • [ ] 非法日期不会进入仓库。
  • [ ] 弹层采用单一所有者或明确状态机。
  • [ ] 手机、小窗、平板和键盘场景操作可达。
  • [ ] 主题与安全区由壳层和共享状态协调。
  • [ ] AppViewModel 没有混入纯 UI 覆盖层状态。

二十一、总结:入口越薄,状态边界越容易验证

时光清单 当前源码已经给出一个清楚的拆分结果:Index 是根 Navigation 宿主;MainTabShell 拥有 Tab 选择和按需创建;RouteMap 集中二级页面目的地;AddView 持有类型、标题、日期、备注和保存反馈;AppViewModel 只协调仓库动作。真实 Index 中没有业务弹层和编辑草稿,这正是入口页从复杂状态中解脱出来的表现。

后续新增能力时,应继续遵循同一判断:导航基础设施放在应用级,Tab 状态放在壳层,编辑草稿放在业务页,弹层放在拥有业务上下文的最低组件;只有跨重启或跨设备恢复时,才把草稿提升到专门存储。这样既能减少 ArkUI 无关重建,也能让返回、失败、恢复和多窗口适配拥有可复核的状态路径。


AI 辅助声明: 本文由 AI 辅助整理,当前结构与状态结论均依据 Index.etsMainTabShell.etsRouteMap.etsRouteNames.etsAddView.etsStateKeys.etsAppViewModel.ets 的真实源码人工复核;草稿持久化、弹层状态机和路由参数改造为明确标注的演进建议。

Logo

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

更多推荐