HarmonyOS 7 精准碰一碰 SlideDrop 实战 05:状态同步 + 生命周期收口完成演示协作工作台【鸿蒙心迹】
这套 SlideDrop 五连载写到第五篇,我反而没有继续追着“新能力”往前冲。前四篇已经把主链路、精准落位、规则判断、冲突恢复都讲得差不多了,最后这一篇如果还继续堆新点,就容易把整套 Demo 写散。第五篇我更想做的一件事,是把前面零散补起来的能力真正收住:状态怎么同步、页面退出以后资源怎么释放、会话怎么归零、最近结果怎么保留、最后又怎么做一轮完整验收。
说白了,第五篇不是“再做一个功能”,而是把 SlideDrop 从“能演示”收成“能交代”的工程 Demo。它不一定最热闹,但它决定这个五连载最后是不是站得稳。

一、为什么第五篇不再追新功能,而是开始做工程收口
做到第四篇的时候,SlideDrop 其实已经挺像一个完整 Demo 了:
- 手机端可以发起精准碰一碰;
- 目标区域可以识别并完成落位;
- 素材类型与槽位规则已经建立起来;
- 冲突替换、取消、超时、重试这些边界也有了基本闭环。
但如果站在“文章完结”和“项目交付”的角度看,前四篇还差最后一层东西。那就是:系统在任务结束以后,能不能回到一个干净、稳定、可继续演示的状态。
这个问题其实比前面任何一篇都更像真实工程。因为功能做出来以后,最容易出问题的往往不是“执行时”,而是“执行完之后”。比如:
- 页面退出了,但上一次会话状态还留在内存里;
- 最近结果列表能看到记录,可是对应缓存没有清掉;
- 预览图还在,占着本地空间;
- 失败重试队列其实已经没意义了,但没有释放;
- 一轮 Demo 演示跑完以后,再重新开始第二轮,会发现界面还带着上一次的痕迹。
这些问题看起来都不是大 Bug,但它们会直接影响读者对 Demo 的判断:这到底是一个“演示后就乱”的半成品,还是一个“跑完一轮还能马上继续演示”的工程化样品。
所以第五篇我给自己定下来的核心目标是四个字:收口、同步。
- 收口:把任务、会话、页面和资源统一收回到干净状态;
- 同步:让手机端、页面端、日志端、结果端都对同一轮任务有一致认知。
二、第五篇的主线:让 SlideDrop 从“跑通”变成“跑完也稳定”
如果说第一篇是搭主链路,第二篇是做落位,第三篇是做插入规则,第四篇是补异常恢复,那第五篇很自然就该进入“工程收官”阶段。
我后来把第五篇真正想讲的东西,收成了下面这条线:
会话状态同步 → 页面生命周期收尾 → 本地资源释放 → 最近结果保留 → 最终验收输出。
这条线看起来没有前三篇那么“显眼”,但它非常重要。因为 SlideDrop 这种跨设备演示 Demo,并不是只在某一帧 UI 上成立,它更像一段完整过程:开始、传输、结束、清理、再开始。如果结束以后收不干净,前面做得再漂亮,后面也会越来越脏。
所以第五篇我不是再去扩展一个新页面,而是尽量让整个 Demo 的末端逻辑变得更顺:
- 当前会话结束以后,状态中心能立刻同步到“空闲”;
- 最近结果区保留本轮成功记录,便于文章展示与后续验收;
- 预览缓存、临时文件、失败任务队列都能及时释放;
- 页面重新进入时,不会被旧状态污染;
- 最终还能输出一份“验收通过”的可见结果。
从文章写作角度看,第五篇也是我最愿意写“工程手记”的一篇。因为它不再只是讲某个局部技巧,而是在讲一个 Demo 怎么真正收束成作品。
三、第五篇我重新看了一遍项目目录,确定最后该收哪些层
到第五篇时,目录结构已经比较完整了,这一篇我没有再大拆,而是重新确认哪些层需要做最后的收尾。
SlideDropDemo
├─ entry
│ ├─ src/main/ets
│ │ ├─ pages
│ │ │ ├─ HomePage.ets
│ │ │ ├─ SlideDropPage.ets
│ │ │ ├─ ResultPage.ets
│ │ │ └─ ProfilePage.ets
│ │ ├─ components
│ │ │ ├─ ProgressCard.ets
│ │ │ ├─ ResultItem.ets
│ │ │ └─ StatusCard.ets
│ │ ├─ manager
│ │ │ ├─ TransferManager.ets
│ │ │ ├─ ConflictRecoveryManager.ets
│ │ │ └─ FinalAcceptanceService.ets
│ │ ├─ store
│ │ │ ├─ TransferStore.ets
│ │ │ └─ AppStore.ets
│ │ ├─ model
│ │ │ ├─ TransferMission.ets
│ │ │ ├─ RecentResult.ets
│ │ │ └─ SessionInfo.ets
│ │ ├─ utils
│ │ │ ├─ FileUtil.ets
│ │ │ ├─ SessionUtils.ets
│ │ │ └─ DateUtil.ets
│ │ └─ common
│ │ └─ Logger.ets
│ └─ resources
└─ module.json5
这一版目录我最关心的是三个点:
TransferStore:负责把当前会话、最近结果、同步状态放在统一位置;SlideDropPage:负责页面进入和离开时的状态同步与清理;FinalAcceptanceService:负责最后做一致性校验、资源释放检查和验收快照。
也就是说,第五篇并不是重写项目,而是把前面几篇零散分布的“结束动作”重新聚拢起来。

