HarmonyOS 7 平行视界实战 05:生命周期、恢复与成品化打磨,完成“双栏研读台”最终收尾【鸿蒙心迹】
写到第五篇,我终于可以把“双栏研读台”这个 Demo 当成一个完整系列来回看了。
第一篇,我解决的是“能不能搭起来”;第二篇,我处理的是“能不能顺着读下去”;第三篇,我开始把固定导航、右侧覆盖与阅读路径组织清楚;第四篇,则重点把分栏比例、多尺寸适配和状态同步做稳。到了这一篇,我最关心的已经不是“有没有新页面”,而是另一个更真实的问题:
如果把这个 Demo 当成一个接近可交付的成品来看,它还缺什么?
答案其实并不复杂:缺的是生命周期意识、恢复策略、细节一致性,以及最后那一层把工程做“收”的能力。
很多技术文章做到第四篇时,往往已经把主要能力讲得差不多了,但最后离“成品感”总差一点。差的往往不是大功能,而是下面这些东西:
- 应用重新进入时,界面状态是不是自然恢复;
- 用户上次看到哪篇文章、分栏比例是多少、列表滚到哪里了,这些状态是不是能准确回来;
- 页面从前台到后台、从大屏到小窗、从平板到折叠屏展开态时,行为是不是一致;
- 代码结构是否已经从“能跑”变成“能维护”。
所以第五篇,我决定不再继续加功能,而是专门把“双栏研读台”做一次真正意义上的收尾:
- 用更明确的生命周期方案,接住应用创建、显示、切后台、再恢复这些场景;
- 把快照恢复逻辑从“能恢复”提升到“恢复得自然”;
- 再次整理项目结构,把状态、布局、路由和存储边界彻底拉开;
- 给 Demo 加上接近成品的细节判断标准。
这篇写完,我希望这个系列不是停在“示例代码”,而是停在一个足够像真实项目原型的程度。

