You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何拆分复杂ViewModel为可复用模块?基于MVVM与Jetpack Compose

解决Jetpack Compose复用弹窗时ViewModel动作逻辑重复的问题

针对你遇到的复用弹窗导致多个ViewModel重复实现相同接口方法的问题,推荐使用独立状态处理器+组合模式,将弹窗的交互逻辑从ViewModel中剥离,实现逻辑复用,同时保持MVVM架构的清晰性。以下是具体实现方案:

核心思路

创建一个独立的DialogStateHandler类,封装弹窗所有的UI状态管理和交互逻辑(即原ExampleDialogActions接口的所有实现),ViewModel通过持有该处理器来复用逻辑,无需重复实现接口方法。这种方式既分离了关注点(ViewModel负责业务层交互,处理器负责弹窗UI逻辑),又能让多个ViewModel共用同一套弹窗逻辑。

代码实现

1. 独立弹窗状态处理器

将原ViewModel中弹窗相关的状态更新逻辑全部迁移到这个处理器中,同时实现ExampleDialogActions接口:

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

class ExampleDialogStateHandler(
    // 传入自定义校验逻辑,支持不同场景的个性化需求
    private val validateAmount: (String) -> ValidationResult,
    initialCurrency: Currency
) : ExampleDialogActions {
    // 弹窗UI状态
    private val _uiState = MutableStateFlow(
        ExampleDialogUIState.initialState.copy(currency = initialCurrency)
    )
    val uiState: StateFlow<ExampleDialogUIState> = _uiState.asStateFlow()

    // 初始化弹窗数据:从主实体加载列表
    fun initializeDialog(items: List<YourEntityListItem>) {
        _uiState.update { currentState ->
            currentState.copy(
                visible = true,
                exampleItems = items.map { entityItem ->
                    ExampleItem(
                        id = entityItem.id,
                        amount = entityItem.amount.toString(),
                        amountInputIsError = false,
                        // 映射其他实体字段到弹窗Item
                    )
                }
            )
        }
    }

    // 获取编辑后的列表,供ViewModel保存到Room
    fun getEditedItems(): List<ExampleItem> = _uiState.value.exampleItems

    // 实现ExampleDialogActions接口的所有方法
    override fun onExampleAmountChanged(exampleItem: ExampleItem, value: String) {
        _uiState.update { currentState ->
            val updatedItems = currentState.exampleItems.toMutableList()
            val index = updatedItems.indexOfFirst { it.id == exampleItem.id }
            if (index != -1) {
                updatedItems[index] = updatedItems[index].copy(
                    amount = value,
                    amountInputIsError = !validateAmount(value).status
                )
            }
            currentState.copy(exampleItems = updatedItems)
        }
    }

    override fun onExamplePeriodCountChanged(exampleItem: ExampleItem, value: String) {
        _uiState.update { currentState ->
            val updatedItems = currentState.exampleItems.toMutableList()
            val index = updatedItems.indexOfFirst { it.id == exampleItem.id }
            if (index != -1) {
                updatedItems[index] = updatedItems[index].copy(periodCount = value)
                // 可添加自定义校验逻辑
            }
            currentState.copy(exampleItems = updatedItems)
        }
    }

    override fun onExampleAddClicked() {
        _uiState.update { currentState ->
            val newItem = ExampleItem(
                id = java.util.UUID.randomUUID().mostSignificantBits, // 临时ID,保存时替换
                amount = "",
                amountInputIsError = false,
                // 设置其他默认字段
            )
            currentState.copy(exampleItems = currentState.exampleItems + newItem)
        }
    }

    override fun onExampleDeleteClicked(exampleItem: ExampleItem) {
        _uiState.update { currentState ->
            currentState.copy(exampleItems = currentState.exampleItems.filter { it.id != exampleItem.id })
        }
    }

    override fun onCurrencySelected(currency: Currency) {
        _uiState.update { it.copy(currency = currency) }
    }

    override fun onDismissExampleDialog() {
        _uiState.update { it.copy(visible = false) }
    }

    // 实现其他接口方法(如onInfoClicked、onDismissInfoDialog等)
    override fun onInfoClicked() {
        _uiState.update { it.copy(showInfoDialog = true) }
    }

    override fun onDismissInfoDialog() {
        _uiState.update { it.copy(showInfoDialog = false) }
    }

    // ... 剩余接口方法实现
}

// 辅助校验结果类(根据你的实际需求调整)
sealed class ValidationResult {
    object Valid : ValidationResult()
    data class Invalid(val message: String) : ValidationResult()
    val status get() = this is Valid
}

2. 修改ViewModel,复用处理器逻辑

ViewModel不再直接实现ExampleDialogActions,而是持有处理器实例,仅负责业务层交互(如从Room加载实体、保存编辑结果):

