文章目录
- 前言
- [一、传统 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
CommonDialog 、WheelPicker 参考之前的博客
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 类型变得简单,同时保持了代码的整洁和一致性。这种模式特别适合需要大量交互界面的复杂应用。