一、为什么第五篇不继续加功能,而是选择“收口”
一套平行视界 Demo 能不能让人信服,其实不是看它塞了多少页面,而是看它有没有一个稳定的结束姿势。
我自己做这套“双栏研读台”时,前四篇已经把“阅读场景”里最主要的能力搭完了:
- 左侧稳定导航;
- 中间文章列表;
- 右侧阅读详情;
- 多尺寸下的分栏比例;
- 覆盖式详情切换;
- 状态同步与基础恢复。
如果第五篇还只是继续加一个“收藏页”或者“评论页”,文章当然也能写,但它并不会让这个系列更完整。反过来,如果我认真把生命周期、恢复策略和成品化细节收好,这一篇反而会把前四篇都串起来。
这是因为平行视界这种能力,本质上不是一个单点功能,而是一种跨设备、多尺寸、长时段持续可用的界面组织方式。只要它不能稳定恢复、不具备自然的生命周期行为,那它看起来再漂亮,也仍然像个半成品。
所以第五篇的核心目标很明确:
让“双栏研读台”从“已经能演示的 Demo”,走到“已经具备成品感的原型”。
二、先重新看一遍项目目录:第五篇开始,结构比页面更重要
到第五篇,我对工程目录又做了一次收整。不是为了“看起来更高级”,而是因为第五篇开始,真正复杂的已经不是单个页面,而是状态和服务之间的边界。
DualReadDemo
├─ entry
│ ├─ src/main/ets
│ │ ├─ pages
│ │ │ └─ Index.ets
│ │ ├─ components
│ │ │ ├─ ArticleList.ets
│ │ │ ├─ ArticleDetail.ets
│ │ │ ├─ RatioSelector.ets
│ │ │ └─ ReadingHeader.ets
│ │ ├─ route
│ │ │ └─ Router.ets
│ │ ├─ model
│ │ │ ├─ Article.ets
│ │ │ ├─ ReadingState.ets
│ │ │ └─ RestoreSnapshot.ets
│ │ ├─ service
│ │ │ ├─ StateSyncService.ets
│ │ │ ├─ ThemeService.ets
│ │ │ └─ StorageService.ets
│ │ ├─ store
│ │ │ └─ ReadingSessionStore.ets
│ │ └─ common
│ │ ├─ Constants.ets
│ │ └─ Logger.ets
│ └─ resources
└─ module.json5
这个目录里,我特别在意三个点:
1)页面只负责呈现和事件响应
Index.ets、ArticleList.ets、ArticleDetail.ets 仍然重要,但它们不再承担“存储状态”这种职责。页面该做的,是读取状态、展示状态、触发事件。
2)恢复和同步逻辑全部下沉到服务层
StateSyncService.ets 是第五篇的关键角色,它负责把“比例、文章、滚动位置、导航、覆盖层深度”等内容组装成统一快照,再决定何时存、何时取、如何恢复。
3)模型层不再只是数据结构,而是明确的状态协议
ReadingState.ets 和 RestoreSnapshot.ets 让“什么叫可恢复状态”这件事变得清楚。对于这种系列型文章来说,这个清晰度非常重要,因为它会直接决定文章讲出来是不是工程化。
三、我先把“可恢复状态”重新定义了一遍
前几篇已经有恢复逻辑,但到第五篇我越来越觉得,想真正做到成品化,状态不能只是零散字段,而应该是一份可以完整表达阅读现场的快照。
所以我把它统一成下面这个结构:
export interface RestoreSnapshot {
navId: string
articleId: string
ratio: '1:1' | '1:2' | '2:1'
overlayDepth: number
listScrollOffset: number
detailScrollOffset: number
ratioPinned: boolean
saveTime: number
}
这里比第四篇多出来的关键点有两个:
detailScrollOffset:右侧文章正文也要记住读到哪里;ratioPinned:区分“系统推荐比例”和“用户主动固定比例”。
这两个字段看起来很小,但它们非常像“成品感开关”。
如果没有 detailScrollOffset,应用重新打开以后,用户虽然回到了同一篇文章,却可能又被送回正文顶部;如果没有 ratioPinned,系统每次窗口变化都可能覆盖用户刚刚选好的阅读习惯。
第五篇真正想传递的,就是这种“细节决定完成度”的感觉。
四、StateSyncService 是第五篇真正的主角
这一篇最重要的,不是页面长什么样,而是状态服务到底怎么设计。
我最后让 StateSyncService 接管了四类职责:
- 读取本地快照;
- 保存当前快照;
- 根据快照恢复界面关键状态;
- 把用户的手动选择固定下来。
它的核心代码大概像这样:
import { Preferences } from '@kit.ArkData'
export class StateSyncService {
private static readonly STORE_KEY: string = 'dual_read_session'
private static readonly RATIO_KEY: string = 'split_ratio'
private static readonly ARTICLE_KEY: string = 'current_article'
async syncCurrentState(articleId: string, listOffset: number, detailOffset: number,
ratio: string, navId: string, overlayDepth: number): Promise<void> {
await this.saveSnapshot({
articleId,
navId,
listScrollOffset: listOffset,
detailScrollOffset: detailOffset,
ratio,
overlayDepth,
saveTime: Date.now()
})
}
async saveSnapshot(snapshot: RestoreSnapshot): Promise<void> {
const pref = await Preferences.getPreferences(StateSyncService.STORE_KEY)
await pref.putSync('snapshot', JSON.stringify(snapshot))
}
async restoreSnapshot(): Promise<RestoreSnapshot | null> {
const pref = await Preferences.getPreferences(StateSyncService.STORE_KEY)
const raw = pref.getSync<string>('snapshot', '')
return raw ? JSON.parse(raw) as RestoreSnapshot : null
}
}
这一层一旦稳定,页面会轻松很多。
更重要的是,它让“保存状态”和“恢复状态”变成了可被单独理解、单独测试、单独扩展的能力,而不是散落在各个页面事件里的小碎片。

