如何拆分复杂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
相关产品推荐
相关产品推荐

