HarmonyOS 页面选完数据怎么返回?startAbilityForResult 实战【鸿蒙心迹】

👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀
前言
很多页面跳转只需要“打开下一页”,但选择类业务不一样。
比如首页打开城市选择页,用户选中“深圳”后,城市选择页不仅要关闭,还要把“深圳”传回原来的页面;联系人选择、地址选择、文件类型选择,本质上都是同一类交互。
这种场景如果只用 startAbility(),拉起动作本身没有“等待目标 UIAbility 返回业务结果”的语义。HarmonyOS 的 UIAbilityContext 提供了 startAbilityForResult():调用方拉起目标 UIAbility 后,可以在目标 UIAbility 结束时获得 AbilityResult;目标方则可以使用 terminateSelfWithResult() 在退出时把 resultCode 和数据一并带回。
这次就用一个最小“城市选择”示例,把这条链路完整走一遍。
一、为什么这里不直接用 startAbility
先区分两个接口的目的。
startAbility() 的 Promise 返回类型是 Promise<void>,它关心的是“UIAbility 是否成功发起启动”;startAbilityForResult() 的 Promise 返回类型则是 Promise<AbilityResult>,它还会等待目标 UIAbility 退出并接收返回数据。
所以:
普通页面跳转:
A ── startAbility ──> B
选择并返回:
A ── startAbilityForResult ──> B
A <── AbilityResult ───────── B
这两个 API 看起来只差了几个单词,但业务语义完全不同。
城市、联系人、地址、文件类型这类“进入一个独立页面完成一次选择,然后把结果交还调用方”的流程,更适合第二种方式。
二、先把版本和接口边界弄清楚
截至当前华为官方文档,HarmonyOS 7.0 对应的 API 版本为 26.0.0。华为在 26.0.0 起调整了 API 版本号格式,并在 HarmonyOS 7.0 升级适配说明中明确给出了两者的对应关系。
本文按照当前 HarmonyOS 7.0 / API 26.0.0 文档核对接口,但 startAbilityForResult() 并不是 HarmonyOS 7 才出现的新能力。
当前 UIAbilityContext 文档明确说明:
UIAbilityContext属于 Ability Kit;- Stage 模型下使用;
- 模块首批接口从 API version 9 开始支持;
startAbilityForResult()和terminateSelfWithResult()都只能在主线程调用;- 系统能力为
SystemCapability.Ability.AbilityRuntime.Core; - 元服务从 API version 11 开始也可以使用相关接口。
AbilityResult 则包含两个与本文直接相关的成员:
resultCode: number
want?: Want
正常情况下,resultCode 是目标方主动返回的结果码;异常情况下官方定义会返回 -1。want 是可选项,用于携带返回数据。
本文只演示同一应用内两个 UIAbility 的显式拉起,不额外引入权限。跨应用场景还要继续检查目标组件可见性、权限以及 Stage 模型组件启动规则,不能把同应用示例直接等价为任意跨应用拉起。startAbilityForResult() 本身也定义了诸如目标 Ability 不存在、组件不可见、权限不足等错误码。
三、搭一个最小实践:城市选择页
工程中准备两个 UIAbility:
EntryAbility
└── pages/Index.ets
CityPickerAbility
└── pages/CityPickerPage.ets
流程很简单:
1. Index 页面点击“选择城市”
2. EntryAbility 通过 startAbilityForResult 拉起 CityPickerAbility
3. CityPickerAbility 接收启动 Want
4. 用户点击“深圳”
5. CityPickerAbility 调用 terminateSelfWithResult
6. resultCode = 0,Want.parameters 中携带 city
7. Index 页面拿到 AbilityResult 并更新界面
为了同时演示取消,再约定两个业务结果码:
const RESULT_OK: number = 0;
const RESULT_CANCEL: number = 1;
这里刻意不使用 -1 作为业务取消码,因为官方已经把异常退出时的 resultCode = -1 定义为异常结果。
3.1 配置第二个 UIAbility
首先需要让系统知道 CityPickerAbility 的存在。
在现有 module.json5 的 abilities 中加入一个 UIAbility:
{
"module": {
// 省略工程已有配置
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true
},
{
"name": "CityPickerAbility",
"srcEntry": "./ets/citypickerability/CityPickerAbility.ets",
"exported": false
}
]
}
}
abilities 中的 name 对应后面 Want 的 abilityName,srcEntry 指向 UIAbility 的 ArkTS 入口文件。官方 module.json5 示例同样使用 abilities、name、srcEntry、exported 描述 UIAbility。
这里的选择页只供当前应用内部使用,因此示例将 exported 设为 false。
页面配置文件中同时加入选择页,例如:
{
"src": [
"pages/Index",
"pages/CityPickerPage"
]
}
四、A 页面:发起 startAbilityForResult
A 页面真正需要关心的不是“怎么跳过去”,而是“跳过去以后等待什么结果”。
Index.ets 可以这样组织:
import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
const RESULT_OK: number = 0;
const RESULT_CANCEL: number = 1;
@Entry
@Component
struct Index {
@State selectedCity: string = '尚未选择';
private async openCityPicker(): Promise<void> {
const context =
this.getUIContext().getHostContext() as common.UIAbilityContext;
const want: Want = {
bundleName: 'com.example.resultdemo',
abilityName: 'CityPickerAbility',
parameters: {
scene: 'city-picker'
}
};
try {
const result: common.AbilityResult =
await context.startAbilityForResult(want);
if (result.resultCode === RESULT_OK) {
const city = result.want?.parameters?.['city'];
if (typeof city === 'string') {
this.selectedCity = city;
}
return;
}
if (result.resultCode === RESULT_CANCEL) {
console.info('User canceled city selection.');
return;
}
if (result.resultCode === -1) {
console.error('CityPickerAbility exited abnormally.');
return;
}
console.info(`Unknown resultCode: ${result.resultCode}`);
} catch (error) {
const err = error as BusinessError;
console.error(
`startAbilityForResult failed, code=${err.code}, message=${err.message}`
);
}
}
build() {
Column({ space: 16 }) {
Text(`当前城市:${this.selectedCity}`)
.fontSize(20)
Button('选择城市')
.onClick(() => {
this.openCityPicker();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
这里有三个关键点。
第一个是页面中获取的必须是当前页面对应的 UIAbilityContext。当前 ArkUI 文档推荐在页面侧通过 getUIContext().getHostContext() 获取 Ability Context,以避免多实例环境下使用无明确归属的全局上下文。
第二个是 Want。
Want 是组件之间传递信息的载体。启动 UIAbility 时,bundleName 和 abilityName 用来指定目标组件,而 parameters 可以携带业务参数。
所以这里传入:
parameters: {
scene: 'city-picker'
}
它属于 A → B 的启动参数。
第三个才是 await context.startAbilityForResult(want)。
这一行不会只得到“启动成功”这么简单的结果,而是在目标 UIAbility 结束后得到 AbilityResult。这也是整个选择页模式成立的核心。
五、B Ability:接收启动参数
目标 UIAbility 在创建时可以通过 onCreate() 参数取得启动 Want。
CityPickerAbility.ets:
import {
AbilityConstant,
UIAbility,
Want
} from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class CityPickerAbility extends UIAbility {
onCreate(
want: Want,
launchParam: AbilityConstant.LaunchParam
): void {
const scene = want.parameters?.['scene'];
if (typeof scene === 'string') {
console.info(`CityPickerAbility scene: ${scene}`);
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/CityPickerPage');
}
}
官方开发资料明确展示了 UIAbility 可以在 onCreate(want) 中读取 want.parameters 中的启动数据。
这里也能看出一个很实用的边界:
A → B:
读取启动时传入的 Want
B → A:
通过 AbilityResult.want 返回
虽然两边都使用 Want,但方向不同,不要把“启动参数”和“返回参数”混成一份状态。
六、B 页面:选择完成后返回业务数据
接下来是闭环中最关键的一步。
用户选择城市后,不是简单执行 terminateSelf(),而是调用:
terminateSelfWithResult()
CityPickerPage.ets:
import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
const RESULT_OK: number = 0;
const RESULT_CANCEL: number = 1;
@Entry
@Component
struct CityPickerPage {
private async finishWithCity(city: string): Promise<void> {
const context =
this.getUIContext().getHostContext() as common.UIAbilityContext;
const resultWant: Want = {
bundleName: 'com.example.resultdemo',
abilityName: 'EntryAbility',
parameters: {
city: city
}
};
const abilityResult: common.AbilityResult = {
resultCode: RESULT_OK,
want: resultWant
};
try {
await context.terminateSelfWithResult(abilityResult);
} catch (error) {
const err = error as BusinessError;
console.error(
`terminateSelfWithResult failed, code=${err.code}, message=${err.message}`
);
}
}
private async cancel(): Promise<void> {
const context =
this.getUIContext().getHostContext() as common.UIAbilityContext;
const abilityResult: common.AbilityResult = {
resultCode: RESULT_CANCEL
};
try {
await context.terminateSelfWithResult(abilityResult);
} catch (error) {
const err = error as BusinessError;
console.error(
`cancel failed, code=${err.code}, message=${err.message}`
);
}
}
build() {
Column({ space: 12 }) {
Text('选择城市')
.fontSize(24)
Button('北京')
.onClick(() => {
this.finishWithCity('北京');
})
Button('上海')
.onClick(() => {
this.finishWithCity('上海');
})
Button('深圳')
.onClick(() => {
this.finishWithCity('深圳');
})
Button('取消')
.onClick(() => {
this.cancel();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
官方对 terminateSelfWithResult() 的定义非常明确:它会销毁当前 UIAbility;如果当前 UIAbility 原本就是通过 startAbilityForResult() 拉起的,则把传入的 AbilityResult 返回调用方。反过来说,如果目标 UIAbility 不是通过 startAbilityForResult() 拉起的,即使调用 terminateSelfWithResult(),也不会形成本文这条结果返回链路。
七、resultCode 和业务数据最好分开表达
这个地方比较容易理解错。
不要让 resultCode 同时承担大量业务数据。
更清晰的方式是:
resultCode
负责描述这次交互“发生了什么”
Want.parameters
负责描述这次交互“返回了什么”
例如:
| 情况 | resultCode | 返回数据 |
|---|---|---|
| 正常选中 | 0 | city = 深圳 |
| 用户取消 | 1 | 不需要 |
| UIAbility 异常退出 | -1 | 按异常处理 |
这样调用方处理结果时非常直观:
if (result.resultCode === RESULT_OK) {
// 解析业务数据
} else if (result.resultCode === RESULT_CANCEL) {
// 用户主动取消
} else if (result.resultCode === -1) {
// 异常结果
}
尤其是 -1,不建议重新拿来定义“用户取消”。
因为按照 AbilityResult 官方定义,异常情况下系统本身就会返回 -1。
八、为什么异常退出也必须单独考虑
很多代码只写这一句:
const result = await context.startAbilityForResult(want);
然后默认“只要 Promise 返回了,就一定是业务成功”。
这个理解不完整。
官方在 startAbilityForResult() 文档中特别说明:目标 UIAbility 被异常销毁时,也会向调用方返回结果,此时 resultCode 为 -1。如果目标 UIAbility 使用单实例模式,并被不同应用多次通过该接口启动,还存在只有最后一个调用方获得正常结果、其他调用方获得 -1 的情况。
因此至少要区分两类失败。
第一类是:
startAbilityForResult 本身调用失败
例如目标 Ability 不存在、不可见、权限或参数有问题。这类问题走 Promise 的 catch。
第二类是:
Ability 已经成功拉起,但之后异常结束
这类情况可能得到一个 AbilityResult,但其中:
result.resultCode === -1
这两条错误路径不是同一回事。
九、取消操作不要和异常混在一起
对于选择页面,“没选数据”不一定表示出错。
用户点取消属于正常业务分支。因此示例专门使用:
const RESULT_CANCEL = 1;
并主动:
await context.terminateSelfWithResult({
resultCode: RESULT_CANCEL
});
这样 A 页面能够明确知道:
不是接口报错
不是 Ability 异常
只是用户不想选了
如果产品还要求把系统返回键也严格视为同一种“业务取消”,则还要结合项目的返回行为统一处理;本文不额外扩展未经验证的返回键实现,而把重点放在 startAbilityForResult / terminateSelfWithResult 这条官方明确支持的结果链路上。
十、封装成一个可复用的“选择页面”模式
当项目中城市、地址、联系人分类、文件类型等页面越来越多时,不建议每个页面都重新发明结果协议。
可以统一约定:
export const RESULT_OK: number = 0;
export const RESULT_CANCEL: number = 1;
再约定每个选择器自己的参数 key:
城市选择:
parameters['city']
地址选择:
parameters['addressId']
联系人选择:
parameters['contactId']
文件类型:
parameters['fileType']
调用模式就会稳定成:
构造 Want
↓
startAbilityForResult()
↓
目标 Ability 展示选择页面
↓
用户完成 / 取消
↓
terminateSelfWithResult()
↓
调用方按 resultCode 分支
↓
再解析 Want.parameters
甚至可以在业务层继续封装成:
async function selectCity(): Promise<string | undefined>
由这个函数内部处理 UIAbilityContext、Want、resultCode 和参数校验,上层页面只关心最终有没有获得城市。
真正值得复用的并不是某一段按钮代码,而是这个请求—选择—结果协议。
十一、容易出错的几个地方
1. abilityName 写错
Want 中的:
abilityName: 'CityPickerAbility'
必须和 module.json5 中声明的 Ability 名称对应。
官方为 startAbilityForResult() 定义了 16000001:指定 Ability 不存在。
排查这类问题时,先核对配置,不要先怀疑 ArkUI 页面。
2. 把 startAbility 当成 startAbilityForResult
如果需求是等待结果,就应该从调用入口开始确定结果模型。
startAbility() 返回 Promise<void>;它不是“先 startAbility,然后想办法从 B 页面回调 A 页面”的简写版本。
3. B 页面只 terminateSelf,没有返回结果
terminateSelf() 的目标只是结束 UIAbility。
需要把选择结果一起交回调用方时,应使用 terminateSelfWithResult()。
4. 不检查 want 是否存在
AbilityResult.want 在接口定义中是可选的。
所以不要直接:
result.want.parameters['city']
更稳妥的是:
const city = result.want?.parameters?.['city'];
if (typeof city === 'string') {
// 使用数据
}
业务结果码正确,不等于业务参数一定完整。
5. 把所有非 0 结果都当接口异常
resultCode 本来就允许目标方返回自己的业务结果。
所以“用户取消”完全可以设计成正常的业务码,不应该和 catch 混为一谈。
十二、实际项目中怎么排查
遇到“页面能打开,但结果回不来”时,可以按下面顺序检查。
第一步:确认调用的是 startAbilityForResult()。
如果入口就是普通 startAbility(),先把结果模型纠正。
第二步:确认目标确实是被这次调用拉起的 UIAbility。
检查 bundleName、abilityName 和 module.json5。
第三步:检查 B 侧是否真正执行了 terminateSelfWithResult()。
只切页面、调用普通退出接口,都不是同一套结果机制。
第四步:检查 AbilityResult.resultCode。
先区分:
0 / 自定义成功码
业务取消码
-1 异常结果
再决定要不要解析 Want。
第五步:检查 result.want?.parameters。
不要把“拿到了 AbilityResult”和“业务字段一定存在”当成同一件事。
第六步:如果 Promise 直接进入 catch,查接口错误码。
例如:
16000001 目标 Ability 不存在
16000004 不能启动不可见组件
16000005 进程权限不满足
401 参数错误
具体错误码和版本范围应继续以当前 UIAbilityContext 官方 API Reference 为准。
开发经验总结
startAbilityForResult() 本身并不复杂,真正需要设计清楚的是调用双方之间的结果协议。
对于“打开一个独立选择页面,选完再回到原页面”的场景,可以抓住四件事:
- A 侧用
startAbilityForResult(),而不是普通startAbility(); - 启动数据放进启动 Want,B 可以在 UIAbility 的
onCreate()中接收; - B 侧完成选择后通过
terminateSelfWithResult()返回AbilityResult; resultCode描述成功、取消或异常,真正的业务数据放进返回 Want 的parameters。
HarmonyOS 7.0 / API 26.0.0 仍然支持这套能力,而且它不是 HarmonyOS 7 才加入的新接口。对于现有工程升级到 HarmonyOS 7,更应该关注工程 SDK 配套关系和组件启动规则,而不是重新设计选择页面的数据回传方式。
如果项目里已经有多个“选择页面”,可以顺手检查一下:现在这些页面是在各自维护一套返回规则,还是已经统一成了一个明确的 resultCode + Want.parameters 协议。后者通常更容易维护和排查。
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。
更多推荐



所有评论(0)