五、应用生命周期里,最需要拎清的是哪几个时间点
第五篇如果只讲“onPageShow” 或 “aboutToAppear” 肯定不够,因为真正的收尾一定要把应用从创建到恢复的几个关键节点讲清楚。
我最后重点关注了下面四个时机:
1)应用首次创建
第一次进入时,没有快照,就要走默认布局和默认内容。这个时候更重要的是“快速可用”,而不是“恢复”。
2)页面重新显示
这通常发生在从后台回前台、从其他页面返回、或者窗口重新激活。这个阶段最适合做“状态恢复”。
3)页面交互过程中
只要用户切文章、切分栏比例、滚动列表、滚动正文,就应该触发节流后的状态同步。这里不能每一个像素滚动都存,但也不能完全不存。
4)页面即将离开或应用进入后台
这是保存快照的关键兜底时机。哪怕前面的增量同步有遗漏,这里也要再做一次完整保存。
所以在 Index.ets 里,我最后会把生命周期组织成这样:
@Entry
@Component
struct Index {
@State private ratio: RatioType = '1:2'
@State private navId: string = 'home'
@State private articleId: string = ''
@State private overlayDepth: number = 0
private stateSync: StateSyncService = new StateSyncService()
async onWindowStageCreate() {
const snapshot = await this.stateSync.restoreSnapshot()
if (snapshot) {
this.applySnapshot(snapshot)
}
}
async onPageShow() {
const snapshot = await this.stateSync.restoreSnapshot()
if (snapshot) {
this.applySnapshot(snapshot)
}
}
async onPageHide() {
await this.saveCurrentSnapshot()
}
}
我知道很多人看到这里会说:onWindowStageCreate 和 onPageShow 里都恢复,会不会重复?
答案是:会有重叠,但这是有意的。
因为成品化收尾时,我更在意的是“恢复可靠”,而不是“生命周期写得多短”。真正好的做法不是完全避免重复,而是让重复依然有规则,比如:
onWindowStageCreate负责首次进入时的基础恢复;onPageShow负责重新显示时的二次校正;- 如果两次读到的快照一致,页面只会做轻量更新,不会造成闪动。
六、恢复不是把值塞回去就完了,而是要“按顺序恢复”
这是第五篇我觉得最值得拿出来单写的一点。
很多 Demo 做恢复时,会把 ratio、articleId、navId 一次性全塞回去,页面表面上也能跑起来。但只要场景一复杂,就会出现顺序问题:
- 文章还没加载出来,滚动位置先恢复了,结果无效;
- 左侧列表还没切到对应栏目,文章却先跳到旧文章;
- 比例还没应用完成,正文区域高度又变了,导致位置错位。
所以我在第五篇里,专门把恢复流程拆成了一个顺序化过程:
恢复导航 → 恢复分栏比例 → 恢复当前文章 → 恢复覆盖层深度 → 恢复列表滚动位置 → 恢复正文滚动位置
代码里大概像这样:
private async applySnapshot(snapshot: RestoreSnapshot) {
this.navId = snapshot.navId
this.applyRatio(snapshot.ratio)
this.articleId = snapshot.articleId
this.overlayDepth = snapshot.overlayDepth
await this.restoreListOffset(snapshot.listScrollOffset)
await this.restoreDetailOffset(snapshot.detailScrollOffset)
}
这段逻辑的重点不是复杂,而是顺序意识。
第五篇真正收尾的感觉,就是从这里出来的:以前我只是“会恢复”,现在我要让它“恢复得像真的”。
七、第五篇里,我还专门处理了“用户固定比例”和“系统推荐比例”的冲突
第四篇其实已经埋下这个问题,但第五篇必须把它处理完整。
因为当应用逐渐像成品时,用户对界面会形成记忆。这个时候,系统自动推荐比例当然仍然有价值,但它不能粗暴覆盖用户自己的选择。
所以我的策略是:
- 默认情况下,系统按宽度断点推荐比例;
- 用户一旦主动点击
1:1 / 1:2 / 2:1,就标记为ratioPinned = true; - 后续普通尺寸变化只做提示,不强制改回推荐值;
- 只有在跨形态场景(例如从窄窗切到大平板或折叠屏展开态)时,才提醒用户是否重新适配。
对应代码大概会这样写:
private onRatioChange(nextRatio: RatioType) {
this.ratio = nextRatio
this.readingState.ratioPinned = true
this.saveCurrentSnapshot()
}
private handleWindowChange(width: number) {
const recommended = LayoutService.autoSelectRatio(width)
if (!this.readingState.ratioPinned) {
this.applyRatio(recommended)
}
}
这套规则其实体现的是一个很朴素的产品判断:
当用户已经明确表达了自己的偏好时,系统应该学会退后一步。
八、无边框运行图里,第五篇最重要的不是花哨,而是“像成品”
如果前几篇的运行图强调的是功能,这一篇的无边框纯界面图,更应该突出的是“完成度”。
所以这张图里,我最希望看到的是几个东西同时成立:
- 左侧导航不再只是能点,而是层级和数量都显得稳定;
- 顶部比例按钮不再像测试控件,而像产品的一部分;
- 中间列表与右侧正文在信息密度上形成稳定节奏;
- 页面上能看到“已恢复上次阅读状态”这类轻提示,但不过度抢眼。
说白了,第五篇要表达的是:
这不再只是一个为了配图而存在的双栏界面,而是一个用户可以真的在上面连续阅读的应用。

九、类型二综合讲解图,在收尾篇里像一张“总验收图”
我一直很喜欢“类型二综合讲解图”,因为它特别适合技术文章。到第五篇,它的价值就更明显了:它不只是说明某个点,而像一张总验收图。
这一张图里,我会把五类信息放在一起:
- DevEco 里的关键代码结构;
- 模拟器中的最终 UI;
- 生命周期与恢复流程关系图;
- 实机桌面场景;
- 状态保存和恢复的日志。
这样读者看到它时,不用来回切换上下文,就能一次看明白:
- 这套 Demo 怎么写的;
- 最终界面长什么样;
- 生命周期如何组织;
- 真机跑起来是不是自然;
- 状态恢复有没有证据。
从写作者角度讲,这张图其实也是在帮助第五篇“收口”——它把前四篇铺下来的内容,用一种非常直接的方式重新汇总了一遍。

