👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 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返回数据
正常选中0city = 深圳
用户取消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() 本身并不复杂,真正需要设计清楚的是调用双方之间的结果协议。

对于“打开一个独立选择页面,选完再回到原页面”的场景,可以抓住四件事:

  1. A 侧用 startAbilityForResult(),而不是普通 startAbility();
  2. 启动数据放进启动 Want,B 可以在 UIAbility 的 onCreate() 中接收;
  3. B 侧完成选择后通过 terminateSelfWithResult() 返回 AbilityResult;
  4. resultCode 描述成功、取消或异常,真正的业务数据放进返回 Want 的 parameters。

HarmonyOS 7.0 / API 26.0.0 仍然支持这套能力,而且它不是 HarmonyOS 7 才加入的新接口。对于现有工程升级到 HarmonyOS 7,更应该关注工程 SDK 配套关系和组件启动规则,而不是重新设计选择页面的数据回传方式。

如果项目里已经有多个“选择页面”,可以顺手检查一下:现在这些页面是在各自维护一套返回规则,还是已经统一成了一个明确的 resultCode + Want.parameters 协议。后者通常更容易维护和排查。

📝 写在最后

如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!

我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!

感谢你的阅读,我们下篇文章再见~👋

✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