四、先把状态同步做好:否则页面一退出,前面的结果全都可能失真
我自己做这类 Demo 时,最怕出现的情况不是“功能没做出来”,而是“功能做出来了,但状态散在各处”。比如页面上显示成功了,日志还停在传输中;或者最近结果列表更新了,但当前会话却没有被清空。这种“不一致”比直接失败更烦,因为它最容易把读者绕晕。
所以第五篇第一件事,我先回头把 TransferStore 再收了一遍,让当前会话和最近结果都通过同一个 Store 管理。
文件位置: entry/src/main/ets/store/TransferStore.ets
用途: 统一维护当前会话状态、最近结果、同步进度和页面共享数据。
export enum SessionState {
IDLE = 'IDLE',
WAITING = 'WAITING',
TRANSFERRING = 'TRANSFERRING',
SUCCESS = 'SUCCESS',
FAILED = 'FAILED'
}
export interface SessionInfo {
id: string
status: SessionState
progress: number
fileName: string
updateTime: number
}
export class TransferStore {
private static instance: TransferStore
private currentSession?: SessionInfo
private recentResults: RecentResult[] = []
static getInstance(): TransferStore {
if (!TransferStore.instance) {
TransferStore.instance = new TransferStore()
}
return TransferStore.instance
}
updateSession(session: SessionInfo) {
this.currentSession = session
}
getCurrentSession(): SessionInfo | undefined {
return this.currentSession
}
resetCurrentSession() {
this.currentSession = {
id: '',
status: SessionState.IDLE,
progress: 0,
fileName: '',
updateTime: Date.now()
}
}
pushRecentResult(result: RecentResult) {
this.recentResults.unshift(result)
this.recentResults = this.recentResults.slice(0, 20)
}
getRecentResults(): RecentResult[] {
return this.recentResults
}
}
这段代码看起来很基础,但它把第五篇最重要的一件事定下来了:页面不是自己保存状态,页面只是消费状态。
这样一来,SlideDrop 页面、结果页、状态页看到的都是同一份会话数据。任务结束以后,系统要回到 IDLE,也只需要在一个入口里统一重置。
五、生命周期收尾必须认真做:aboutToAppear 和 aboutToDisappear 终于成了关键角色
我以前写 Demo 时,经常把生命周期函数当成“顺手写一下”。但到第五篇,我反而觉得它们变成了整套 Demo 能不能收住的关键。
原因很直接:
- 页面进入时,你得把上次会话同步回来;
- 页面离开时,你得把该释放的资源释放掉;
- 如果这里做得马虎,前面所有状态同步都会被拖垮。
所以第五篇里我重点收的是 SlideDropPage.ets。这次不再把它仅仅当作一个显示页面,而是让它真正承担起“页面级的状态同步与资源回收”。
文件位置: entry/src/main/ets/pages/SlideDropPage.ets
用途: 页面进入时同步当前会话,页面离开时完成缓存释放、重试队列清理和会话归零。
import { AppStorage } from '@ohos.storage'
import { TransferStore, SessionState } from '../store/TransferStore'
import { syncCurrentSession, clearPendingTasks, releasePreviewCache } from '../utils/SessionUtils'
@Entry
@Component
struct SlideDropPage {
@State currentTab: number = 0
@State sessionState: SessionState = SessionState.IDLE
private store: TransferStore = TransferStore.getInstance()
aboutToAppear(): void {
console.info('[SlideDrop] [Appear] page aboutToAppear')
syncCurrentSession().then((session) => {
this.sessionState = session.status
this.store.updateSession(session)
console.info('[SlideDrop] [Appear] sync page state, sessionId=' + session.id)
}).catch((err) => {
console.error('[SlideDrop] [Appear] sync page state failed: ' + err)
})
}
aboutToDisappear(): void {
console.info('[SlideDrop] [Dispose] page aboutToDisappear')
releasePreviewCache().then(() => {
console.info('[SlideDrop] [Dispose] release preview cache')
})
clearPendingTasks().then(() => {
console.info('[SlideDrop] [Dispose] clear pending retry tasks')
})
AppStorage.setOrCreate<SessionState>('slideDrop_session_state', SessionState.IDLE)
this.store.resetCurrentSession()
console.info('[SlideDrop] [State] session reset to IDLE')
}
}
这段代码最直接的价值,就是把“演示结束以后要干什么”说清楚了:
- 释放预览缓存;
- 清理失败重试任务;
- 当前会话重置为
IDLE; - 下次再进入页面时,从干净状态重新开始。
对于读者来说,这种代码很有说服力。因为它不是为了凑一个生命周期 API,而是真在解决“跑完一轮以后能不能马上再演示一次”的问题。