十、DevEco 图这次我最想展示的,是“恢复逻辑已经成为工程的一部分”
第五篇的第二张 DevEco 图,我刻意没有去放太多视觉噱头,而是让它更像真实开发中的一个状态:
- 左侧项目目录清楚可见;
- 中间是
Index.ets或与生命周期相关的核心代码; - 右侧模拟器统一放在右边;
- 底部日志能直接看到 restore、saveSnapshot、applyRatio 这些关键输出。
因为到了这一篇,读者最需要相信的已经不是“你会不会写 ArkTS”,而是“你有没有把一套阅读应用真正打磨到能长期工作的程度”。
所以我会特别保留这样的日志:
[Lifecycle] onWindowStageCreate start
[State] restore success, navId=home, articleId=1024
[Layout] apply ratio=1:2, overlayDepth=0
[State] save snapshot, ratio=1:2, articleId=1024
[Lifecycle] onPageShow: restore success
有了这些输出,第五篇就不只是“讲思路”,而是有了非常具体的工程证据。

十一、实机效果最能暴露问题,也最能说明“收尾是不是到位了”
做平行视界这种主题,模拟器当然必不可少,但我一直觉得,最后真正能说明问题的,还是实机。
因为一旦把它放到平板或者更大的横向设备里,很多小问题都会变得特别明显:
- 导航区是不是太挤;
- 列表密度是不是太松;
- 正文区滚动后,重新进入能不能顺着读;
- 顶部比例切换是不是影响沉浸感。
而第五篇的收尾,恰恰就靠这些问题来反推。
我自己在体验时,最满意的一点其实不是界面变得更好看,而是重新回到应用时,会明显感觉到:
- 左边还是你刚才看的那个栏目;
- 中间还是那组文章;
- 右边还是那篇读到一半的内容;
- 比例也还是你习惯的那个比例。
这种“继续感”,才是我心里对成品化最重要的判断标准。

十二、第五篇最后怎么验收,我给自己定了六条标准
到了最终篇,我不再只看“功能点都做了没有”,而是用下面六条标准来验收:
1)首次进入是否自然
没有历史快照时,应用能不能快速进入一个默认但合理的阅读状态。
2)恢复是否完整
重新进入后,能否把栏目、文章、比例、列表位置、正文位置一并恢复。
3)恢复是否按顺序
不能出现先滚再载入、先恢复正文再切栏目这种时序错乱。
4)手动选择是否被尊重
用户主动选中的分栏比例,不能轻易被系统窗口变化覆盖。
5)日志是否能解释行为
恢复失败也好,恢复成功也好,日志都应该能看出发生了什么。
6)代码是否还能继续维护
页面层、状态层、服务层是否已经分清楚,为后续继续扩展留出空间。
如果这六条都能成立,第五篇就算真的把这个 Demo 收住了。
十三、这一套五连载,到这里我会怎么总结
现在回头看这五篇,我觉得它们不是五个散点,而是一条很完整的学习路线:
- 第一篇:从零搭好双栏阅读骨架,理解平行视界适合什么样的内容场景;
- 第二篇:把列表、详情和连续阅读主链路串起来;
- 第三篇:解决固定导航、右侧覆盖切换和阅读路径组织;
- 第四篇:处理多尺寸下的分栏比例与状态同步;
- 第五篇:用生命周期、恢复策略和成品化打磨,把 Demo 做到收口。
这五篇一路写下来,我自己最大的感受反而不是“又掌握了一个新 API”,而是越来越清楚:
平行视界真正考验的,从来不只是布局能力,而是内容组织能力、状态管理能力和跨设备体验设计能力。
这也是我想给社区读者留下的核心印象。
如果只想做一张好看的大屏图,可能第一篇就够了;但如果真想做出一个值得长期维护、也值得拿出来讲的 Demo,这第五篇就非常必要。
十四、本文小记
写到这里,“双栏研读台”这个系列终于完整了。
如果说前几篇更像是在不断向外长功能,那第五篇就是在向内收结构、收状态、收体验。它不再追求“再多一个能力点”,而是把已经有的东西真正打磨到能够交代清楚、恢复自然、工程边界稳定的状态。
这也是我越来越喜欢这种连载写法的原因:
- 它能把一个看似抽象的系统能力,拆成一条可学习、可验证、可演进的路径;
- 它也逼着我不只写“会用”,而是继续往“用得像产品”推进。
所以第五篇的收尾,我最想留下的一句话其实是:
HarmonyOS 7 平行视界的价值,不只在于把界面并列展示出来,而在于让阅读、切换、恢复、继续这些行为,在多设备场景里都变得自然。
到这一步,“双栏研读台”已经不再只是一个为文章服务的示例工程,而是一个足够像真实原型的平行视界学习样本。
如果后面还要继续扩展,我会优先考虑评论区折叠、文章批注、跨设备接力阅读和更细致的主题切换。但那已经不再是“补作业”,而是在一个稳定底座上的继续演进了。
更多推荐


所有评论(0)