HarmonyOS :自定义弹窗(CustomDialog)的解耦实践

引言

在鸿蒙开发中使用 CustomDialogController@CustomDialog 可实现自定义弹窗交互。但 controller 的定义位置却有很大的限制。

限制如下:

CustomDialogController仅在作为@CustomDialog@Component struct的成员变量,且在@Component struct内部定义时赋值才有效

对于 Dialog 的能力封装我们通常会将其与调用者页面解耦,以 Flutter 为例,我们可能会这么封装:

typedef DialogCallback = Function(String data);

// 随便一个工具类
abstract class DialogTool {
  
  static void showCustomDialog(
    BuildContext context,
    DialogCallback callback, // 定义回调
  ) {
    showDialog(
      context: context,
      builder: (context) {
        return Dialog(
          child: YourWidget(), // dialog UI
        );
      },
    ).then(
      (data) => {
        callback(data),
      },
    );
  }
  
}

在工具类提供静态方法,调用方可直接调用并获取返回值。

而在 ArkTS 页面内开发一个自定义弹窗,不可避免的会有一堆冗余代码需要被嵌在调用者UI层,下面我来举个例子:

  • 首先使用 @CustomDialog 定义一个简单的带回调的 Dialog
@CustomDialog
export struct MyCustomDialog {
  private controller: CustomDialogController
  //确定按钮回调
  confirmed: (message: string) => void = (_) => {
  }

  build() {
    Column() {
      Text('这是一个自定义弹窗')
      Button('点击传递数据给调用方').onClick(() => {
        this.controller.close()
        this.confirmed('你好,我是弹窗点击返回的内容')
      })
    }
  }
}
  • 调用者页面需要定义 CustomDialogController 绑定 MyCustomDialog 来唤起自定义弹窗
@Component
struct HomePage {
  dialogController?: CustomDialogController | null

  private async showDialog(): Promise<string | undefined> {
    let resolveFunc: (value: string | undefined | PromiseLike<string | undefined>) => void = () => {
    }
    const promise = new Promise<string | undefined>(
      (resolve, _) => {
        resolveFunc = resolve
      }
    )
    this.dialogController = new CustomDialogController({
      builder: MyCustomDialog(
        {
          confirmed: (info) => {
            resolveFunc(info)
          },
        }
      )
    })
    this.dialogController?.open()
    return promise
  }

  build() {
    Column(){
      ... 你的UI布局
    }
  }
}

如上,页面内冗余代码主要体现在 showDialog(){} 内,当你的页面需要具备多种业务的自定义弹窗时,你就需要在同一个页面定义多个 showDialog() 方法用以显示不同的弹窗,接收不同的回执传参。

整体的代码会变得很乱,结构封装会失去简洁性。

如果不能避开限制,怎么做解耦?

我们实践的方案是做一个空白中间层,用于存放 CustomDialogControllerCustomDialog 的绑定工作(代码),只给上层UI提供一个 controller 用来控制弹窗开启、关闭、以及接收返回值。以此达到与调用方浅解耦的目的。

  • 已上文中的 MyCustomDialog 为例,新建一个文件 CustomDialogContainer, 代码如下:
/// 为了将dialog的初始化与与使用dialog的页面解耦
@Component
export struct CustomDialogContainer {
  @Link @Watch('onDialogToggle') controller: DialogContainerController
  dialogController?: CustomDialogController | null

  onDialogToggle() {
    if (this.controller.isOpen) {
      this.showDialog().then((data) => {
        this.controller.onDialogConfirm(data)
      })
    } else {
      this.dialogController?.close()
    }
  }

  private async showDialog(): Promise<string | undefined> {
    let resolveFunc: (value: string | undefined | PromiseLike<string | undefined>) => void = () => {
    }
    const promise = new Promise<string | undefined>(
      (resolve, _) => {
        resolveFunc = resolve
      }
    )
    this.dialogController = new CustomDialogController({
      builder: MyCustomDialog(
        {
          confirmed: (info) => {
            resolveFunc(info)
          },
        }
      ),
    })
    this.dialogController?.open()
    return promise
  }

  build() {

  }
}

export class DialogContainerController {
  isOpen: boolean = false;
  onDialogConfirm: (value: string | undefined) => void

  constructor(onConfirm: (value: string | undefined) => void) {
    this.onDialogConfirm = onConfirm
  }

  public open() {
    this.isOpen = true
  }

  public close() {
    this.isOpen = false
  }
}

build(){} 可以看出这是一个空白的 Component 组件。

在调用者页面我们这样使用

@Component
export struct HomePage {
  @State dialogController: DialogContainerController = new DialogContainerController(
    (message: string | undefined) => {
      // 这里接收到弹窗内确定按钮点击返回的数据
      if (message != undefined) {
        // 你的业务逻辑
      }
    }
  )

  // 如果有多个样式的弹窗,定义多个 dialogController 然后统一在这里管理
  @Builder
  DialogControllerBuilder() {
    Column() {
      CustomDialogContainer({
        controller: this.dialogController
      })
    }
  }

  build() {
    Column() {
      this.DialogControllerBuilder()
      ... 你的UI
    }
  }
}

调用者业务只持有自定义弹窗的 controller 处理交互,如果有多个自定义弹窗,则定义多个 controller ,统一存放管理,在一定程度内减少了独立业务的代码冗余。

附注(Example)

Demo示例(基于API11开发,支持NEXT及以上版本运行)已上传可供参考,包含如下内容:

  • 静态库+多模块设计
  • 状态管理+统一路由管理
  • 网络请求、Loading、Log(支持长文本打印) 等工具库封装
  • 自定义组件、自定义弹窗(解耦)
  • EventBus 事件通知
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容