六、第五篇里我最在意的,是“成功结果要保留,运行痕迹要清掉”
做工程收口时,很容易出现一个极端:要么什么都不保留,任务一结束页面像没发生过一样;要么什么都保留,页面上到处都是旧痕迹。SlideDrop 第五篇我想要的,不是这两个极端里的任何一个。
我最后给自己定的原则是:
成功结果要保留,运行痕迹要清掉。
什么意思?
- 当前会话卡片里的“传输中”“等待连接”这些状态,是运行痕迹,任务结束以后应该清掉;
- 最近结果列表里的“第 4 章产品设计方案传输成功”这种记录,是成果信息,应该保留;
- 预览缓存和临时文件,是技术过程中的中间产物,应该释放;
- 验收结果和统计快照,是最后要对外展示的证据,可以保留。
第五篇最让我满意的一点,就是我终于把这四类信息分清楚了。以前很多 Demo 做着做着,什么该留、什么该删会越来越模糊。到最后页面虽然热闹,但结构是乱的。现在 SlideDrop 到第五篇,这个边界已经清楚了。
七、手机端状态页必须讲人话:用户要看懂“这一轮已经收完了”
技术文章配图里,手机图最容易只顾好看,不顾表达。第五篇我反而特别在意手机端状态页是不是够“像人话”。因为这一页本质上承担的是“告诉用户这轮任务已经收口”的责任。
所以我在这一页里保留了四组信息:
- 当前会话已经完成;
- 清理状态已经通过;
- 最近结果还能看到刚刚成功的记录;
- 验收清单已经走完。
这四组信息组合起来,读者一眼就能看懂:这不是单纯显示一个成功图标,而是在说明本轮传输、资源释放、状态归位,都已经处理结束。

