文章目录
-
- 前言
- 一、这次迁移先要改掉全局调用思路
- [二、`UIContext` 在哪里拿,取决于代码站在哪一层](#二、
UIContext在哪里拿,取决于代码站在哪一层) - 三、跨模块和异步调用,别再用单例硬兜
- 四、兼容层可以留,但别把旧接口继续当主路径
- 总结
前言
很多老项目升到鸿蒙 6 API 20 之后,会先看到一条很扎眼的告警,TextPickerDialog.show 已经进入废弃路径。华为开发者文档给出的口径很明确,这个接口从 API version 8 开始支持,从 API version 18 开始废弃,推荐改用 showTextPickerDialog,调用前要先拿到 UIContext。
这次迁移表面上看只是一个弹窗接口换写法,真正牵动的是 UI 调用方式。Stage 模型下,UI 实例和窗口是关联的,UIContext 也跟具体窗口绑定。文档里对这一层关系写得很直接,WindowStage 或 Window 通过 loadContent 加载页面并创建 UI 实例,页面内容渲染到关联窗口上,所以 UI 实例和窗口是一一关联的。

一、这次迁移先要改掉全局调用思路
TextPickerDialog.show 以前用起来很顺手,传入选项、默认值和回调,弹窗就出来了。问题在复杂场景里会开始冒头。窗口变多之后,系统需要知道这个弹窗该挂到哪一个窗口、哪一个 UI 实例上。UIContext.showTextPickerDialog 解决的就是这件事,它要求开发者把上下文交清楚,系统按这个上下文去显示弹窗。
所以这轮迁移里最先要丢掉的习惯,就是把文本选择弹窗当成一个随手能调的全局能力。项目里只要还在沿着这个思路写,异步回调、跨页面调用、多窗口场景都会越来越难控。把显示动作收回到 UIContext 上之后,弹窗归属、渲染位置和后续排查都会清楚很多。
二、UIContext 在哪里拿,取决于代码站在哪一层
如果代码就在自定义组件内部,最直接的方式就是 this.getUIContext()。自定义组件内置方法文档已经把这个能力列出来了,getUIContext 会返回当前组件对应的 UIContext 实例。
这种场景下,迁移代码可以直接写成这样:
typescript
@Entry
@Component
struct PickerDemo {
@State selectedText: string = '未选择'
private showPicker() {
const uiContext = this.getUIContext()
uiContext.showTextPickerDialog({
range: ['选项一', '选项二', '选项三'],
selected: 0,
onAccept: (value: string, index: number) => {
this.selectedText = `${value} - ${index}`
}
})
}
build() {
Column({ space: 12 }) {
Text(this.selectedText)
Button('打开选择器').onClick(() => this.showPicker())
}
.padding(16)
}
}
如果代码在 UIAbility 或窗口初始化流程里,就不要硬从组件层去找上下文。更稳的写法是在 windowStage.loadContent(...) 完成后,再从主窗口拿 UIContext。文档里对这一点说得很明确,getUIContext 需要在 windowStage.loadContent 之后调用,这样 UIContext 才已经初始化完成;调用过早,返回结果可能不准确。
对应代码可以这样写:
typescript
import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
export default class EntryAbility extends UIAbility {
private appUIContext?: UIContext
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err && err.code) {
console.error(`loadContent failed: ${JSON.stringify(err)}`)
return
}
const mainWindow = windowStage.getMainWindowSync()
this.appUIContext = mainWindow.getUIContext()
this.appUIContext.showTextPickerDialog({
range: ['北京', '上海', '深圳'],
selected: 0,
onAccept: (value: string, index: number) => {
console.info(`accept value=${value}, index=${index}`)
}
})
})
}
}
这两种拿法已经覆盖了绝大多数业务代码。一个在组件里就近拿,一个在窗口真正就绪后从主窗口拿。
三、跨模块和异步调用,别再用单例硬兜
之前有一个全局服务容器去保存 UIContextService。这种写法在单窗口项目里短期能跑,放到多窗口应用里风险会明显变大。因为 UIContext 本身是和具体窗口绑定的,窗口一多,全局只存一个上下文,很容易把弹窗弹到错误窗口,或者后来的窗口把前面的上下文覆盖掉。
所以,跨模块适配更稳的做法,是按窗口持有自己的弹窗服务,或者把 UIContext 显式传给业务模块。这样写虽然多传一个参数,边界是清楚的,后面排查问题也不会绕。
先看一个窗口级的服务封装:
typescript
class TextPickerDialogService {
constructor(private readonly uiContext: UIContext) {}
show(options: TextPickerDialogOptions | TextPickerDialogOptionsExt): void {
this.uiContext.showTextPickerDialog(options)
}
}
然后在窗口初始化完成后创建它:
typescript
import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
export default class EntryAbility extends UIAbility {
private dialogService?: TextPickerDialogService
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err && err.code) {
console.error(`loadContent failed: ${JSON.stringify(err)}`)
return
}
const uiContext = windowStage.getMainWindowSync().getUIContext()
this.dialogService = new TextPickerDialogService(uiContext)
})
}
}
异步链路里也尽量走同一个原则。不要在深层业务代码里临时猜一个当前窗口出来,也不要继续保留一个全局静态弹窗入口。更稳的写法,是在发起异步任务的那一层,把 UIContext 或对应服务一起传下去。
typescript
async function loadOptionsAndShow(
dialogService: TextPickerDialogService
): Promise<void> {
const options = await Promise.resolve(['苹果', '橙子', '香蕉'])
dialogService.show({
range: options,
selected: 0,
onAccept: (value: string, index: number) => {
console.info(`accept value=${value}, index=${index}`)
}
})
}
这样写的一个直接好处,是 UI 归属关系从入口到弹出点都没有丢。后面即便业务模块拆得更多,弹窗属于哪个窗口,代码里一眼就能看出来。
四、兼容层可以留,但别把旧接口继续当主路径
如果项目还要兼容更低版本,保留一个适配层是合理的。TextPickerDialog.show 从 API version 18 开始废弃,低版本项目里它仍然存在;UIContext.showTextPickerDialog` 则已经进入明确推荐路径。
这类兼容层更适合写成过渡方案,不适合长期继续做主路径。代码可以像这样:
typescript
class TextPickerAdapter {
static show(
options: TextPickerDialogOptions | TextPickerDialogOptionsExt,
uiContext?: UIContext,
apiVersion: number = 20
): void {
if (apiVersion >= 18 && uiContext) {
uiContext.showTextPickerDialog(options)
return
}
if (typeof TextPickerDialog?.show === 'function') {
TextPickerDialog.show(options)
return
}
console.error('当前环境没有可用的文本选择弹窗接口')
}
}
这种做法解决的是版本兼容,不是新的日常写法。项目一旦以 API 20 为主线,就应该把组件内和窗口级代码逐步迁到 UIContext.showTextPickerDialog 上。继续长期依赖旧接口,只会把后面的多窗口、复杂页面和跨模块问题越堆越多。
总结
这次从 TextPickerDialog.show 到 UIContext.showTextPickerDialog 的迁移,弹窗显示动作要和具体 UI 实例绑定。TextPickerDialog.show 从 API version 18 开始废弃,推荐路径已经切到 UIContext;UIContext 本身和窗口绑定,窗口初始化完成后再拿,结果才稳定。组件内部可以直接 this.getUIContext(),窗口级逻辑可以在 loadContent 完成后从主窗口拿。
工程里更值得注意的,是跨模块和异步调用的写法。全局单例服务在多窗口场景里容易埋雷,按窗口持有服务或者显式传递 UIContext,后面会稳很多。兼容层可以留,用来照顾旧版本;API 20 主线代码还是应该尽快收口到 UIContext.showTextPickerDialog 上。