Android Compose 应用中实现全局Dialog管理器的设计与实践

文章目录

  • 前言
  • [一、传统 Dialog 实现的问题](#一、传统 Dialog 实现的问题)
  • 二、全局状态管理的实现
    • [1.CompositionLocal 介绍](#1.CompositionLocal 介绍)
    • 2.全局状态管理应用
      • [①. 定义 CompositionLocal](#①. 定义 CompositionLocal)
      • [②. 抽象基类设计](#②. 抽象基类设计)
      • [③. 具体 Dialog 参数类型实现](#③. 具体 Dialog 参数类型实现)
      • [④. 全局 Dialog 组件](#④. 全局 Dialog 组件)
    • [3.完整实现 DialogManager](#3.完整实现 DialogManager)
    • 4.实际应用
      • [①. 提供全局状态](#①. 提供全局状态)
      • [②. 使用](#②. 使用)
      • [③. 效果](#③. 效果)
  • 总结

前言

在 Android Compose 开发中,Dialog 的使用确实非常常见。然而,传统的 Dialog 实现方式往往存在一些问题,比如状态管理混乱、重复代码多、难以维护等。本文将详细介绍一个完整的全局Dialog管理器设计与实践,展示如何优雅地管理各种类型的对话框。


一、传统 Dialog 实现的问题

传统的 Compose Dialog 实现通常存在以下问题:

  • 状态管理混乱:每个组件都需要单独管理 Dialog 的显示状态
  • 代码重复:相似的 Dialog 结构重复出现在多个地方
  • 难以扩展:新增Dialog 类型需要修改多个文件
  • 生命周期管理困难:容易出现内存泄漏或状态不一致

二、全局状态管理的实现

1.CompositionLocal 介绍

CompositionLocal 是 Jetpack Compose 中的一个重要概念,用于在组合树中隐式传递数据的机制,特别适用于全局状态管理场景。

2.全局状态管理应用

①. 定义 CompositionLocal

kotlin 复制代码
/**
 * 对话框状态数据类
 * @property dialogs 对话框参数列表
 */
data class DialogState(
    val dialogs: List<DialogParams> = emptyList()
)

/**
 * 全局对话框状态
 */
val dialogState = mutableStateOf(DialogState())

/**
 * 定义 CompositionLocal 创建本地状态提供者
 */
val LocalDialog = staticCompositionLocalOf {
    mutableStateOf(DialogState())
}

②. 抽象基类设计

kotlin 复制代码
/**
 * 对话框参数基类,定义了对话框的基本属性
 * @property id 对话框唯一标识符,默认使用UUID生成
 * @property visible 对话框是否可见
 * @property isAnimationDismiss 是否动画关闭
 */
abstract class DialogParams {
    abstract val id: String
    abstract val visible: Boolean
    abstract val isAnimationDismiss: Boolean
    
    abstract fun createCopy(
        id: String = this.id,
        visible: Boolean = this.visible,
        isAnimationDismiss: Boolean = this.isAnimationDismiss
    ): DialogParams
}

③. 具体 Dialog 参数类型实现

kotlin 复制代码
/**
 * 性别选择对话框参数
 * @property id 对话框唯一标识符
 * @property visible 对话框是否可见
 * @property isAnimationDismiss 是否动画关闭
 * @property defValue 默认选中的性别值
 * @property onConfirm 选择确认回调函数
 */
data class SexPickerDialogParam(
    override val id: String = UUID.randomUUID().toString(),
    override val visible: Boolean = false,
    override val isAnimationDismiss: Boolean = false,
    val defValue: Int = 0,
    val onConfirm: (Int) -> Unit = {}
) : DialogParams()...

/**
 * 单位选择对话框参数
 * @property id 对话框唯一标识符
 * @property visible 对话框是否可见
 * @property isAnimationDismiss 是否动画关闭
 * @property defValue 默认选中的单位值
 * @property onConfirm 选择确认回调函数
 */
data class UnitPickerDialogParam(
    override val id: String = UUID.randomUUID().toString(),
    override val visible: Boolean = false,
    override val isAnimationDismiss: Boolean = false,
    val defValue: Int = 0,
    val onConfirm: (Int) -> Unit = {}
) : DialogParams()...

④. 全局 Dialog 组件

kotlin 复制代码
/**
 * 全局对话框组件,负责渲染所有需要显示的对话框
 */
@Composable
fun GlobalDialog() {
    val localDialogState = LocalDialog.current
    val currentState = localDialogState.value
    currentState.dialogs.forEach { dialogParam ->
        when (dialogParam) {
            is SexPickerDialogParam -> { /* 性别选择对话框实现 */ }
            is UnitPickerDialogParam -> { /* 单位选择对话框实现 */ }
            else -> {}
        }
    }
}

3.完整实现 DialogManager

CommonDialogWheelPicker 参考之前的博客

kotlin 复制代码
/**
 * 对话框管理器,用于统一管理和显示各种类型的对话框
 */
object DialogManager {

    /**
     * 定义 CompositionLocal 创建本地状态提供者
     */
    val LocalDialog = staticCompositionLocalOf {
        mutableStateOf(DialogState())
    }
    
    /**
     * 对话框状态数据类
     * @property dialogs 对话框参数列表
     */
    data class DialogState(
        val dialogs: List<DialogParams> = emptyList()
    )
    
    /**
     * 全局对话框状态
     */
    val dialogState = mutableStateOf(DialogState())

    /**
     * 弹窗动画时间
     */
    const val animationDuration = DialogDefTimeMillis

    /**
     * 全局对话框组件,负责渲染所有需要显示的对话框
     */
    @Composable
    fun GlobalDialog() {
        val customColors = LocalCustomColors.current
        val localDialogState = LocalDialog.current
        val currentState = localDialogState.value
        currentState.dialogs.forEach { dialogParam ->
            when (dialogParam) {
                is SexPickerDialogParam -> {
                    if (dialogParam.visible) {
                        CommonDialog(
                            onDismissRequest = {
                                dismissRequest(dialogParam.id)
                            },
                            properties = DialogProperties(
                                usePlatformDefaultWidth = false
                            ),
                            timeMillis = animationDuration,
                            enter = slideInVertically(
                                initialOffsetY = { it },
                                animationSpec = tween(animationDuration)
                            ),
                            exit = slideOutVertically(
                                targetOffsetY = { it },
                                animationSpec = tween(animationDuration)
                            ),
                            isAnimatedDismissed = dialogParam.isAnimationDismiss,
                            boxContentAlignment = Alignment.BottomCenter,
                        ) {
                            Column(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .background(
                                        color = customColors.dialogBackground,
                                        shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
                                    ),
                                verticalArrangement = Arrangement.Bottom
                            ) {
                                val maleText = stringResource(R.string.user_info_male)
                                val femaleText = stringResource(R.string.user_info_female)
                                val sexList = listOf(0, 1)
                                Box(
                                    modifier = Modifier
                                        .fillMaxWidth()
                                        .defaultMinSize(minHeight = 200.dp),
                                    contentAlignment = Alignment.Center
                                ) {
                                    WheelPicker(
                                        modifier = Modifier.fillMaxWidth(),
                                        items = sexList,
                                        properties = PickerProperties(
                                            itemHeight = 40.dp,
                                            itemSpace = 10.dp,
                                            extraRow = 1,
                                            isLooping = false
                                        ),
                                        onItemSelected = { index, item ->
                                            dialogParam.onConfirm(item)
                                        },
                                        selectedIndex = if (dialogParam.defValue != -1) sexList.indexOf(dialogParam.defValue) else 0,
                                        selectOverlay = {
                                            Box(
                                                modifier = Modifier
                                                    .fillMaxWidth()
                                                    .height(40.dp)
                                                    .background(color = Color.Gray.copy(alpha = 0.2f))
                                            )
                                        },
                                        itemContent = { index, item, isItemSelected ->
                                            Text(
                                                text = when (item) {
                                                    0 -> maleText
                                                    1 -> femaleText
                                                    else -> ""
                                                },
                                                fontSize = 21.sp,
                                                color = if (isItemSelected) customColors.textPrimary else customColors.textSecondary
                                            )
                                        }
                                    )
                                }
                                DialogBottomButton(
                                    leftTextStr = stringResource(R.string.dialog_cancel_btn),
                                    rightTextStr = stringResource(R.string.dialog_confirm_btn),
                                    leftTextColor = customColors.textSecondary,
                                    rightTextColor = customColors.appTheme,
                                    onLeftTextClick = {
                                        dialogParam.onConfirm(-1)
                                        dismissDialogById(dialogParam.id)
                                    },
                                    onRightTextClick = {
                                        dismissDialogById(dialogParam.id)
                                    }
                                )
                            }
                        }
                    }
                }

                is UnitPickerDialogParam -> {
                    if (dialogParam.visible) {
                        CommonDialog(
                            onDismissRequest = {
                                dismissRequest(dialogParam.id)
                            },
                            properties = DialogProperties(
                                usePlatformDefaultWidth = false
                            ),
                            timeMillis = animationDuration,
                            enter = slideInVertically(
                                initialOffsetY = { it },
                                animationSpec = tween(animationDuration)
                            ),
                            exit = slideOutVertically(
                                targetOffsetY = { it },
                                animationSpec = tween(animationDuration)
                            ),
                            isAnimatedDismissed = dialogParam.isAnimationDismiss,
                            boxContentAlignment = Alignment.BottomCenter,
                        ) {
                            Column(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .background(
                                        color = customColors.dialogBackground,
                                        shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
                                    ),
                                verticalArrangement = Arrangement.Bottom
                            ) {
                                val metricStr = stringResource(R.string.user_info_unit_metric)
                                val imperialStr = stringResource(R.string.user_info_unit_imperial)
                                val unitList = listOf(0, 1)
                                Box(
                                    modifier = Modifier
                                        .fillMaxWidth()
                                        .defaultMinSize(minHeight = 200.dp),
                                    contentAlignment = Alignment.Center
                                ) {
                                    WheelPicker(
                                        modifier = Modifier.fillMaxWidth(),
                                        items = unitList,
                                        properties = PickerProperties(
                                            itemHeight = 40.dp,
                                            itemSpace = 10.dp,
                                            extraRow = 1,
                                            isLooping = false
                                        ),
                                        onItemSelected = { index, item ->
                                            dialogParam.onConfirm(item)
                                        },
                                        selectedIndex = if (dialogParam.defValue != -1) unitList.indexOf(dialogParam.defValue) else 0,
                                        selectOverlay = {
                                            Box(
                                                modifier = Modifier
                                                    .fillMaxWidth()
                                                    .height(40.dp)
                                                    .background(color = Color.Gray.copy(alpha = 0.2f))
                                            )
                                        },
                                        itemContent = { index, item, isItemSelected ->
                                            Text(
                                                text = when (item) {
                                                    0 -> metricStr
                                                    1 -> imperialStr
                                                    else -> ""
                                                },
                                                fontSize = 21.sp,
                                                color = if (isItemSelected) customColors.textPrimary else customColors.textSecondary
                                            )
                                        }
                                    )
                                }

                                DialogBottomButton(
                                    leftTextStr = stringResource(R.string.dialog_cancel_btn),
                                    rightTextStr = stringResource(R.string.dialog_confirm_btn),
                                    leftTextColor = customColors.textSecondary,
                                    rightTextColor = customColors.appTheme,
                                    onLeftTextClick = {
                                        dialogParam.onConfirm(-1)
                                        dismissDialogById(dialogParam.id)
                                    },
                                    onRightTextClick = {
                                        dismissDialogById(dialogParam.id)
                                    }
                                )
                            }
                        }
                    }
                }

                else -> {}
            }
        }
    }

    /**
     * 处理对话框请求关闭事件
     * @param dialogId 要关闭的对话框ID
     */
    private fun dismissRequest(dialogId: String) {
        dialogState.value = dialogState.value.copy(
            dialogs = dialogState.value.dialogs.map { dialog ->
                if (dialog.id == dialogId) {
                    dialog.createCopy(
                        id = dialog.id,
                        visible = false,
                        isAnimationDismiss = false
                    )
                } else {
                    dialog
                }
            }
        )
    }

    /**
     * 显示性别选择对话框
     * @param defValue 默认值
     * @param onConfirm 选择回调
     * @return 对话框ID
     */
    fun showSexPickerDialog(defValue: Int, onConfirm: (Int) -> Unit): String {
        // 移除现有的性别选择对话框参数
        val filteredDialogs = dialogState.value.dialogs.filter { dialog ->
            dialog !is SexPickerDialogParam
        }
        // 创建新对话框参数
        val newDialog = SexPickerDialogParam(
            visible = true,
            isAnimationDismiss = false,
            defValue = defValue,
            onConfirm = onConfirm
        )
        //先移除旧的,再添加新的
        dialogState.value = dialogState.value.copy(
            dialogs = filteredDialogs + newDialog
        )
        return newDialog.id
    }

    /**
     * 显示单位选择对话框
     * @param defValue 默认值
     * @param onConfirm 选择回调
     * @return 对话框ID
     */
    fun showUnitPickerDialog(defValue: Int, onConfirm: (Int) -> Unit): String {
        // 移除现有的单位选择对话框参数
        val filteredDialogs = dialogState.value.dialogs.filter { dialog ->
            dialog !is UnitPickerDialogParam
        }
        // 创建新对话框参数
        val newDialog = UnitPickerDialogParam(
            visible = true,
            isAnimationDismiss = false,
            defValue = defValue,
            onConfirm = onConfirm
        )
        //先移除旧的,再添加新的
        dialogState.value = dialogState.value.copy(
            dialogs = filteredDialogs + newDialog
        )
        return newDialog.id
    }

    /**
     * 关闭指定ID的对话框
     * @param dialogId 对话框ID
     */
    fun dismissDialogById(dialogId: String) {
        dialogState.value = dialogState.value.copy(
            dialogs = dialogState.value.dialogs.map { dialog ->
                if (dialog.id == dialogId) {
                    dialog.createCopy(
                        id = dialog.id,
                        visible = dialog.visible,
                        isAnimationDismiss = true
                    )
                } else {
                    dialog
                }
            }
        )
    }

    /**
     * 关闭所有对话框
     */
    fun dismissAllDialogs() {
        dialogState.value = dialogState.value.copy(
            dialogs = dialogState.value.dialogs.map { dialog ->
                dialog.createCopy(
                    id = dialog.id,
                    visible = dialog.visible,
                    isAnimationDismiss = true
                )
            }
        )
    }

}


/**
 * 对话框参数基类,定义了对话框的基本属性
 * @property id 对话框唯一标识符,默认使用UUID生成
 * @property visible 对话框是否可见
 * @property isAnimationDismiss 是否动画关闭
 */
abstract class DialogParams {
    abstract val id: String
    abstract val visible: Boolean
    abstract val isAnimationDismiss: Boolean

    abstract fun createCopy(
        id: String = this.id,
        visible: Boolean = this.visible,
        isAnimationDismiss: Boolean = this.isAnimationDismiss
    ): DialogParams
}


/**
 * 性别选择对话框参数
 * @property id 对话框唯一标识符
 * @property visible 对话框是否可见
 * @property isAnimationDismiss 是否动画关闭
 * @property defValue 默认选中的性别值
 * @property onConfirm 选择确认回调函数
 */
data class SexPickerDialogParam(
    override val id: String = UUID.randomUUID().toString(),
    override val visible: Boolean = false,
    override val isAnimationDismiss: Boolean = false,
    val defValue: Int = 0,
    val onConfirm: (Int) -> Unit = {}
) : DialogParams() {
    override fun createCopy(
        id: String,
        visible: Boolean,
        isAnimationDismiss: Boolean
    ): DialogParams {
        return copy(
            id = id,
            visible = visible,
            isAnimationDismiss = isAnimationDismiss
        )
    }
}

/**
 * 单位选择对话框参数
 * @property id 对话框唯一标识符
 * @property visible 对话框是否可见
 * @property isAnimationDismiss 是否动画关闭
 * @property defValue 默认选中的单位值
 * @property onConfirm 选择确认回调函数
 */
data class UnitPickerDialogParam(
    override val id: String = UUID.randomUUID().toString(),
    override val visible: Boolean = false,
    override val isAnimationDismiss: Boolean = false,
    val defValue: Int = 0,
    val onConfirm: (Int) -> Unit = {}
) : DialogParams() {
    override fun createCopy(
        id: String,
        visible: Boolean,
        isAnimationDismiss: Boolean
    ): DialogParams {
        return copy(
            id = id,
            visible = visible,
            isAnimationDismiss = isAnimationDismiss
        )
    }
}

//region 预览
@Preview(showSystemUi = true)
@Composable
fun GlobalDialogPreview() {
// 创建一个临时的状态用于预览
    val previewState = remember {
        mutableStateOf(
            DialogManager.DialogState(
                dialogs = listOf(
                    SexPickerDialogParam(
                        visible = true,
                        isAnimationDismiss = false,
                        defValue = 1,
                        onConfirm = {}
                    )
                )
            )
        )
    }

    // 使用临时状态进行预览
    CompositionLocalProvider(DialogManager.LocalDialog provides previewState) {
        DialogManager.GlobalDialog()
    }
}
//endregion

设计优势

  • 统一管理:所有对话框逻辑集中处理,便于维护和扩展
  • 单一数据源:DialogManager.dialogState 作为全局对话框状态的唯一来源
  • 解耦合:UI 组件不需要直接持有对话框实例,降低了组件间的耦合度
  • 易用性:简单的 API 接口,方便在应用的任何地方调用对话框
  • 状态集中化:所有对话框的状态(显示/隐藏、类型、参数等)都存储在一个 DialogState 对象中
  • 响应式更新:DialogManager.dialogState状态变化时的自动 UI 更新,基于状态的响应式更新,只有需要更新的组件才会重组
  • 可扩展性:新增 Dialog 类型只需继承 DialogParams 并在 GlobalDialog() 中添加相应逻辑。

4.实际应用

①. 提供全局状态

kotlin 复制代码
/**
 * 在 BaseComposeActivity 中通过 CompositionLocalProvider 提供全局对话框状态
 * 所有子组件都能访问同一份状态数据
 */
abstract class BaseComposeActivity : ComponentActivity() {
	//...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AppTheme {
                CompositionLocalProvider(
                    DialogManager.LocalDialog provides DialogManager.dialogState
                ) {
                    ProviderContent()

                    DialogManager.GlobalDialog()
                }
            }
        }
    }

    @Composable
    abstract fun ProviderContent()
    //...
}

②. 使用

kotlin 复制代码
// 任意需要显示性别选择对话框调用
val dialogId = DialogManager.showSexPickerDialog(defValue = 0) { selectedValue ->
    // 处理选择结果
    when(selectedValue) {
        0 -> { /* 男性 */ }
        1 -> { /* 女性 */ }
    }
}

// 需要关闭对话框
DialogManager.dismissDialogById(dialogId)

③. 效果

总结

这个 DialogManager 设计模式提供了一个完整的解决方案,不仅解决了传统 Dialog 实现的问题,还提供了良好的扩展性和维护性。通过抽象化设计,使得新增 Dialog 类型变得简单,同时保持了代码的整洁和一致性。这种模式特别适合需要大量交互界面的复杂应用。

相关推荐
容华谢后2 小时前
Android消息推送 MQTT方案实践
android
_李小白2 小时前
【Android 美颜相机】第十五天:GPUImage3x3TextureSamplingFilter 解析
android·数码相机
Whisper_Sy2 小时前
Flutter for OpenHarmony移动数据使用监管助手App实战 - 月报告实现
android·开发语言·javascript·网络·flutter·ecmascript
天才少年曾牛2 小时前
Android 怎么写一个AIDL接口?
android
冬奇Lab3 小时前
【Kotlin系列14】编译器插件与注解处理器开发:在编译期操控Kotlin
android·开发语言·kotlin·状态模式
橘子133 小时前
MySQL表的约束(五)
android·mysql·adb
2501_915918413 小时前
Wireshark、Fiddler、Charles抓包工具详细使用指南
android·ios·小程序·https·uni-app·iphone·webview
aaa最北边3 小时前
进程间通信-1.管道通信
android·java·服务器
灰灰勇闯IT4 小时前
【Flutter for OpenHarmony--Dart 入门日记】第3篇:基础数据类型全解析——String、数字与布尔值
android·java·开发语言