【时光清单|12】HarmonyOS ArkTS 单页复杂状态实战:拆分 Index 中的导航、弹层和编辑草稿
【时光清单|12】HarmonyOS ArkTS 单页复杂状态实战:拆分 Index 中的导航、弹层和编辑草稿
HarmonyOS 应用的入口页很容易变成“总控页面”:既维护 NavPathStack,又切换底部 Tab,还保存编辑表单、控制弹层、处理安全区、读取业务数据。功能刚开始时写在一个 Index.ets 里很直接,几轮迭代后却会出现状态互相影响:返回二级页时表单被重建,关闭弹层误清空草稿,切换 Tab 触发重复加载,主题变化又把业务组件全部刷新。
时光清单 的当前真实源码恰好展示了拆分后的结果:Index.ets 保持精简,只持有全局 NavPathStack 和主题背景,构建根 Navigation;底部 Tab 的选择与懒加载由 MainTabShell.ets 管理;二级页面映射集中在 RouteMap.ets;新建事项的类型、标题、日期、备注和成功状态全部留在 AddView.ets。当前 Index 中没有业务弹层,也没有编辑草稿字段,这是已经完成职责下沉的证据。
本文基于当前工程中的这些文件以及 DetailView.ets、AppViewModel.ets 复核状态所有权,不把未核验的平台版本、构建结果或历史重构过程写成事实。重点不是假设源码里仍有巨型入口页,而是从当前结构倒推:导航、条件面板和编辑草稿为什么不能由同一组件统管;如果未来增加 Sheet、Dialog、跨 Tab 草稿和深链恢复,状态该落在哪一层。

本文将完成这些工程分析:
- 还原
Index -> MainTabShell -> RouteMap的导航链。 - 区分
@State、@StorageLink和@StorageProp的真实用途。 - 解释 Tab 懒加载为什么用
visitedTabs。 - 说明编辑草稿为何留在
AddView,而不是提升到入口页。 - 给出未来弹层状态的归属判断和状态机设计。
- 分析返回恢复、保存成功、数据刷新与多设备窗口适配。
本文唯一标记:
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 跨页面共享,适合 AppStorage;currentIndex 只影响底部壳,使用本地 @State;title 只属于新建页面,继续留在 AddView。

把状态放得过高,会扩大刷新范围和耦合;放得过低,则可能在组件重建时丢失。正确位置是“能完整覆盖它所需生命周期的最低共同拥有者”。
三、MainTabShell:Tab 状态不进入 Index
MainTabShell 自己维护:
@State currentIndex: number = 0;
@State visitedTabs: boolean[] =
[true, false, false, false, false];
currentIndex 决定当前 Tab 与选中样式,visitedTabs 决定某个页面是否已经创建。它们不参与二级路由,也不需要被详情页修改,因此没必要提升到 Index 或 AppStorage。
.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' 等魔法值。
六、NavPathStack 为什么是 StorageLink
多个业务页面通过同一个键获取导航栈:
@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 更贴近单向消费。

共享并不意味着任意业务数据都应放进 AppStorage。导航栈是跨页面基础设施,编辑标题不是。
七、编辑草稿属于 AddView,而不是 Index
AddView 真实草稿状态:
@State selectedType: AnniversaryType = 'countdown';
@State title: string = '';
@State targetDate: string = '';
@State remark: string = '';
@State showSuccess: boolean = false;
这些字段只参与新建事项:
selectedType控制模板选择。title、targetDate、remark绑定输入控件。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();
}
这是一个轻量失效信号:它不保存事项数量,也不代表数据库版本。任何业务页都不应从这个数字推导实体内容。
当写操作增多时,可考虑事件总线或仓库订阅,但当前版本号方案简单直观。要避免的问题包括:
- 保存失败仍递增版本。
- 多次连续写入触发重复全量加载。
- 业务页面把版本号当成计数器展示。
- 页面卸载后监听没有按生命周期释放。
信号只负责“数据可能变化,请重新读取真源”。
当前源码边界:条件面板不是完整状态机
谈“弹层拆分”前,必须先说明当前界面的真实形态。AddView 的 showSuccess 控制的是页面内条件渲染的成功反馈,DetailView 的 showShareCard 控制的也是 Scroll 内容中的分享卡片。检查到的代码没有用 Dialog 或 CustomDialog 承载这两块内容,所以不能仅凭视觉上出现了一块覆盖区域,就把它们描述成系统弹窗或自定义对话框。
详情页还有另一组本地状态:isEditing、editTitle、editTargetDate 和 editRemark。进入编辑时,页面把当前事项的标题、格式化日期和备注复制到编辑字段;取消时只把 isEditing 设为 false;再次进入时再从当前事项复制。保存路径会校验标题和日期,调用 AppViewModel.saveAnniversary,更新本地事项,退出编辑,递增 DATA_VERSION 并显示提示。这些都是当前源码可以逐项对应的事实。
边界也很清楚。isEditing 与 showShareCard 是彼此独立的布尔值,当前字段本身并不保证“编辑”和“分享预览”互斥;取消编辑没有脏草稿比较,也没有返回确认。AddView 同样没有原始快照、isDirty、离开 Tab 确认和持久化恢复;它只校验标题非空,非空但不可解析的日期也没有显式 isNaN 或 Number.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。
建议把转换动作也收窄为 beginEdit、cancelEdit、saveEdit、openSharePreview 和 closeSharePreview。每个命令只负责一次可命名的状态变化,并在入口统一处理不兼容状态。例如 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);
}
它没有 showDialog、currentTab、titleText 等 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 保持在应用层;把 currentIndex、visitedTabs 留给 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 弹层。
- [ ]
currentIndex与visitedTabs归 MainTabShell。 - [ ] 新建草稿归 AddView,不进入全局状态。
- [ ] 路由名集中在
RouteNames,映射集中在RouteMap。 - [ ] 二级页面能通过共享 NavPathStack 返回。
- [ ] 保存成功后才清空草稿并发送刷新信号。
- [ ] 非法日期不会进入仓库。
- [ ] 弹层采用单一所有者或明确状态机。
- [ ] 手机、小窗、平板和键盘场景操作可达。
- [ ] 主题与安全区由壳层和共享状态协调。
- [ ] AppViewModel 没有混入纯 UI 覆盖层状态。
二十一、总结:入口越薄,状态边界越容易验证
时光清单 当前源码已经给出一个清楚的拆分结果:Index 是根 Navigation 宿主;MainTabShell 拥有 Tab 选择和按需创建;RouteMap 集中二级页面目的地;AddView 持有类型、标题、日期、备注和保存反馈;AppViewModel 只协调仓库动作。真实 Index 中没有业务弹层和编辑草稿,这正是入口页从复杂状态中解脱出来的表现。
后续新增能力时,应继续遵循同一判断:导航基础设施放在应用级,Tab 状态放在壳层,编辑草稿放在业务页,弹层放在拥有业务上下文的最低组件;只有跨重启或跨设备恢复时,才把草稿提升到专门存储。这样既能减少 ArkUI 无关重建,也能让返回、失败、恢复和多窗口适配拥有可复核的状态路径。
AI 辅助声明: 本文由 AI 辅助整理,当前结构与状态结论均依据 Index.ets、MainTabShell.ets、RouteMap.ets、RouteNames.ets、AddView.ets、StateKeys.ets 与 AppViewModel.ets 的真实源码人工复核;草稿持久化、弹层状态机和路由参数改造为明确标注的演进建议。
更多推荐




所有评论(0)