大家好,我是不想掉发的鸿蒙开发工程师 城中的雾 ,在上一期中,我们理清了 Context 的关系,了解了 ApplicationContext,在 API 10 以前,我们习惯用全局 API(如 router、promptAction)来干这些事。但随着鸿蒙 多窗口 (Multi-Window) 能力的增强,这些全局 API 开始显得"力不从心"------系统不知道你这个弹窗该弹在哪个窗口上,也不知道按照哪个屏幕的密度去计算像素,这一期,我们来聊聊这位专管界面的"管家"UIContext 。
1. 为什么要引入 UIContext?
在早期的鸿蒙开发中,我们习惯这样写代码:
import router from '@ohos.router';
import promptAction from '@ohos.promptAction';
// 旧写法(全局单例)
router.pushUrl({ url: 'pages/Detail' });
promptAction.showToast({ message: 'Hello' });
这种写法有什么问题?
如果你的 App 同时在手机主屏和外接显示器上运行(或者在折叠屏的分屏模式下),这时候有两个窗口。当你调用 router.pushUrl 时,系统会懵圈:"大哥,你到底想在哪个窗口跳转?"
这就是官方文档中反复提到的 "UI 上下文不明确" 问题。
解决方案:UIContext
UIContext 是直接绑定在 WindowStage(窗口舞台)上的。每一个 UIContext 都死死盯着一个具体的窗口。通过它去调用弹窗或路由,系统就非常清楚要把内容画在哪里。

2. 获取方式:this.getUIContext()
在 API 10+ 的 ArkTS 组件 (@Component) 中,获取 UIContext 的标准姿势只有一种:
@Entry
@Component
struct Index {
build() {
Button('点击我')
.onClick(() => {
// ✅ 推荐:直接获取当前组件绑定的 UIContext
let uiContext = this.getUIContext();
// ❌ 不推荐:不要再试图通过 getContext(this) 去强转了
// 虽然也能拿到,但不够直接,而且类型定义可能不全
})
}
}
3. UIContext 的四大核心能力
既然官方强推,那它到底接管了哪些能力?
(1) 弹窗与提示 (PromptAction)
替代全局的 @ohos.promptAction。
let uiContext = this.getUIContext();
let prompt = uiContext.getPromptAction();
// 弹 Toast
prompt.showToast({
message: '操作成功',
duration: 2000
});
// 弹 Dialog
prompt.showDialog({
title: '提示',
message: '确定要删除吗?',
buttons: [
{ text: '取消', color: '#000000' },
{ text: '确定', color: '#FF0000' }
]
});
(2) 路由跳转 (Router)
替代全局的 @ohos.router。
let uiContext = this.getUIContext();
let router = uiContext.getRouter();
router.pushUrl({
url: 'pages/SecondPage'
});
(3) 屏幕尺寸转换 (vp2px)
以前我们要在工具类里算 vp 转 px 很麻烦,现在直接找 UIContext,因为它知道当前屏幕的像素密度(Density)。
let uiContext = this.getUIContext();
// 将 20vp 转换为当前屏幕对应的 px 值
let widthInPx = uiContext.vp2px(20);
let widthInVp = uiContext.px2vp(100);
(4) 浮层管理 (Overlay)
如果你想在当前窗口最上层悬浮一个自定义组件(比如全局 Loading、悬浮球),UIContext 提供了 getOverlay() 能力。
// 这是一个进阶功能,适合做全局自定义弹窗
this.getUIContext().getOverlayManager().addComponentContent(...)
4. 避坑实战:异步回调里的"幽灵"
这是 UIContext 最容易踩的坑。
场景:
用户点击按钮后,发起一个网络请求,3秒后请求回来,弹出一个 Toast。
错误写法:
@Component
struct WrongDemo {
build() {
Button('请求网络')
.onClick(async () => {
// 模拟网络请求
await new Promise(r => setTimeout(r, 3000));
// 风险:如果 3秒内用户把这个页面关了,或者组件销毁了
// this.getUIContext() 可能会失效或报错
this.getUIContext().getPromptAction().showToast({ message: '成功' });
})
}
}
正确写法 (闭包捕获):
在进入异步操作前,先把 uiContext 拿到手里捏死。
@Component
struct RightDemo {
build() {
Button('请求网络')
.onClick(async () => {
// 1. 先保存 UIContext 引用
// 只要这个闭包还在,uiContext 就在,它依然指向那个窗口
let uiContext = this.getUIContext();
await new Promise(r => setTimeout(r, 3000));
// 2. 使用保存的引用
// 即使当前组件销毁了,只要窗口还在,Toast 就能弹出来
try {
uiContext.getPromptAction().showToast({ message: '成功' });
} catch (e) {
// 如果窗口都销毁了,这里会报错,需要捕获
console.error('窗口已销毁,无法弹窗');
}
})
}
}
5. 工具类封装指南
如果你想封装一个 ToastUtils,千万别直接 import 全局的 promptAction。
推荐封装方式:
// ToastUtils.ets
import { common } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export class ToastUtils {
// 方案 A: 每次调用传 context
static show(uiContext: UIContext, msg: string) {
uiContext.getPromptAction().showToast({ message: msg });
}
// 方案 B: 传入 getContext(this) 拿到的 BaseContext,自动转换
// 这种方式兼容性好,但有时候从 BaseContext 转 UIContext 比较绕
/*
static showByBase(context: common.Context, msg: string) {
// 略,比较复杂,建议直接传 UIContext
}
*/
}
调用方:
// Index.ets
Button('工具类弹窗')
.onClick(() => {
ToastUtils.show(this.getUIContext(), '封装成功!');
})
总结:UIContext 使用准则
- 认准入口 :在
.ets组件里,凡是跟 界面显示 有关的(弹窗、路由、尺寸),首选this.getUIContext()。 - 拒绝全局 :尽量少用
@ohos.router和@ohos.promptAction的全局方法,拥抱实例方法。 - 异步锁死 :在
setTimeout或await之前,先let uiContext = this.getUIContext()保存引用,防止this上下文丢失。 - 桥梁作用 :如果需要获取 AbilityContext(比如要停止自身),请使用
this.getUIContext().getHostContext()。
下一期预告:
界面搞定了,业务逻辑怎么办?
- 怎么跳转到别人的 App?
- 怎么申请相机权限?
- 怎么在代码里关闭应用?
📚 充电时间
如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧!初高级证书还没获取的,点这里: