前言

很多老项目升到鸿蒙 6 之后,最先看到的一批告警,就是单位转换相关的全局接口不再推荐继续使用。vp2pxpx2vpfp2pxpx2fplpx2pxpx2lpx 这些写法,过去很顺手,现在继续写也不是立刻不能跑,但已经不适合继续当主路径。

这件事表面上看是接口替换,实际上牵涉的是另一层问题。以前很多代码默认单位转换和当前界面天然绑定,所以直接全局调用就行。

到了 Stage 模型、多窗口、异步回调这些场景里,这个前提已经不稳了。真正要改的,不只是函数名,而是单位转换这件事到底跟谁绑定。

一、这轮迁移的核心是把单位转换绑回当前 UI 实例

全局单位转换接口的问题,不在换算能力本身,而在上下文归属不清。你在一个普通页面里直接调,看上去没什么问题。一旦到了异步逻辑、复杂页面、多个 UI 实例并存的场景,系统未必能准确判断这次换算到底应该使用哪一个界面的参数。

这也是为什么现在更推荐走 UIContext。因为 UIContext 对应的是具体 UI 实例的运行时环境,单位转换放到这里之后,谁来换、按什么参数换,关系就明确了。代码虽然比过去多了一步,但结果更稳,尤其是在多窗口和异步场景里。

所以这轮迁移不要理解成旧接口过时了,换个新接口继续写就行。更准确的理解是,单位转换从一个看起来谁都能调的全局工具,收回到了具体 UI 实例自己的上下文里。这个逻辑想清楚了,后面的改造就不会乱。

二、组件里怎么拿Ability 里怎么拿先分清楚

如果代码就在自定义组件内部,最直接的写法就是 this.getUIContext()。这种情况最简单,也最适合优先改。页面按钮点击、局部布局计算、组件内部状态联动,这些地方本来就和当前 UI 实例绑得很紧,直接就近拿上下文就行。

@Entry
@Component
struct ProfileCard {
  build() {
    Column() {
      Text('个人资料')
        .fontSize(20)

      Button('计算宽度')
        .onClick(() => {
          const uiContext = this.getUIContext();
          const widthPx = uiContext.vp2px(120);
          console.info(`120vp -> ${widthPx}px`);
        })
    }
    .padding(16)
  }
}

这种写法的好处不是形式新,而是归属清楚。当前组件拿当前组件所在的 UI 上下文,不需要猜,也不需要兜底。

如果代码在 UIAbility 这一层,或者在窗口初始化流程里,那就不要硬去找组件的 this。这时候更合适的入口是窗口。等 windowStage.loadContent(...) 完成后,再从主窗口拿 UIContext。这样更符合 Stage 模型的实际结构。

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();

      const pxValue = this.appUIContext.vp2px(24);
      console.info(`24vp -> ${pxValue}px`);
    });
  }
}

这里最重要的是时机。界面还没真正挂到窗口上时,就不要急着拿。拿早了,后面的问题不一定立刻暴露,但会在一些边界场景里慢慢冒出来。

三、最容易翻车的地方是异步回调和工具函数

页面里的全局 vp2px 其实不难改,搜一下基本都能找到。真正麻烦的,是那些异步回调、Promise 结束后的处理逻辑、还有一些项目里共用多年的工具函数。这些地方最容易留下半新半旧的写法,看上去已经迁移了,实际上还是在偷偷依赖全局思路。

最稳的办法不是继续做一个伪全局适配层,然后内部再想办法兜底。而是把 UIContext 显式传进去。这样虽然多了一个参数,但依赖关系是明的,代码也更容易维护。

export function vp2pxBy(uiContext: UIContext, value: number): number {
  return uiContext.vp2px(value);
}

export function px2vpBy(uiContext: UIContext, value: number): number {
  return uiContext.px2vp(value);
}

组件里用的时候也别偷懒,真正执行换算的那个点,再拿当前可用的 UIContext

@Entry
@Component
struct AsyncLayoutDemo {
  @State widthPx: number = 0;

  async refreshLayout() {
    await new Promise<void>((resolve) => setTimeout(() => resolve(), 100));

    const uiContext = this.getUIContext();
    this.widthPx = vp2pxBy(uiContext, 100);
  }

  build() {
    Column() {
      Text(`当前宽度: ${this.widthPx}`)
      Button('刷新布局').onClick(() => this.refreshLayout())
    }
  }
}

这类地方最忌讳的是想当然。不要觉得既然是同一个页面发起的异步任务,后面随便怎么算都还是它。工程里最容易出问题的,恰恰就是这些默认不出问题的地方。

四、迁移要先统一规则,再做缓存

项目一大,这类迁移最怕的不是改不完,而是改着改着出现三四种写法并存。有人在组件里直接 this.getUIContext(),有人从窗口层往下传,有人又自己包了一个适配器,最后表面上都能跑,后面维护的人根本不知道该按哪套来。

更稳的方式是先把规则定清楚。

组件内部统一走 this.getUIContext()。Ability 或窗口级逻辑统一在 loadContent 完成后,从主窗口拿 UIContext。公共工具函数不要再偷偷依赖上下文,统一显式接收 UIContext

这三条先定下来,再开始逐步替换,项目里的风格才会收得住。

等替换做完了,再考虑性能层面的事。比如一些高频使用的尺寸,确实可以缓存。但缓存也不能回到老路上,做成一个全局静态值到处复用。更合理的方式,是让缓存和当前 UIContext 绑定。哪个页面、哪个窗口在用,就按它自己的上下文缓存。

class UnitCache {
  constructor(private uiContext: UIContext) {}

  readonly space4 = this.uiContext.vp2px(4);
  readonly space8 = this.uiContext.vp2px(8);
  readonly icon24 = this.uiContext.vp2px(24);
  readonly icon32 = this.uiContext.vp2px(32);
}

@Entry
@Component
struct CachedLayoutDemo {
  private cache?: UnitCache;

  aboutToAppear() {
    this.cache = new UnitCache(this.getUIContext());
  }

  build() {
    Column({ space: this.cache?.space8 ?? 0 }) {
      Text('缓存后的常用尺寸')
        .margin({ bottom: this.cache?.space4 ?? 0 })

      Image($r('app.media.ic_public'))
        .width(this.cache?.icon24 ?? 24)
        .height(this.cache?.icon24 ?? 24)
    }
  }
}

这种写法看着比过去麻烦一点,但边界是清楚的。尤其是多窗口、折叠屏这类设备环境里,这种清楚比省几行代码更重要。

总结

这轮迁移最容易被误解成一次普通的接口替换。实际上不是。真正要改的是思路,单位转换以后不能再默认是一个谁都能随手调的全局工具,它必须和具体 UI 实例绑定。

组件里就近拿 this.getUIContext(),窗口级逻辑在 loadContent 完成后从主窗口拿 UIContext,公共工具函数显式接收 UIContext。再进一步,把高频尺寸缓存也跟当前上下文绑在一起,而不是继续做成全局静态值。

Logo

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

更多推荐