鸿蒙 ArkUI 的 Dialog 系统提供两类:promptAction.showDialog 简单 + 系统外观;@CustomDialog 完全自定义。独立 app 一般两套都要------简单确认用前者,复杂业务(选择器 / 表单)用后者。做了几个 app 后发现 @CustomDialog 很容易写成 N 个独立组件 ,可复用度低。记录一下 用 enum type 做多类型统一管理 的 pattern,顺便分享给有需要的朋友。
一、@CustomDialog 的基本用法
typescript
@CustomDialog
export struct ConfirmDialog {
controller?: CustomDialogController
@Prop title: string = ''
@Prop message: string = ''
onConfirm: () => void = () => {}
build() {
Column() {
Text(this.title).fontSize(18).fontWeight(FontWeight.Bold)
Text(this.message).fontSize(14).margin({ top: 12 })
Row() {
Button('取消').onClick(() => this.controller?.close())
Button('确认').onClick(() => {
this.onConfirm()
this.controller?.close()
})
}
.margin({ top: 16 })
}
.padding(20)
.backgroundColor(Color.White)
.borderRadius(16)
}
}
// 使用
@Entry @Component
struct Page {
private dialogController: CustomDialogController = new CustomDialogController({
builder: ConfirmDialog({
title: '删除确认',
message: '确定吗?',
onConfirm: () => this.doDelete(),
}),
alignment: DialogAlignment.Center,
})
build() {
Button('删除').onClick(() => this.dialogController.open())
}
}
几个点:
controller :dialog 自己通过 controller.close() 关闭。
@Prop 传入数据 :title / message / onConfirm。
CustomDialogController 在父组件声明 :open() 显示 dialog。
二、多个 dialog 的膨胀问题
随着 app 变复杂:
erlang
ConfirmDialog.ets
DatePickerDialog.ets
CategoryPickerDialog.ets
InputDialog.ets
ActionSheetDialog.ets
InfoDialog.ets
...
每个 Dialog 一个组件 + 一个 controller + 一个 open 。页面里N 个 controller 字段 。代码膨胀。
问题:
- 样式不统一(每个 Dialog 独立实现 border / padding)
- state 分散(每个 controller 独立管)
- 跨组件打开 Dialog 麻烦(EventBus 或 prop drilling)
三、统一 Dialog Manager pattern
思路 :一个 CustomDialog + enum type 区分内容。
typescript
export enum DialogType {
None = 'none',
Confirm = 'confirm',
Input = 'input',
DatePicker = 'date',
CategoryPicker = 'category',
Info = 'info',
}
export interface DialogConfig {
type: DialogType
title: string
message?: string
defaultValue?: string
options?: string[]
onConfirm?: (value: string) => void
onCancel?: () => void
}
AppStorage 存当前 dialog:
typescript
// 全局
AppStorage.setOrCreate('dialog.current', null as DialogConfig | null)
// 任何地方触发
AppStorage.set('dialog.current', {
type: DialogType.Confirm,
title: '删除',
message: '确定吗',
onConfirm: () => doDelete(),
})
// 关闭
AppStorage.set('dialog.current', null)
四、统一的 UniversalDialog 组件
typescript
@CustomDialog
export struct UniversalDialog {
controller?: CustomDialogController
@StorageLink('dialog.current') config: DialogConfig | null = null
build() {
Column() {
if (this.config) {
Text(this.config.title).fontSize(18).fontWeight(FontWeight.Bold)
this.renderBody()
this.renderActions()
}
}
.padding(20)
.backgroundColor(Color.White)
.borderRadius(16)
.width(300)
}
@Builder
renderBody() {
if (this.config?.type === DialogType.Confirm) {
Text(this.config.message ?? '').fontSize(14)
} else if (this.config?.type === DialogType.Input) {
TextInput({ text: this.config.defaultValue ?? '' })
.onChange((v) => { this.inputValue = v })
} else if (this.config?.type === DialogType.CategoryPicker) {
ForEach(this.config.options ?? [], (opt: string) => {
Button(opt).onClick(() => {
this.config?.onConfirm?.(opt)
this.close()
})
})
}
// ... 其他类型
}
@Builder
renderActions() {
Row() {
Button('取消').onClick(() => this.close())
if (this.config?.type !== DialogType.CategoryPicker) {
Button('确认').onClick(() => {
this.config?.onConfirm?.(this.inputValue)
this.close()
})
}
}
}
@State private inputValue: string = ''
private close(): void {
this.controller?.close()
AppStorage.set('dialog.current', null)
}
}
所有类型 Dialog 用一个组件 。renderBody 根据 type 分发。
五、顶层挂一个 UniversalDialog
Index.ets:
typescript
@Entry @Component
struct Index {
@StorageLink('dialog.current') dialogConfig: DialogConfig | null = null
private universalDialog: CustomDialogController = new CustomDialogController({
builder: UniversalDialog(),
alignment: DialogAlignment.Center,
autoCancel: true,
})
@Watch('onDialogConfigChange')
dialogConfigWatch: boolean = false
private onDialogConfigChange(): void {
if (this.dialogConfig !== null) {
this.universalDialog.open()
} else {
this.universalDialog.close()
}
}
build() {
// 整个 app 的 UI
// UniversalDialog 自动打开 / 关闭
}
}
流程:
- 任何地方 set AppStorage 的 dialog.current
- @Watch 响应 → open universalDialog
- Dialog 内部根据 type render
- 用户确认 / 取消 → set null
- @Watch 响应 → close universalDialog
六、各处调用的简化 API
typescript
export class DialogService {
static confirm(title: string, message: string, onOk: () => void): void {
AppStorage.set('dialog.current', {
type: DialogType.Confirm,
title,
message,
onConfirm: () => onOk(),
})
}
static input(title: string, defaultValue: string, onInput: (v: string) => void): void {
AppStorage.set('dialog.current', {
type: DialogType.Input,
title,
defaultValue,
onConfirm: (v) => onInput(v),
})
}
static categoryPicker(title: string, options: string[], onPick: (v: string) => void): void {
AppStorage.set('dialog.current', {
type: DialogType.CategoryPicker,
title,
options,
onConfirm: (v) => onPick(v),
})
}
}
// 使用
DialogService.confirm('删除', '确定吗?', () => this.doDelete())
DialogService.input('修改名字', this.name, (v) => this.name = v)
DialogService.categoryPicker('选分类', ['工作', '生活', '娱乐'], (v) => this.category = v)
调用方的代码 :一行。
不用 :在页面组件声明 CustomDialogController。
七、Dialog 复用 vs 独立的权衡
复用(本案):
- 优点:调用简单,样式统一,state 集中
- 缺点:UniversalDialog 组件复杂(switch 多个 type)
独立:
- 优点:每个 Dialog 自己 clean
- 缺点:多处调用 麻烦 ,样式各做各的
独立开发者的建议:
- 5 种以内 dialog → 独立
- 5-15 种 → 复用(本案)
- 15+ 种 → 可能你的 dialog 太多了,考虑专门的页面代替
八、Dialog 的 onDismiss 和 autoCancel
typescript
new CustomDialogController({
builder: UniversalDialog(),
autoCancel: true, // 点外部关闭
onCancel: () => {
AppStorage.set('dialog.current', null)
},
cornerRadius: 16,
alignment: DialogAlignment.Center,
})
autoCancel: true :点 dialog 外部关闭。
- 用户不想强行限制操作
- 配合onCancel 清 AppStorage
autoCancel: false :只能通过按钮关闭。
- 重要确认 (如"真的要删除所有数据吗")用
- 防止误触
独立开发者的建议 :95% 的 Dialog 用 autoCancel: true 。强制交互只在高危操作用。
踩坑清单
- 简单确认用 promptAction.showDialog(系统外观)
- 复杂业务用 @CustomDialog
- 多类型 Dialog 用 enum + 统一组件
- AppStorage 存当前 dialog config
- 顶层挂一个 UniversalDialog
- 调用方用 DialogService.confirm/input/picker(一行)
- autoCancel: true 大部分场景
- autoCancel: false 强制确认场景
- renderBody / renderActions 分开 @Builder
尾声
Dialog 统一管理 是独立 app 的工程化基础设施 。早期不管,后期 N 个 controller 难受。
独立开发者的建议:
- 项目早期定好 Dialog pattern
- 5+ 种 Dialog 果断上统一管理
- 调用方只能看到 DialogService.xxx(禁用裸 CustomDialogController)
对比:
- React Native:react-native-modal + context
- iOS:UIAlertController + 自定义 modal
- Flutter:showDialog + custom builder
- 鸿蒙:本案的 enum + AppStorage pattern
UI 组件复用 是独立开发者的生产力 。把 Dialog 做好了,一个 app 的调用方全部一行,心智清爽。