八、最后要有一个真正的“验收出口”,所以我补了 FinalAcceptanceService
如果第五篇只停在“生命周期清理”这一层,工程意味还是不够强。因为它只是做了过程整理,还没有给出一个明确的“最后结论”。所以这篇我又补了一层 FinalAcceptanceService,把最后那一步做完整:做一轮一致性检查,并给出验收结果。
我做这个类时,其实只想解决三个问题:
- 当前状态是不是一致;
- 资源是不是真的清掉了;
- 最近结果是不是还能查到。
如果这三件事都成立,我就认为这一轮 Demo 可以被视为“完成收口”。
文件位置: entry/src/main/ets/manager/FinalAcceptanceService.ets
用途: 做状态一致性校验、资源释放检查、最近结果验证,并生成演示快照。
import { AppStore } from '../store/AppStore'
import { FileUtil } from '../utils/FileUtil'
import { Logger } from '../common/Logger'
export class FinalAcceptanceService {
private store: AppStore = AppStore.getInstance()
private logger: Logger = Logger.getInstance()
async validateStateConsistency(): Promise<boolean> {
const storeState = this.store.getState()
const queueEmpty = storeState.transferQueue.length === 0
const activeTask = storeState.currentTransfer == undefined
const resultCountOk = storeState.recentResults.length >= 0
const pass = queueEmpty && activeTask && resultCountOk
this.logger.info(`[Accept] state consistency ${pass ? 'pass' : 'fail'}`)
return pass
}
async validateResourceRelease(): Promise<boolean> {
const tempDir = `${this.store.getCacheDir()}/temp`
const files = await FileUtil.listFiles(tempDir)
const released = files.length === 0
this.logger.info(`[Accept] resource release ${released ? 'pass' : 'fail'}`)
return released
}
async validateRecentResult(): Promise<boolean> {
const results = this.store.getRecentResults()
const hasSuccess = results.some(r => r.status === 'success')
this.logger.info(`[Accept] recent result ${hasSuccess ? 'found' : 'not found'}`)
return hasSuccess
}
async runFinalAcceptance(): Promise<boolean> {
const s1 = await this.validateStateConsistency()
const s2 = await this.validateResourceRelease()
const s3 = await this.validateRecentResult()
const pass = s1 && s2 && s3
this.logger.info(`[Accept] final acceptance ${pass ? 'SUCCESS' : 'FAILED'}`)
return pass
}
}
这段代码让我觉得第五篇终于有了一个“结束句号”。因为你前面无论做了多少同步、多少清理,最后总要有个地方能说清楚:这一轮到底算不算完整结束。
九、类型二综合讲解图,这一篇我最想强调的是“从同步到清理的完整闭环”
前几篇你已经让我固定了两种图的语言:简约封面和类型二综合讲解图。到第五篇,这张类型二图的意义比前面更大,因为它不再只是解释一个单点逻辑,而是把整条闭环压缩成了一张图。
在这张图里,我最想放进去的是四种证据:
- DevEco 里的状态监听与页面刷新代码;
- 手机端的同步完成与清理状态界面;
- 中间的状态流 / 资源流关系图;
- 下方的实机传输过程。
这样读者一眼就会明白,第五篇不是在讲“某一个按钮怎么写”,而是在讲:
一轮跨设备演示任务结束以后,系统怎样把状态同步、会话管理、资源清理和最终验收收成一个闭环。

十、这一篇我怎么验收:重点已经不是功能成立,而是“下一轮还能不能马上继续”
第五篇的验收方式,和前几篇已经很不一样了。前面几篇更像“这条功能链路是不是成立”,而这一篇更像“这一轮结束以后系统还能不能马上继续工作”。
我这次基本按下面几条来验收。
1)当前会话是否能回到 IDLE
这是最基础的一条。传输完成以后,当前会话页不能一直挂着上一次任务信息,而是要回到空闲态,明确告诉用户“现在没有进行中的任务”。
2)最近结果是否被保留
虽然当前任务状态要清掉,但最近结果区必须能看到上一轮成功记录。这样读者和用户都能回看刚才发生了什么,也方便文章截图佐证。
3)缓存与临时文件是否被释放
这是第五篇的重点之一。我会故意跑一轮带预览、带文件中转的完整流程,再检查缓存和临时目录,看释放动作是否真的执行。
4)失败重试队列是否被清空
第四篇引入了重试机制,第五篇必须把这件事收住。否则页面看起来结束了,队列里还留着失败任务,后面肯定会污染下一轮演示。
5)页面重新进入时是否干净
我会退出页面,再重新进入,观察是否还会带上旧 sessionId、旧进度、旧文件名。如果这些痕迹还在,就说明“收口”并没有真正完成。
6)最终验收日志是否清晰
最后我会看日志:
- 有没有出现状态同步;
- 有没有出现资源释放;
- 有没有出现最近结果验证;
- 最终是否打印
SUCCESS。
只有这些日志和页面表现一致,第五篇我才算真正写踏实了。

