鸿蒙 HarmonyOS 6 | TextPickerDialog 迁移实战

文章目录

前言

很多老项目升到鸿蒙 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.showUIContext.showTextPickerDialog 的迁移,弹窗显示动作要和具体 UI 实例绑定。TextPickerDialog.show 从 API version 18 开始废弃,推荐路径已经切到 UIContextUIContext 本身和窗口绑定,窗口初始化完成后再拿,结果才稳定。组件内部可以直接 this.getUIContext(),窗口级逻辑可以在 loadContent 完成后从主窗口拿。

工程里更值得注意的,是跨模块和异步调用的写法。全局单例服务在多窗口场景里容易埋雷,按窗口持有服务或者显式传递 UIContext,后面会稳很多。兼容层可以留,用来照顾旧版本;API 20 主线代码还是应该尽快收口到 UIContext.showTextPickerDialog 上。

相关推荐
独特的螺狮粉2 小时前
开源鸿蒙跨平台Flutter开发:DNA测序波峰色谱可视化分离平台:基于 Flutter 的信号解耦与基因组流体渲染架构
flutter·华为·架构·开源·harmonyos·鸿蒙
Swift社区2 小时前
ArkUI 项目结构设计:小项目 vs 大项目
harmonyos·arkui
云和数据.ChenGuang2 小时前
鸿蒙应用对接DeepSeek大模型:构建智能问答系统的技术实践
java·华为·langchain·harmonyos·euler·openduler
AI_零食2 小时前
开源鸿蒙跨平台Flutter开发:生物力学与力量周期-臂力训练矩阵架构
学习·flutter·ui·华为·矩阵·开源·harmonyos
前端不太难2 小时前
从 0 开发一个鸿蒙小游戏(完整实战)
华为·harmonyos
独特的螺狮粉2 小时前
开源鸿蒙跨平台Flutter开发:地震震源探测系统-地震波形与波干涉渲染架构
开发语言·flutter·华为·架构·开源·harmonyos
小雨天気.2 小时前
Flutter 框架跨平台鸿蒙开发 - 反向社交应用
flutter·华为·harmonyos·鸿蒙
枫叶丹42 小时前
【HarmonyOS 6.0】窗口能力增强:PC/2in1与自由多窗模式的深度解析
开发语言·华为·harmonyos
世人万千丶2 小时前
开源鸿蒙跨平台Flutter开发:幼儿园成语序列与海马体印迹锚定引擎-突触链式网络渲染架构
学习·flutter·开源·harmonyos·鸿蒙