鸿蒙 NEXT 意图框架实战:用 Intent 把应用“长“在一起

一个智能设备真正好用,往往不在于单点能力多强,而在于多个应用之间能不能像人一样"递话"——天气 App 查到暴雨,地图 App 自动建议改道;笔记 App 里写了一个待办,日历 App 立刻建好提醒。
这种"递话"机制,在鸿蒙 NEXT 里由**意图框架(Intents Framework)**统一承载。它和前两篇的 AI 能力正交:AI 负责"理解",意图框架负责"分发"。理解之后必须能落到某个具体的 Ability 上,智能才有意义。下面全部代码基于 Stage 模型、ArkTS + API 12+,可直接在 DevEco Studio 跑通。
一个更具体的画面:用户在日历里写下"周六爬山",系统结合意图框架与设备传感器,隐式唤起一个运动健康 provider 去评估当日天气与空气质量,再把结果以卡片形式回填到日历。整个过程没有任何一方在代码里写死另一方——这正是意图框架想要解耦的状态。当你习惯用"意图"而非"对象引用"去思考应用间的协作,很多原本需要后端联调的跨 App 流程,会天然地前移到端侧完成。
小结:意图框架是鸿蒙 NEXT 的"应用间神经系统",负责把一次意图精准路由到目标 Ability。
一、先想清楚:什么是意图,为什么不是普通跳转
很多开发者第一次接触意图框架,会以为"这不就是页面跳转 + 传参吗?"这是一个危险的误解。普通的 router.pushUrl 只能在本应用内跳,而且目标写死在代码里。意图框架解决的是两个更本质的问题:
第一,解耦调用方与实现方。调用方只描述"我想查北京天气",不关心到底是哪个 App、哪个 Ability 来处理。系统根据已安装应用的声明,动态匹配最合适的目标。新增一个天气 provider,调用方一行代码都不用改。
第二,跨应用与跨设备。意图是系统级的中立契约,天然支持从一个应用拉起另一个应用,也支持把意图投送到附近设备上的 Ability。这正是鸿蒙"全场景"叙事的技术底座。
用一个比喻:普通跳转像你直接拨某个员工的私人手机号;意图框架像你给公司总机说"我要报修水管",总机帮你转到当天值班的维修工——维修工换人了,你不用重存号码。
一句话区分:跳转是"我知道你要去哪",意图是"我只说我要干嘛"。
二、核心四件套:Intent、Want、Ability、AbilityFilter
要写好意图框架,必须先把四个概念的关系理顺。它们职责不同,但环环相扣:
- Intent(意图):抽象层面的"用户想做什么",由
action(动作)+entity(实体类别)+uri(可选的数据定位)三者共同描述。它不直接携带数据,而是描述"一类"能力。 - Want:意图框架的"信封"。它是
Intent的运行时实例,除了上述匹配字段,还用parameters携带这次调用具体的业务参数(如城市名、经纬度)。同时 Want 也负责显式指定目标(bundleName+abilityName)。 - Ability:实际干活的组件。在意图框架语境里,主要指
UIAbility,它既可以是带界面的页面,也可以是"启动即执行、执行完就退"的隐式入口。 - AbilityFilter:Ability 在
module.json5里声明的"接单条件"。系统拿到一个 Want 后,会拿它去和所有已安装 Ability 的 filter 做匹配,命中谁就路由给谁。
理解这四者的关系,有一个心法:把 Intent 当成"动词短语",把 Want 当成"填好参数的工单",把 Ability 当成"能执行该动词的工人",把 AbilityFilter 当成"工人门口挂的岗位职责牌"。系统就是那个"调度中心",读工单、对照职责牌、派单。这个心智模型一旦建立,后面所有 API 都只是它的具象化。
匹配规则分两类,务必记牢:
- 显式匹配:Want 里写了
bundleName和abilityName,系统直接定位,跳过 filter 匹配。适合"我就是要点名某个 App"的场景。 - 隐式匹配:Want 只写
action/entity/uri,系统遍历所有 Ability 的skills配置,满足action与entity命中、uri符合约束,才算匹配成功。适合"谁行谁上"的开放场景。
小结:Intent 描述"想干嘛",Want 是带数据的信封,Ability 是干活的人,AbilityFilter 是它的接单条件。
三、声明意图:在 module.json5 里"挂牌接单"
意图框架的第一个动作,是让目标 Ability 在配置文件里声明自己能接什么单。位置在 src/main/module.json5 的 abilities[].skills 数组。下面我们做一个"天气查询"的 provider,让它既能处理显式拉起,也能响应隐式意图。
{
"module": {
"name": "entry",
"type": "entry",
"deviceTypes": ["phone", "tablet", "pc"],
"abilities": [
{
"name": "WeatherQueryAbility",
"srcEntry": "./ets/abilities/WeatherQueryAbility.ts",
"description": "$string:WeatherQueryAbility_desc",
"icon": "$media:icon",
"label": "$string:WeatherQueryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"actions": [
"ohos.want.action.queryWeather",
"ohos.want.action.viewData"
],
"entities": [
"entity.weather.query",
"entity.system.browsable"
],
"uris": [
{
"scheme": "weather",
"host": "query",
"path": "city"
}
]
}
]
}
]
}
}
这里有几个关键点,很多人第一次写会踩坑:
exported: true 是跨应用意图的前提。如果忘了开,别的 App 根本拉不起你,系统会直接报 Ability not exportable。但开了之后要注意:任何能匹配到你 filter 的应用都能拉起你,所以敏感操作必须在 Ability 内部再做权限校验。
actions 和 entities 是自己定义的字符串,约定俗成用反向域名风格(如 ohos.want.action.queryWeather),避免和别的 App 撞车。系统自带的 action 也有(如 ohos.want.action.viewData),复用它们能白嫖系统的统一处理。
uris 是可选的精细化匹配。这里我们声明 weather://query/city,意味着只有 uri 符合这个 scheme+host+path 前缀的 Want 才会命中,方便按数据类型分流。
小结:
skills就是你的"服务菜单",exported是"是否对外营业"的开关,二者缺一不可。
四、系统路由:一个 Want 是怎么被送到目标 Ability 的
声明完之后,调用方怎么"递话"?核心就是构建一个 Want,然后交给 UIAbilityContext.startAbility。先看最朴素的显式调用——点名拉起。
// pages/Index.ets
import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct Index {
private context = getContext(this) as common.UIAbilityContext;
// 显式拉起:我知道目标是谁
private startWeatherExplicit(city: string): void {
const want: Want = {
bundleName: 'com.example.weather',
abilityName: 'WeatherQueryAbility',
parameters: { city: city, source: 'homePage', timestamp: Date.now() }
};
this.context.startAbility(want).then(() => {
console.info('[Intent] 显式拉起成功');
}).catch((err: BusinessError) => {
console.error(`[Intent] 拉起失败 code=${err.code} msg=${err.message}`);
});
}
build() {
Column({ space: 16 }) {
Button('查北京天气(显式)')
.onClick(() => this.startWeatherExplicit('北京'))
}
.width('100%')
.padding(24)
}
}

