在之前的文章中(鸿蒙自定义 Dialog 的 6 种方式),提到了自定义 Dialog 某些情况下无法渲染显示的问题,其本质是 UI 组件构造时,UI 上下文获取异常,一般在异步回调或者非 UI 组件环境中构造全局类的组件(例如弹窗或者 HUD)时,容易遇到这个问题。
1. 问题复现
使用我的 XTEasyHUD,不预先在 UI 组件生命周期中对其进行初始化配置,直接在异步场景中调用,就会导致 HUD 无法显示。
import axios, { AxiosResponse } from '@ohos/axios'
import {
XTEasyHUD
} from '@jxt/xt_hud'
class viewModel {
async asyncShowToast() {
let response: AxiosResponse<string> = await axios.get('https://www.baidu.com')
console.log('end', response.data)
// 这个 toast 无法显示
XTEasyHUD.showToast('请求完成')
}
}
@Entry
@Component
struct Index {
vm: viewModel = new viewModel()
build() {
Column() {
Button('异步显示 Toast')
.onClick(() => {
this.vm.asyncShowToast()
})
}
.height('100%')
.width('100%')
}
}
我在该组件库的文档中,有做该问题的特别说明和解决方案描述:已知问题特别注意
解决方案:
import axios, { AxiosResponse } from '@ohos/axios'
import {
XTEasyHUD
} from '@jxt/xt_hud'
class viewModel {
async asyncShowToast() {
let response: AxiosResponse<string> = await axios.get('https://www.baidu.com')
console.log('end', response.data)
XTEasyHUD.showToast('请求完成')
}
}
@Entry
@Component
struct Index {
vm: viewModel = new viewModel()
// Warning: 强烈建议在全局根页面组件中事先做一次XTEasyHUD的全局配置,如果使用默认样式,可以直接进行空配置
// 这样可以保证后续使用时,异步场景中的 HUD 可以正常加载
aboutToAppear(): void {
XTEasyHUD.globalConfigToast()
XTEasyHUD.globalConfigLoading()
XTEasyHUD.globalConfigProgress()
}
build() {
Column() {
Button('异步显示 Toast')
.onClick(() => {
this.vm.asyncShowToast()
})
}
.height('100%')
.width('100%')
}
}
这个本质做的是,在 UI 组件环境中,执行了 HUD 组件的预初始化,避免后续执行 HUD 显示时(此时 HUD 可能还未初始化,组件库内部逻辑是懒加载的)的 UI 上下文获取异常。
2. 更优雅的方案:runScopedTask
其实 ArkUI 框架中有对应的 UIContext 回溯方法:runScopedTask
在系统很多 API 的执行过程中,该方法都很有用:
- DisplaySync.start():start接口是将DisplaySync关联到UI实例和窗口,若在非UI页面中或者一些异步回调中进行start操作,可能无法跟踪到当前UI的上下文,导致start接口失败,会进一步导致订阅函数无法执行
- Environment和UIContext相关联
import {
XTEasyHUD
} from '@jxt/xt_hud'
import { window } from '@kit.ArkUI'
class viewModel {
async asyncShowToast() {
let response: AxiosResponse<string> = await axios.get('https://www.baidu.com')
console.log('end', response.data)
// 回溯 UI 上下文
let windowClass = await window.getLastWindow(getContext())
let uiContext = windowClass.getUIContext()
uiContext.runScopedTask(() => {
XTEasyHUD.showToast('请求完成')
})
}
}
@Entry
@Component
struct Index {
vm: viewModel = new viewModel()
build() {
Column() {
Button('异步显示 Toast')
.onClick(() => {
this.vm.asyncShowToast()
})
}
.height('100%')
.width('100%')
}
}
更好的方式,自然是我在我的组件库内部逻辑中,做好对应的 UI 上下文回溯操作:
static async showToast(text: string, options?: XTHUDToastOptions) {
if (!(text.length > 0)) {
return
}
// 懒加载
await _EasyHUDManager.initXTEasyHUDToast(null)
_EasyHUDManager.showToast(text, options)
}
async initXTEasyHUDToast(globalOptions?: ((options: XTHUDToastOptions) => void) | null) {
if (!_XTEasyHUDManager._toast) {
// 在 UI 上下文环境中,执行 HUD 组件初始化
let windowClass = await window.getLastWindow(getContext())
let uiContext = windowClass.getUIContext()
uiContext.runScopedTask(() => {
_XTEasyHUDManager._toast = new XTEasyHUDToast(undefined, {
globalOptions: globalOptions
})
_XTEasyHUDManager._toast.mounted()
})
}
}
但这样操作有很大的弊端:
首先,因为getLastWindow操作是异步的,会导致整体函数变为异步操作,破坏了整体 API 的执行时序和封装;其次,getContext()操作并未传入任何组件(可选入参),也就是该上下文获取操作,很可能会执行失败,导致后续runScopedTask回溯失败。
实测在 Ability 启动阶段,如果强制执行上述操作,可能会导致同样的异常问题,这个依旧算不上安全。
3. 最佳实践
如果想在任意环境中构建 UI 组件,最好的方案,还是使用 ComponentContent。
ComponentContent 在实例化时,需要显示的传入 UIContext,至于如何获取 UIContext,那就是另一个问题了。
let contentNode = new ComponentContent(uiContext, wrapBuilder(buildText), new Params(this.message));
我在我的组件库 V3 版本中新增的 XTPromptHUD ,就是利用 ComponentContent 做的,且内置了一套相对完善的 UI 上下文自动获取逻辑,依旧可以做到一句话执行显示 HUD 的操作,但相对合理和安全的,还是用户在使用前自定义传入 UIContext。
/// 异步获取context
private static asyncGetUIContext(callback: (uiContext: UIContext | null) => void) {
let context = getContext()
if (context) {
window.getLastWindow(context).then((windowClass: window.Window) => {
let uiContext = windowClass.getUIContext()
if (callback) {
callback(uiContext)
}
}).catch(() => {
if (callback) {
callback(null)
}
})
} else {
if (callback) {
callback(null)
}
}
}
/// 异步自动初始化 toast
private static asyncConfigToast(callback: (toast: XTPromptHUDToastClass | null) => void) {
XTPromptHUD.asyncGetUIContext((uiContext: UIContext | null) => {
if (uiContext) {
let toastHUD = XTPromptHUD.initToastInstance(uiContext)
if (callback) {
callback(toastHUD)
}
} else {
if (callback) {
callback(null)
}
}
})
}
static showToast(text: string, options?: XTHUDToastOptions): void {
if (!(text.length > 0)) {
return
}
if (XTPromptHUD._toast) {
XTPromptHUD._toast.showToast(text, options)
} else {
XTPromptHUD.asyncConfigToast((toast: XTPromptHUDToastClass | null) => {
if (toast) {
console.warn('[XTPromptHUD.showToast] warning: globalConfigToast is not executed, the default context is used!')
toast.showToast(text, options)
} else {
console.error('[XTPromptHUD.showToast] error: globalConfigToast must be executed first!')
// throw new Error('globalConfigToast must be executed first!')
}
})
}
}