ArkTS多线程的多线程系列(五)HarmonyOS 鸿蒙Next:通过子线程实现全局弹窗
ArkTS多线程的多线程系列(五)HarmonyOS 鸿蒙Next:通过子线程实现全局弹窗
- ArkTS多线程的多线程系列(一):ArkTS多线能力入门
- ArkTS多线程的多线程系列(二):基于Sendable共享对象实现跨线程通信及UI状态刷新
- ArkTS多线性的多线程系列(三):基于单例实现跨线程缓存
- ArkTS多线程的多线程系列(四):基于生产者-消费者实现多线程协同
当应用需要对网络状态进行长期管理监控时,当网络不好时弹窗提示用户当前网络状态差,或者网络连接中断时需要弹窗提示用户网络断开、任务异常终止等。
方案介绍
我们知道弹窗只能在UI主线程弹出,因此最简单的处理方法是将需要弹窗的类型、弹窗内容都通过子线程发送到主线程,这样主线程再根据不同弹窗类型进行弹窗。但是这样主线程就会有大量的弹窗代码,业务耦合严重。
因此我们将要弹出什么样的窗的权利下放给子线程,这样子线程就可以根据自己的需要构建弹窗,主线程只需要提供弹窗的统一调用即可,这样就实现了弹窗开发与主线程开发的解耦(主线程不需要感知子线程需要弹什么样的框)。此种方法仅限于系统类型弹窗(如AlertDialog,Toast等)。但是,对于自定义弹窗,由于@Buidler方法无法通过sendable传递,因此需要通过在主线程构建统一的自定义弹窗框架,子线程将弹窗参数传递到主线程。
DialogBuilder为Sendable类型的共享接口,提供了showDialog方法,用于弹窗。DialogBuilderWrapper是一个包装类,将DialogBuilder包装后以实现从子线程向主线程的数据传递。
-
方案的优势:主线程不感知DialogBuilder具体实现类的实现,在主线程中只需要通过调用DialogBuilder的接口showDialog方法构建弹窗。子线程可以独立/按需构建各种弹窗对象,虽然最终的弹窗还是在主线程完成,但是主线程不再感知弹窗的细节。
-
方案的遗憾:对于非系统类弹窗(自定义弹窗),由于@Builder方法无法在Sendable类中使用,因此子线程只能构建出自定义弹窗的参数(customerDialogParam),将构建参数传递给主线程,在抓线程中显示实现弹窗。
核心代码
Step1:构建Senbable共享类型的DialogBuilder接口,接口提供showDialog方法入参为uiContext,用于弹窗构建并弹窗。
import { lang } from ‘@kit.ArkTS’;
type ISendable = lang.ISendable;
export interface DialogBuilder extends ISendable {
showDialog(uiContext: UIContext): void;
}
Step2:以弹出AlertDialog为例,AlertDialogBuilder需要实现DialogBuilder,并在showDialog方法中使用uiContext完成弹窗的构建。
export class ToastDialogBuilder implements DialogBuilder {
showDialog(uiContext: UIContext): void {
uiContext.getPromptAction().showToast({
message: ‘this toast was built by worker’,
duration: 2000
})
}
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
Step3:实现一个DialogWorker,用来接收主线程通知并进行弹窗,在实际业务中可能是一个网络状态管理子线程。
// ./src/main/ets/show_dialog/workers/DialogWorker.ets
…
workerPort.onmessage = (e: MessageEvents) => {
switch (e.data) {
case “showAlertDialog”: {
let alertDialogBuilder = new AlertDialogBuilder();
let builderWrapper: DialogBuilderWrapper = new DialogBuilderWrapper(DialogBuilderWrapper.SYSTEM_DIALOG, alertDialogBuilder);
workerPort.postMessageWithSharedSendable(builderWrapper);
break;
}
case “showToastDialog”: {
let toastDialogBuilder = new ToastDialogBuilder();
let builderWrapper : DialogBuilderWrapper = new DialogBuilderWrapper(DialogBuilderWrapper.SYSTEM_DIALOG, toastDialogBuilder);
workerPort.postMessageWithSharedSendable(builderWrapper);
break;
}
case “showCustomerDialog” : {
let builderWrapper : DialogBuilderWrapper = new DialogBuilderWrapper(DialogBuilderWrapper.CUSTOMER_DIALOG, null, new CustomerDialogParam(“this content is from worker”));
workerPort.postMessageWithSharedSendable(builderWrapper);
break;
}
}
}
…
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
从代码中可以看出,在postMessageWithSharedSendable方法中,传递的参数不是之前定义的AlertDialogBuilder共享数据类型,而是DialogBuilderWrapper,这是因为在dialogWorker.onmessage方法中,无法将mesEvent.data as 为Sendable类型,因此在传递Sendable对象类型时,需要再其外面增加一层封装。
export class DialogBuilderWrapper {
static SYSTEM_DIALOG : string = “systemDialog”;
static CUSTOMER_DIALOG : string = “customerDialog”;
dialogType = “systemDialog”
customerDialogParam !: CustomerDialogParam | undefined;
dialogBuilder !: DialogBuilder | null;
constructor(dialogType : string, dialogBuilder : DialogBuilder | null, customerDialogParam ?: CustomerDialogParam) {
this.dialogType = dialogType
this.dialogBuilder = dialogBuilder;
this.customerDialogParam = customerDialogParam;
}
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
注意
DialogBuilderWrapper对象不是Sendable类型的,因此不需要提供get/set方法,因为方法是无法完成序列化的。
DialogBuilderWrapper对象里面的dialogBuilder和customerDialogParam是Sendable类型,因此他们提供的方法跨线程依旧可以调用。
Step4:启动DialogWorker,并将他放到AppStorage中,以便后续页面需要使用此Worker时可以方便获取。因为在弹窗时希望弹窗的逻辑与UI是解耦的,因此在Abiiltiy的onWindowStage回调中启动Worker。
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// 启动Worker
let dialogWorker : worker.ThreadWorker = new worker.ThreadWorker(‘entry/ets/show_dialog/workers/DialogWorker.ets’);
// 将Worker存入AppStorage
AppStorage.setOrCreate(“dialogWorker”, dialogWorker);
dialogWorker.onmessage = async (msgEvent : MessageEvents) => {
let dialogBuilderWrapper = msgEvent.data as DialogBuilderWrapper
if (dialogBuilderWrapper.dialogType == DialogBuilderWrapper.SYSTEM_DIALOG) {
// 如果是系统弹框,将uiContext传入buidler方法,完成弹窗构建。
let dialogBuilder = dialogBuilderWrapper.dialogBuilder
if (dialogBuilder) {
// 调用具体的DialogBuilder构建弹窗
dialogBuilder.showDialog((await windowStage.getMainWindow()).getUIContext())
}
} else {
// 如果是自定义弹窗,则在主线程获取弹窗Builder,并将子线程传递来的弹窗参数传递给Builder
if (dialogBuilderWrapper.customerDialogParam) {
let uiContext = (await windowStage.getMainWindow()).getUIContext()
let promptAction = uiContext.getPromptAction()
let contentNode = new ComponentContent(uiContext, getCustomerDialogBuilder(), dialogBuilderWrapper.customerDialogParam);
// 全局弹出自定义弹窗
promptAction.openCustomDialog(contentNode);
}
}
}
…
}
…
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
Step5:对于自定义弹窗,需要提前构建好组件Builder函数,通过CustomerDialogParam定义自定义弹窗内的具体样式和内容。(这个不是本重点,可以参考其他弹窗类文章进行参考)
@Sendable
export class CustomerDialogParam {
private message : string = “”
constructor(msg: string) {
this.message = msg;
}
getMessage() {
return this.message
}
}
export function getCustomerDialogBuilder() {
return wrapBuilder(buildText);
}
function buildText(params: CustomerDialogParam) {
Column() {
Text(params.getMessage())
.fontSize(25)
.fontWeight(FontWeight.Bold)
}.backgroundColor(’#FFF0F0F0’)
.margin({bottom: 36})
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
Step6:在页面中通过按钮模拟弹窗。
@Component
export struct ShowDialogFromWorkerPage {
private dialogWorker : worker.ThreadWorker | undefined = AppStorage.get(“dialogWorker”);
build() {
NavDestination() {
Column() {
Text(‘worker子线程弹窗’)
Button(‘弹AlertDialog’).onClick(event => {
this.showDialogFromWorker(“showAlertDialog”);
})
Button(‘弹Toast’).onClick(event => {
this.showDialogFromWorker(“showToastDialog”);
})
Button(‘弹自定义弹窗’).onClick(event => {
this.showDialogFromWorker(“showCustomerDialog”);
})
}
.justifyContent(FlexAlign.SpaceEvenly)
.height(‘100%’)
.width(‘100%’)
}.hideTitleBar(true)
}
private showDialogFromWorker(dialogType : string) {
try {
this.dialogWorker?.postMessage(dialogType);
} catch (error) {
promptAction.showToast({
message: ‘Worker instance is not running, maybe worker is terminated when PostMessage’,
duration: 2000
});
}
}
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
更多关于ArkTS多线程的多线程系列(五)HarmonyOS 鸿蒙Next:通过子线程实现全局弹窗的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
更多关于ArkTS多线程的多线程系列(五)HarmonyOS 鸿蒙Next:通过子线程实现全局弹窗的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
很奇怪的一点,添加代码块的时候,竟然没有ArkTS
您好,当前技术文章配套的demo工程正在外发中,后续会发布至gitee上,敬请关注!
嗯嗯
一个月前也是这样说的😭
不能,会报错。利用回调函数、AppStorage等传值
在ArkTS(ArkUI的TypeScript版本)和HarmonyOS开发中,通常UI组件的更新需要在主线程(UI线程)中进行,以保证界面的响应性和一致性。直接在子线程中弹出全局弹窗(如Dialog或Toast)可能会导致UI更新异常或崩溃。
如果你需要在子线程中触发UI更新(如显示弹窗),你应该使用任务调度机制(如Promise
、async/await
配合new Promise
中的resolve
在子线程中完成后在UI线程执行,或使用系统提供的线程间通信机制,如taskDispatcher
)来确保UI更新在主线程执行。
如果问题依旧没法解决请加我微信,我的微信是itying888。
更多关于ArkTS多线程的多线程系列(五)HarmonyOS 鸿蒙Next:通过子线程实现全局弹窗的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html