写笔记写到一半要出门手机一划就转到平板继续——不是云同步那种保存-重新打开而是真正的无缝接续页面栈、滚动位置、输入内容全部原样恢复。HarmonyOS NEXT 的应用接续Continuation就是干这个的。这篇把 onContinue 保存、onCreate 恢复、continuable 配置的完整链路讲清楚。应用接续概览核心概念onContinue——源端手机保存迁移数据写入 wantParamonCreate/onNewWant——对端平板接收迁移数据恢复 UIcontinuable——module.json5 中声明 Ability 支持接续LaunchReason.CONTINUATION——对端通过启动原因判断是接续还是正常启动import{UIAbility,AbilityConstant,Want}fromkit.AbilityKit配置 continuablemodule.json5{ module: { name: entry, type: entry }, abilities: [ { name: EntryAbility, continuable: true, launchType: singleton } ] }要点continuable: true 是必须配置的否则系统不会触发接续流程。launchType 建议用 singleton避免对端出现多实例。源端onContinue 保存数据用户发起迁移时源端 UIAbility 的 onContinue 被触发exportdefaultclassEntryAbilityextendsUIAbility{privatecontent:stringprivatescrollY:number0privateeditMode:stringonContinue(wantParam:Recordstring,Object):AbilityConstant.OnContinueResult{// 保存业务数据到 wantParamwantParam[content]this.content wantParam[scrollY]this.scrollY.toString()wantParam[editMode]this.editMode wantParam[lastEditTime]newDate().toLocaleString()console.info(onContinue: 数据已保存)// 返回 AGREE 表示同意迁移returnAbilityConstant.OnContinueResult.AGREE// 返回 REJECT 表示拒绝迁移如数据未保存// return AbilityConstant.OnContinueResult.REJECT// 返回 MISMATCH 表示版本不匹配// return AbilityConstant.OnContinueResult.MISMATCH}}要点wantParam 的 value 必须是 Object 类型string、number 等复杂对象需要 JSON.stringify 序列化。返回 AGREE 才会继续迁移流程。对端onCreate 恢复数据对端设备的 UIAbility 被启动launchReason 为 CONTINUATIONexportdefaultclassEntryAbilityextendsUIAbility{privatecontent:stringprivatescrollY:number0onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{// 判断是否为接续启动if(launchParam.launchReasonAbilityConstant.LaunchReason.CONTINUATION){// 从 want.parameters 恢复数据if(want.parameters!undefined){letparams:Recordstring,Objectwant.parametersthis.contentparams[content]asstringletscrollYStr:stringparams[scrollY]asstringthis.scrollYparseInt(scrollYStr,10)}console.info(onCreate: 接续数据已恢复)}else{// 正常启动console.info(onCreate: 正常启动)}}}要点必须通过 launchReason 判断是否为接续启动否则正常启动也会走恢复逻辑。want.parameters 和源端 onContinue 的 wantParam 是同一份数据。对端onRestoreData 恢复 UI可选如果需要在恢复后做 UI 特定操作onRestoreData(wantParam:Recordstring,Object):void{// 已废弃建议在 onCreate 中处理// 此回调仅作兼容保留}要点新版本推荐在 onCreate 中直接恢复onRestoreData 已废弃。完整迁移流程源端(手机) 对端(平板) │ │ │ 用户点击迁移按钮 │ │ │ │ onContinue() │ │ ├─ 保存数据到wantParam │ │ └─ return AGREE │ │ │ │ ───── 系统传输数据 ──────→ │ │ │ │ onCreate(want, launchParam) │ ├─ launchReason CONTINUATION │ ├─ 从want.parameters恢复数据 │ └─ 重建UI │ │ │ 源端onStoreData() │ │ └─ 可选保存源端状态 │ │ │ │ onWindowStageRestore() │ └─ 恢复窗口内容页面级数据恢复Ability 层恢复数据后需要传递给页面// EntryAbility.etsonCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{if(launchParam.launchReasonAbilityConstant.LaunchReason.CONTINUATION){letparams:Recordstring,Objectwant.parametersasRecordstring,Object// 通过 AppStorage 传递给页面AppStorage.setOrCreate(continuation_content,params[content]asstring)AppStorage.setOrCreate(continuation_scrollY,params[scrollY]asstring)}}// 页面中接收StorageLink(continuation_content)content:stringStorageLink(continuation_scrollY)scrollYStr:string0要点Ability 和 Page 是两个不同的上下文通过 AppStorage 或 LocalStorage 桥接数据。大数据迁移策略wantParam 有大小限制约 100KB大数据需要用文件或数据库中转onContinue(wantParam:Recordstring,Object):AbilityConstant.OnContinueResult{// 小数据直接放 wantParamwantParam[title]this.title wantParam[cursorPos]this.cursorPos.toString()// 大数据写入临时文件wantParam 只传文件路径if(this.largeData.length50000){letfilePath:stringthis.getContext().tempDir/continuation_data.jsonfs.writeTextSync(filePath,JSON.stringify(this.largeData))wantParam[dataFilePath]filePath wantParam[hasLargeData]true}else{wantParam[content]this.largeData wantParam[hasLargeData]false}returnAbilityConstant.OnContinueResult.AGREE}要点系统会自动传输源端的文件目录files/temp/cache到对端所以文件路径中转是可行的。迁移失败处理onContinue(wantParam:Recordstring,Object):AbilityConstant.OnContinueResult{// 有未保存的草稿if(this.hasUnsavedChanges){// 弹窗提示用户returnAbilityConstant.OnContinueResult.REJECT}// 版本不兼容if(this.localVersionthis.targetVersion){returnAbilityConstant.OnContinueResult.MISMATCH}returnAbilityConstant.OnContinueResult.AGREE}要点REJECT 拒绝迁移用户可感知MISMATCH 版本不匹配系统处理AGREE 同意迁移。不要无条件返回 AGREE未保存的数据可能丢失。完整 Demo 代码Demo 模拟了笔记编辑→选择目标设备→迁移流程动画的全过程。interfaceMigrationStep{name:string;status:string;detail:string;}EntryComponentstruct ContinuationDemo{StatemigrationSteps:MigrationStep[][];StatecurrentPage:string笔记内容编辑中...;StatetargetDevice:string;StateisMigrating:booleanfalse;StatemigrateResult:string;StateeditContent:string这是跨设备接续的笔记内容;StatescrollPosition:number0;StateavailableDevices:string[][华为MatePad Pro,华为智慧屏,华为MateBook];build(){Column({space:0}){Row(){Button( 返回).fontSize(14).backgroundColor(Color.Transparent).fontColor(#1a73e8).onClick((){router.back();})Text(应用接续迁移).fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text(this.isMigrating?迁移中:就绪).fontSize(12).fontColor(this.isMigrating?#F44336:#4CAF50)}.width(100%).height(56).padding({left:12,right:12}).alignItems(VerticalAlign.Center).backgroundColor(#FFFFFF)Scroll(){Column({space:16}){Column({space:12}){Text(模拟编辑场景).fontSize(16).fontWeight(FontWeight.Bold).width(100%)Text(手机编辑笔记 → 一键迁移到平板继续).fontSize(13).fontColor(#999999).width(100%)TextArea({text:this.editContent,placeholder:编辑笔记内容...}).width(100%).height(120).onChange((value:string){this.editContentvalue;})Row(){Text(滚动位置: this.scrollPositionpx).fontSize(12).fontColor(#999999).layoutWeight(1)Button(模拟滚动).onClick((){this.scrollPosition256;})}}.width(100%).padding(16).borderRadius(12).backgroundColor(#FFFFFF)Column({space:12}){Text(目标设备).fontSize(16).fontWeight(FontWeight.Bold).width(100%)ForEach(this.availableDevices,(device:string){Row({space:8}){Radio({value:device,group:devices}).onChange((isChecked:boolean){if(isChecked){this.targetDevicedevice;}})Text(device).fontSize(14).layoutWeight(1)}.width(100%).padding(8)},(device:string)device)Button(发起迁移).width(100%).enabled(this.targetDevice.length0!this.isMigrating).onClick((){this.startMigration();})}.width(100%).padding(16).borderRadius(12).backgroundColor(#FFFFFF)if(this.migrationSteps.length0){Column({space:8}){Text(迁移流程).fontSize(16).fontWeight(FontWeight.Bold).width(100%)ForEach(this.migrationSteps,(step:MigrationStep,index:number){Row({space:8}){Text((index1).toString().).fontSize(13).fontWeight(FontWeight.Bold).fontColor(this.getStepColor(step.status)).width(24)Column({space:2}){Text(step.name).fontSize(14).fontWeight(FontWeight.Medium)Text(step.detail).fontSize(12).fontColor(#999999)}.layoutWeight(1).alignItems(HorizontalAlign.Start)Text(step.status).fontSize(11).fontColor(#FFFFFF).backgroundColor(this.getStepColor(step.status)).borderRadius(4).padding({left:6,right:6,top:2,bottom:2})}.width(100%).padding(8).borderRadius(6).backgroundColor(#FAFAFA)},(step:MigrationStep,index:number)${index})}.width(100%).padding(16).borderRadius(12).backgroundColor(#FFFFFF)}if(this.migrateResult){Text(this.migrateResult).fontSize(14).fontColor(#4CAF50).padding(12).borderRadius(8).backgroundColor(#E8F5E9).width(100%)}}.padding(16)}.layoutWeight(1).width(100%)}.width(100%).height(100%).backgroundColor(#F5F5F5)}privategetStepColor(status:string):string{if(status完成)return#4CAF50if(status进行中)return#FF9800return#9E9E9E}privatestartMigration():void{this.isMigratingtruethis.migrationSteps[]this.migrateResultletsteps:MigrationStep[][{name:源端 onContinue,status:等待,detail:保存当前页面状态到wantParam},{name:系统传输数据,status:等待,detail:跨设备传输wantParam和页面栈},{name:对端 onCreate,status:等待,detail:LaunchReason.CONTINUATION触发恢复},{name:对端恢复UI,status:等待,detail:恢复内容和滚动位置}]this.migrationStepsstepsletstepIndex:number0letintervalId:numbersetInterval((){if(stepIndexthis.migrationSteps.length){this.migrationSteps[stepIndex].status进行中setTimeout((){this.migrationSteps[stepIndex].status完成stepIndexif(stepIndexthis.migrationSteps.length){clearInterval(intervalId)this.isMigratingfalsethis.migrateResult迁移完成笔记内容已同步到this.targetDevice}},800)}},1000)this.migrationSteps[0].status进行中setTimeout((){this.migrationSteps[0].status完成this.migrationSteps[0].detail已保存: contentthis.editContent, scrollYthis.scrollPosition stepIndex1},800)}}应用接续 vs 分布式数据对象对比应用接续分布式数据对象触发方式用户主动迁移修改属性自动同步数据流向单向源→对端双向实时同步恢复内容页面栈数据仅数据使用场景换设备继续用多设备实时协同用户体验明确的迁移动画无感知自动同步要点接续是搬家——把整个应用状态搬过去分布式对象是同步——数据实时保持一致。两者可以配合使用。踩坑清单问题原因解决onContinue 不触发continuable 未配置module.json5 设 continuable: true对端 onCreate launchReason 不对对端不是接续启动检查 launchReason 是否为 CONTINUATION恢复数据为空wantParam value 类型不对value 必须是 Object复杂对象 JSON.stringify迁移大图失败wantParam 超过大小限制大数据用文件中转传文件路径对端页面空白数据未传递到 Page通过 AppStorage/LocalStorage 桥接迁移后源端卡住未正确返回 AGREEonContinue 必须返回 OnContinueResult两端版本不同崩溃数据结构不兼容检查版本号返回 MISMATCHsingleton 模式冲突对端已有实例运行singleton 会复用实例onNewWant 接收数据滚动位置恢复不准onWindowStageRestore 时机windowStage 加载完成后再设置滚动网络断开迁移失败蓝牙/WiFi 不稳定迁移前检查网络失败后重试