如果是隐式调用,则完全不写 bundleName 和 abilityName,只给 action + entity + uri:
// 隐式拉起:谁接单谁上,系统决定
private startWeatherImplicit(city: string): void {
const want: Want = {
action: 'ohos.want.action.queryWeather',
entity: 'entity.weather.query',
uri: `weather://query/city?name=${city}`,
parameters: {
city: city,
needForecast: true
}
};
this.context.startAbility(want).then(() => {
console.info('[Intent] 隐式路由成功,系统已选中最合适的 provider');
}).catch((err: BusinessError) => {
// 没有任何应用声明能接这个意图时,会走到这里
console.error(`[Intent] 无匹配 provider code=${err.code}`);
});
}
需要强调一个工程细节:startAbility 返回的是 Promise。不要忽略它的 reject。隐式调用最常见的失败就是"装了但没声明 filter"或"uri 写错",此时 reject 会带上 error.code,典型值是 16000050(找不到匹配 Ability)。生产代码务必捕获,并给用户一个"去应用市场搜天气应用"之类的兜底提示,否则界面会像死了一样没反应。
小结:显式是"点名",隐式是"招标";隐式调用必须处理"无人接单"的异常分支。
五、意图处理方:接收、解析、响应 Want
目标 Ability 怎么接到这个 Want?答案是两个生命周期入口:onCreate(want) 与 onNewWant(want)。区别在于:Ability 实例首次创建时走 onCreate;如果实例已经存在(比如应用已在后台),系统不会重启它,而是把新 Want 通过 onNewWant 喂进来。两者都必须处理,少一个就会出现"第二次点进来参数没更新"的经典 bug。
下面给出完整的 WeatherQueryAbility,它把接收到的参数解析出来,存进一个全局单例,再拉起真正的 UI 页面。
// ets/abilities/WeatherQueryAbility.ts
import { UIAbility, Want, AbilityConstant, type AbilityStage } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
// 简单的意图参数容器,跨页面共享
export class IntentPayload {
static city: string = '北京';
static source: string = '';
static needForecast: boolean = false;
}
const DOMAIN: number = 0x6677;
export default class WeatherQueryAbility extends UIAbility {
// 首次创建:冷启动路径
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, 'WeatherAbility', 'onCreate, want=%{public}s', JSON.stringify(want));
this.parseWant(want);
}
// 实例已存在,携带新意图再次进入:热路径
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, 'WeatherAbility', 'onNewWant, want=%{public}s', JSON.stringify(want));
this.parseWant(want);
}
private parseWant(want: Want): void {
// 1. 优先从 parameters 取结构化参数
const params = want.parameters;
if (params) {
IntentPayload.city = (params['city'] as string) ?? IntentPayload.city;
IntentPayload.source = (params['source'] as string) ?? '';
IntentPayload.needForecast = (params['needForecast'] as boolean) ?? false;
}
// 2. 兜底:从 uri 里解析(隐式调用常走这里)
const uri = want.uri;
if (uri && uri.startsWith('weather://')) {
const url = new URL(uri);
const name = url.searchParams.get('name');
if (name) {
IntentPayload.city = name;
}
}
hilog.info(DOMAIN, 'WeatherAbility',
'parsed city=%{public}s source=%{public}s', IntentPayload.city, IntentPayload.source);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// 用同一个页面入口,页面内部读取 IntentPayload 决定展示内容
windowStage.loadContent('pages/WeatherResult', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'WeatherAbility', 'loadContent failed: %{public}s', err.message);
}
});
}
}
注意 parseWant 里做了双通道解析:既读 parameters,也兜底解析 uri。原因是隐式调用方可能两种都给,也可能只给一种。健壮的处理方不该假设调用方只走某条路。另外,IntentPayload 用静态字段做跨页面共享,是因为 Ability 与页面不在同一实例层级,windowStage.loadContent 之后页面需要通过某种方式拿到参数,静态容器是最轻量的方案(更严谨地可用 AppStorage 或全局单例类)。
小结:处理方必须同时实现
onCreate与onNewWant,并兼容parameters与uri两种参数来源。
六、跨应用意图流转:AbilityFilter 与权限边界
前面都是"自己拉自己"。真正的威力在跨应用。假设我们另有一个"出行规划"App,它想查天气却不想自己实现,就调用上面那个天气 provider 的隐式意图。这里引出三个工程要点。
其一,跨应用调用无需特殊权限,但目标必须 exported: true。 这一点前面提过,是硬门槛。
其二,参数不要塞敏感数据。 Want 在跨进程传递时是序列化的,理论上同设备应用间可见。城市名、商品 ID 这类公开参数没问题,但 token、手机号、定位坐标这类应谨慎,必要时通过分布式数据或后台接口二次核验。
其三,用 startAbilityForResult 拿到对方的回执。 很多场景是"调用方出题目,处理方给答案",比如出行 App 问天气 App"北京明天适不适合出游",需要返回值。这就要用 startAbilityForResult,并在处理方用 terminateSelfWithResult 回传。
下面是调用方(出行 App)的代码:
// 出行 App:发起跨应用查询并等待回执
private queryWeatherForResult(city: string): void {
const want: Want = {
action: 'ohos.want.action.queryWeather',
entity: 'entity.weather.query',
parameters: {
city: city,
needForecast: true,
requestId: `${Date.now()}`
}
};
this.context.startAbilityForResult(want).then((result) => {
const data = result.want?.parameters;
const suitable = data?.['suitableForTravel'] as boolean;
const temp = data?.['temperature'] as number;
console.info(`[Travel] 收到天气回执:适合出游=${suitable} 温度=${temp}℃`);
}).catch((err: BusinessError) => {
console.error(`[Travel] 跨应用查询失败 code=${err.code}`);
});
}
处理方(天气 provider)在算出结果后回传:
// 天气 provider 内部,计算完成后回写结果
private replyToCaller(context: UIAbilityContext, city: string, temp: number, suitable: boolean): void {
const resultWant: Want = {
parameters: {
city: city,
temperature: temp,
suitableForTravel: suitable
}
};
// 第二个参数是回执码,调用方在 result.resultCode 里拿到
context.terminateSelfWithResult({ want: resultWant, resultCode: 0 });
}
这里有个易错点:startAbilityForResult 的隐式匹配,如果设备上有多个应用都能接这个意图,系统会弹出让用户选择,或按优先级选一个。若你要求稳定命中特定 provider,最稳妥还是回到显式调用(带 bundleName)。隐式适合"开放生态、用户可选",显式适合"强绑定、稳定调用"。
小结:跨应用靠
exported+ 隐式 filter;要回执用startAbilityForResult/terminateSelfWithResult配对。
七、意图与元服务联动:免安装的原子化能力
鸿蒙 NEXT 的**元服务(Atomic Service)**是免安装、即用即走的应用形态。它本质上也是一个 Ability,只是打包与分发方式不同——没有传统的"下载安装"环节,用户通过碰一碰、扫一扫、服务发现即可拉起。意图框架与元服务是天然搭档:你完全可以用上面同一套 Want 机制拉起一个元服务,代码几乎不变。
区别在于元服务的 module.json5 多了 installationFree: true 标记,并且 abilities 通常声明 type: "page" 且 visible 受控。拉起元服务的代码长这样:
// 拉起一个"快递查询"元服务(免安装)
private openExpressMetaService(trackingNo: string): void {
const want: Want = {
bundleName: 'com.example.express.meta', // 元服务的 bundleName
abilityName: 'ExpressAbility',
parameters: {
trackingNo: trackingNo
}
};
this.context.startAbility(want).then(() => {
console.info('[Meta] 元服务已拉起,无需安装');
}).catch((err: BusinessError) => {
// 元服务未预置且无法按需获取时,引导用户去服务中心
console.error(`[Meta] 元服务不可用 code=${err.code}`);
});
}
为什么这种联动值得强调?因为它把"意图框架"从"应用之间"扩展到了"服务之间"。设想一个智能场景:语音助手(前两篇已经跑通)识别出"帮我查这个快递",它产出一条意图,意图框架把它路由到快递元服务,整个过程用户零安装、零等待。AI 理解、意图分发、原子化服务三者串联,才是鸿蒙"全场景智能"的完整闭环。
需要提醒的是:元服务对包体积、启动性能有更苛刻的要求,且部分系统能力受 visible 管控。把它当成"轻量、单一职责、秒开"的组件来设计,而不是把整个 App 塞进去。
小结:元服务是 Ability 的免安装形态,用同一套 Want 拉起;它是意图框架在"服务级"的延伸。
八、端到端示例:一个最小可运行的意图链路
前面七节是拆开讲的,现在把它们拼回一条完整的链路,方便你对照着把工程搭起来。这里刻意保留"发送方"与"接收方"分属两个关注点的结构:发送方只关心"我要发起什么意图",接收方只关心"我接到意图后怎么办",二者通过 module.json5 的 skills 这个唯一契约点耦合。只要这份契约对得上,两边可以独立迭代、独立发版。
下面是"发送方页面 + 接收方 Ability + 结果页"三件套的收口代码,足以在 DevEco Studio 新建的 API 12+ 工程里直接跑。
发送方页面(放在任意 entry 应用):
// pages/Sender.ets
import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct Sender {
private context = getContext(this) as common.UIAbilityContext;
@State tip: string = '点击下方按钮发起意图';
build() {
Column({ space: 20 }) {
Text(this.tip).fontSize(16)
Button('隐式查询北京天气').onClick(() => {
const want: Want = {
action: 'ohos.want.action.queryWeather',
entity: 'entity.weather.query',
uri: 'weather://query/city?name=北京',
parameters: { city: '北京', needForecast: true }
}; // 隐式:不指定 bundleName,由系统路由
this.context.startAbility(want)
.then(() => this.tip = '已发起意图,等待系统路由')
.catch((e: BusinessError) => this.tip = `失败:${e.message}`);
})
Button('显式拉起天气 provider').onClick(() => {
const want: Want = {
bundleName: 'com.example.weather',
abilityName: 'WeatherQueryAbility',
parameters: { city: '上海' }
}; // 显式:点名拉起,稳定命中
this.context.startAbility(want)
.then(() => this.tip = '已显式拉起')
.catch((e: BusinessError) => this.tip = `失败:${e.message}`);
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
接收方结果页(天气 provider 内部):
// pages/WeatherResult.ets
import { IntentPayload } from '../abilities/WeatherQueryAbility';
@Entry
@Component
struct WeatherResult {
@State city: string = IntentPayload.city;
build() {
Column({ space: 14 }) {
Text(`天气结果页`).fontSize(22).fontWeight(FontWeight.Bold)
Text(`城市:${this.city}`).fontSize(18)
Text(`来源:${IntentPayload.source || '直接启动'}`).fontSize(14)
Text(IntentPayload.needForecast ? '已请求未来预报' : '仅当前天气')
.fontSize(14)
.fontColor('#666')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
把这两段与第三节的 module.json5、第五节的 WeatherQueryAbility 组合,就是一个能跑的意图链路:点按钮 → 系统路由 → 天气 Ability 解析参数 → 展示结果页。想验证隐式匹配,先装天气 provider 再点"隐式查询";想验证跨应用,把发送方页面放到另一个 bundle 里即可。
小结:链路口诀——发方建 Want、配置挂 skills、收方解 onCreate/onNewWant、页面读参数。
九、调试技巧与三个常见坑
写意图框架,调试比写更费时间。分享三条实战经验。
坑一:隐式意图"怎么都拉不起"。 九成是 exported 没开,或 action/entity 字符串大小写、拼写和调用方不一致。建议双方把 action 抽成一个共享常量文件,或至少在文档里对齐。DevEco Studio 的"Device File Browser"能看到已安装 Ability 的 filter,是排查利器。
坑二:第二次进来参数不更新。 这是只实现了 onCreate 忘了 onNewWant。Ability 默认 launchType 是 standard,实例复用,新 Want 只走 onNewWant。要么两个入口都解析,要么把 Ability 的 launchType 设为 singleton 并在 onNewWant 里处理。
坑三:Uri 匹配失败。 uris 的匹配是 scheme+host+path 的前缀匹配,不是正则。写 path: "city" 时,weather://query/city/beijing 能命中,但 weather://query/cities 不会。需要通配就用 pathStartWith 或只配 scheme+host。
调试时多打 hilog,把 Want 的 action、entity、uri、parameters 全量打印出来对比,问题往往一目了然。
一句话收尾:意图框架不难,难的是把"解耦"这件事在配置和生命周期两个层面都做扎实。
结语
意图框架是鸿蒙 NEXT 把"单点智能"编织成"系统智能"的关键纽带。它用 Intent 描述能力、Want 承载调用、AbilityFilter 完成解耦路由,再借元服务的免安装形态把能力下沉到服务级。前两篇我们让设备学会了看与听,这一篇我们让应用之间学会了"说话"。当 AI 理解、意图分发、原子化服务三者打通,全场景智能才真正从口号变成体验。
更多推荐


所有评论(0)