import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class ExampleViewModel @Inject constructor(
    private val exampleRepository: ExampleRepository,
    private val currencyRepository: CurrencyRepository,
    private val preferencesManager: DefaultPreferencesManager,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val _mainEntity = MutableStateFlow<YourMainEntity?>(null)
    val mainEntity: StateFlow<YourMainEntity?> = _mainEntity

    // 初始化弹窗处理器,传入个性化校验逻辑和初始货币
    private val dialogHandler = ExampleDialogStateHandler(
        validateAmount = ::validateExampleAmount,
        initialCurrency = DefaultCurrency
    )

    // 暴露弹窗UI状态给Composable
    val exampleDialogUIState: StateFlow<ExampleDialogUIState> = dialogHandler.uiState

    // 暴露弹窗动作,直接指向处理器
    val exampleActions: ExampleDialogActions = dialogHandler

    // 打开弹窗:从主实体加载数据
    fun showExampleDialog() {
        _mainEntity.value?.let { entity ->
            dialogHandler.initializeDialog(entity.someList)
        }
    }

    // 保存编辑结果到Room
    fun saveEditedItems() {
        val editedItems = dialogHandler.getEditedItems()
        _mainEntity.value?.let { entity ->
            val updatedEntity = entity.copy(
                someList = editedItems.map { dialogItem ->
                    // 将弹窗Item映射回实体Item
                    YourEntityListItem(
                        id = dialogItem.id,
                        amount = dialogItem.amount.toDoubleOrNull() ?: 0.0,
                        // 映射其他字段
                    )
                }
            )
            viewModelScope.launch {
                exampleRepository.updateEntity(updatedEntity)
            }
        }
        dialogHandler.onDismissExampleDialog()
    }

    // 自定义金额校验逻辑
    private fun validateExampleAmount(value: String): ValidationResult {
        return if (value.isNotEmpty() && value.toDoubleOrNull() != null) {
            ValidationResult.Valid
        } else {
            ValidationResult.Invalid("金额格式不正确")
        }
    }
}

3. 其他复用弹窗的ViewModel

其他需要使用该弹窗的ViewModel只需创建处理器实例,无需重复实现12个接口方法:

@HiltViewModel
class AnotherViewModel @Inject constructor(
    private val anotherRepository: AnotherRepository,
    private val currencyRepository: CurrencyRepository,
    preferencesManager: DefaultPreferencesManager,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val _anotherEntity = MutableStateFlow<AnotherMainEntity?>(null)
    val anotherEntity: StateFlow<AnotherMainEntity?> = _anotherEntity

    private val dialogHandler = ExampleDialogStateHandler(
        validateAmount = ::validateAnotherAmount,
        initialCurrency = DefaultCurrency
    )

    val exampleDialogUIState: StateFlow<ExampleDialogUIState> = dialogHandler.uiState
    val exampleActions: ExampleDialogActions = dialogHandler

    fun showExampleDialog() {
        _anotherEntity.value?.let { entity ->
            dialogHandler.initializeDialog(entity.anotherList)
        }
    }

    fun saveEditedItems() {
        val editedItems = dialogHandler.getEditedItems()
        _anotherEntity.value?.let { entity ->
            val updatedEntity = entity.copy(
                anotherList = editedItems.map { dialogItem ->
                    AnotherEntityListItem(
                        id = dialogItem.id,
                        amount = dialogItem.amount.toDoubleOrNull() ?: 0.0,
                        // 映射其他字段
                    )
                }
            )
            viewModelScope.launch {
                anotherRepository.updateEntity(updatedEntity)
            }
        }
        dialogHandler.onDismissExampleDialog()
    }

    // 该页面的个性化校验逻辑
    private fun validateAnotherAmount(value: String): ValidationResult {
        return if (value.isNotEmpty() && value.toDouble() > 0) {
            ValidationResult.Valid
        } else {
            ValidationResult.Invalid("金额必须大于0")
        }
    }
}

4. Composable层无需修改

原有的ScreenRoute和ExampleDialog代码保持不变,因为exampleActions现在指向处理器实例,完全兼容原接口定义:

@Composable
fun ScreenRoute(
    viewModel: ExampleViewModel = hiltViewModel(),
) {
    val exampleDialogUIState by viewModel.exampleDialogUIState.collectAsStateWithLifecycle()
    val exampleActions: ExampleDialogActions = viewModel.exampleActions

    if (exampleDialogUIState.visible) {
        ExampleDialog(
            uiState = exampleDialogUIState,
            actions = exampleActions,
            // 其他参数
        )
    }
}

方案优势

  • 逻辑复用:所有弹窗交互逻辑集中在处理器中,多个ViewModel无需重复实现接口方法。
  • 关注点分离:ViewModel专注于业务逻辑(数据加载、保存),处理器专注于弹窗UI状态管理,符合单一职责原则。
  • 可测试性:处理器的逻辑可以单独编写单元测试,无需依赖ViewModel和Repository。
  • 扩展性:如需修改弹窗交互逻辑,仅需修改处理器,所有使用该弹窗的ViewModel自动受益;如需支持不同类型的列表,可将处理器改为泛型类进一步扩展。

内容的提问来源于stack exchange,提问作者Foenix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 02:06:04