十一、第五篇写完以后,这个五连载终于真正收住了
回头看这五篇,其实它们像是在围着同一个 Demo 做五次不同维度的逼近:
- 第一篇解决“能不能建立主链路”;
- 第二篇解决“能不能落到正确位置”;
- 第三篇解决“素材类型和槽位规则能不能协同”;
- 第四篇解决“冲突、取消、重试能不能处理”;
- 第五篇解决“演示结束以后,系统能不能自己收回来”。
写到最后我越来越确定,第五篇虽然不一定是最有冲击力的一篇,但它非常关键。因为它让整个系列从“会做功能”走向“会做收尾”。而一个项目真正成熟,往往就体现在这里。
十二、本文小记
如果让我用一句话总结第五篇,我会说:
第五篇最重要的,不是增加了多少新功能,而是让 SlideDrop 终于具备了“完成一轮演示并回到稳定起点”的能力。
它做的事情包括:
- 统一当前会话与最近结果的状态来源;
- 用生命周期函数把页面进入与离开真正管起来;
- 把预览缓存、失败任务和临时文件收干净;
- 用状态中心页把“收尾结果”对用户讲明白;
- 最后通过验收服务给出完整结论。
如果前四篇是在搭房子,那第五篇更像是在做收边、封口、清场。它不会比建主体更显眼,但没有它,整套 Demo 就始终差最后一口气。现在这口气总算补上了。
而且写完这一篇以后,我对这个五连载最满意的一点也终于确定下来:它不是单纯围着一堆 API 做讲解,而是真的围着一个 Demo 的成长过程在写。这样一来,读者看到的不只是功能点,而是一个项目从“能跑”到“能交代”的完整变化。
十三、第五篇里我专门补了一轮“反向验证”,看收尾逻辑是不是只在理想路径上成立
工程收口这件事,最怕的不是没写,而是只在最理想的一条路径上成立。也就是说,你在 happy path 里跑一遍,页面确实变成了 IDLE,缓存似乎也释放了,于是你以为问题解决了。但只要换一个顺序,比如传到一半退出、成功以后快速重复打开、或者刚生成最近结果就立刻切到后台,很多收尾逻辑就会开始露馅。
所以第五篇我给自己多补了一轮“反向验证”。它不是为了多制造复杂度,而是想确认这套收尾逻辑不是靠运气成立。
我大致按下面几组场景去测:
1)传输成功后立刻离开页面
这个场景主要看两件事:
- 最近结果是否已经写入;
- 当前会话是否已经在离开前被重置。
如果写入顺序不对,很可能出现结果没保存、状态却先清空的情况。这样文章里看起来像“系统干净了”,但用户会觉得“我刚刚传成功的记录怎么没了”。所以我在这一组里特别关注 recentResults 的写入时机。
2)传输过程中主动中断,再返回首页
这组场景不是为了验证成功路径,而是为了验证页面销毁回调是否足够稳。因为一旦中断发生,失败重试队列、临时文件、预览资源都可能残留。
如果 aboutToDisappear() 只做表面清理,没有真正处理好 pending task,那么下一次重新进入页面,系统会误以为还有任务在继续。读者可能看不出这是哪一层出的问题,但会明显感觉“页面不干净”。
3)连续执行两轮演示任务
这一组我觉得最接近实际使用。因为做社区文章、课程演示或线下分享时,你几乎不会只跑一遍。往往是第一轮给大家看主链路,第二轮再补某个细节。如果系统第二轮刚开始就带着第一轮的残留状态,那整套 Demo 的可信度就会一下掉下来。
所以我会连续传两次不同文件:第一次传 会议资料.pdf,第二次传 产品方案_v3.pptx,然后观察:
- 当前会话是否仍然从
IDLE起步; - 最近结果是不是两条都能看到;
- 第二轮是否被第一轮的 sessionId、progress 或缓存影响。
4)切后台再回来
这一组主要看页面重新激活时是否会误判当前状态。有些 Demo 最大的问题,不是前台逻辑不工作,而是用户切出去一下,再回来就乱了。第五篇虽然没有把“前后台切换管理”单独展开成新能力,但我至少希望:
- 回来以后页面不会突然显示旧进度;
- 已经完成的任务不会重新进入“传输中”;
- 状态中心依然能保持收口后的样子。
做完这轮反向验证以后,我心里会更踏实。因为这说明第五篇做的事情,不只是把代码写齐了,而是真的开始经得住一点变化。
十四、这篇里几个容易被忽略,但我觉得很值钱的实现细节
写这篇时我越来越有一个感觉:真正让 Demo 变成熟的,不一定是最显眼的功能,而是那些不太起眼、却能减少后续麻烦的细节。第五篇里我最想记下来的,有下面几件。
1)把“空闲态”明确当成一种状态,而不是默认空白
很多人做页面收尾时,会把“任务完成以后”理解成“把 UI 清空”。但我后来发现,清空不是状态,空闲才是状态。
两者区别很大。
- 清空,意味着界面上什么都没有,用户不知道当前系统是不是正常;
- 空闲,意味着界面明确告诉用户:没有进行中的任务,但系统是准备好的。
所以我在第五篇里反复强调 IDLE,并让手机端页面明确显示“当前无传输任务”。这不是字面小改动,而是把页面从“消失”变成“可理解”。
2)最近结果限制长度,不让状态页越用越重
我在 pushRecentResult() 里保留了截断逻辑,只保存最近 20 条结果。原因很现实:如果把状态中心做成一个无限增长的列表,短时间内看不出问题,后面一定会越来越臃肿。
Demo 虽然不像正式产品那样需要长期运行,但既然第五篇是在做工程收口,那我就不希望它把问题留给以后。限制长度是一个很简单的动作,却能让页面、内存和写作叙述都更干净。
3)清理动作尽量可见,而不是悄悄发生
资源释放本身用户不一定看得见,但我还是希望它是“可感知”的。比如状态页里能看到清理状态、日志里能看到 release 记录、验收清单里能看到资源释放通过。这些信息并不是为了让页面更热闹,而是为了让“清理完成”成为一个能被验证的事实。
如果一切清理都静默完成,技术上也许没问题,但文章层面就少了证据,读者也更难建立信任。
4)日志语义尽量和正文表述保持一致
我这几篇一直在刻意做一件事:正文里怎么说,日志里尽量就怎么表达。正文说“状态同步”,日志就打印 state consistency;正文说“资源释放”,日志就打印 resource release;正文说“最终验收”,日志就打印 final acceptance。
这件事看起来很小,但很有用。因为读者在图里看到一行日志时,不需要再额外做一次语义映射,他会自然知道这行日志对应文章哪一段描述。技术文章如果能做到这一点,阅读成本会低很多。
十五、写到第五篇,我觉得 SlideDrop 真正完成的不是一个功能,而是一种讲清楚工程的方式
这套五连载一开始,我的想法其实没有那么复杂。看到 HarmonyOS 7 的精准碰一碰能力,我只是觉得很适合写一个和演示文稿素材插入相关的小 Demo。它天然有画面感,也天然适合做文章。
但一路写到第五篇,我慢慢发现,真正让我想继续写下去的,不只是它的“新”,而是它很适合把一个工程过程讲清楚。
从第一篇到第五篇,Demo 一直在变:
- 一开始只是把链路跑通;
- 后来开始关心落位和规则;
- 再往后开始关心异常和恢复;
- 最后终于开始关心同步、清理和验收。
这其实就是一个很典型的工程成长路径。你会发现,真正决定一个 Demo 有没有味道的,往往不是第一步把功能做出来,而是后面有没有继续把边界、状态、清理和闭环补完整。
所以如果说第五篇和前几篇最大的不同是什么,我会说:它开始让整套连载带上一点“作品感”。
所谓作品感,不是页面多华丽,也不是功能堆得多满,而是你能从头到尾把这件事讲顺——它为什么存在,它解决了什么,它在异常下怎么处理,它结束以后如何回到稳定起点。这样读者读完以后,记住的就不只是“精准碰一碰能传文件”,而是“我看到了一个 Demo 是怎么一步步被打磨出来的”。
对我来说,这反而是第五篇最有价值的部分。
更多推荐



所有评论(0)