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

Clean Architecture(MVVM)下跨ViewModel传递UserData至UI State最优方案

问题描述

我有一个MainViewModel,它会根据AuthState和当前用户uid,通过UseCase与Repository从Firestore获取UserData,并以StateFlow形式提供给所有屏幕更新UI。另外有一个编辑资料的ProfileScreen,对应EditProfileViewModel与EditProfileUIState。

现在需要将MainViewModel中的UserData传入EditProfileUIState,供用户修改后由EditProfileViewModel更新至Firestore。目前我在Composable中通过LaunchEffect传递数据,但不确定该方案是否合理。想知道在Clean Architecture(MVVM)架构下,实现该需求的最优方式是什么?是否可通过UseCase实现?我不想在每个需要UserData的ViewModel中重复处理响应逻辑。

现有代码

MainViewModel.kt

private val _userData = MutableStateFlow<UserData?>(null)
val userData: StateFlow<UserData?> = _userData

init {
    getAuthState()
}

private fun getAuthState(){
    viewModelScope.launch {
        authUseCases.getAuthState(viewModelScope).collectLatest {
            when(it){
                true -> {
                    _userData.update { null }
                }
                false -> {
                    authUseCases.currentUser()?.uid?.let { userId ->
                        getUserData(userId)
                    } ?: Log.d("PROVES","CAN'T GET USER")
                }
            }
        }
    }
}

private fun getUserData(userId: String){
    Log.d("PROVES","GetUserData")
    viewModelScope.launch {
        userUseCases.getUserData(userId)
            .collectLatest { response ->
            when(response) {
                is Success -> {
                    _userData.update { response.data }
                    Log.d("PROVES","${response.data}")
                }
                is Failure -> {
                    Log.d("PROVES","GET DATA FAILURE RESPONSE")
                }
            }
        }
    }
}

ProfileEditUIState.kt

data class ProfileEditUIState (val isLoading: Boolean = false, val userData: UserData? = null)

ProfileEditViewModel.kt

private val _profileEditUiState = MutableStateFlow(ProfileEditUIState())
val profileEditUiState: StateFlow<ProfileEditUIState> = _profileEditUiState.asStateFlow()

fun onEvent(event: ProfileEditUIEvent) {
    when(event) {
        is ProfileEditUIEvent.UserDataLoaded -> {
            _profileEditUiState.update {
                it.copy(userData = event.userData)
            }
        }
    }
}

屏幕Composable代码

fun ProfileEditContent(
    modifier: Modifier = Modifier,
    mainViewModel: MainViewModel = hiltViewModel(),
    profileEditViewModel: ProfileEditViewModel = hiltViewModel(),
    snackbarHostState: SnackbarHostState,
    onSignInClick: () -> Unit,
) {

    val state by profileEditViewModel.profileEditUiState.collectAsStateWithLifecycle()
    val userData by mainViewModel.userData.collectAsStateWithLifecycle()

    userData?.let {
        LaunchedEffect(true) {
            profileEditViewModel.onEvent(ProfileEditUIEvent.UserDataLoaded(it))
        }
    }
...
最优实现方案

当前方案的问题

你现在用LaunchedEffect(true)传递数据的方式存在两个明显问题:

  1. 关注点分离缺失:Composable层承担了ViewModel间通信的职责,不符合MVVM分层原则,增加了UI层的复杂度。
  2. 数据更新不及时:LaunchedEffect(true)只会执行一次,如果MainViewModel中的UserData后续发生变化,EditProfileViewModel无法感知并更新。

推荐方案:让EditProfileViewModel直接依赖UseCase获取UserData(符合Clean Architecture)

这种方案完全遵循分层原则,每个ViewModel只依赖对应UseCase,避免ViewModel间耦合,同时复用已有的getUserData UseCase,不用重复编写响应逻辑。

步骤1:修改EditProfileViewModel,直接获取UserData

class EditProfileViewModel @Inject constructor(
    private val authUseCases: AuthUseCases,
    private val userUseCases: UserUseCases
) : ViewModel() {
    private val _profileEditUiState = MutableStateFlow(ProfileEditUIState())
    val profileEditUiState: StateFlow<ProfileEditUIState> = _profileEditUiState.asStateFlow()

    init {
        loadUserData()
    }

    private fun loadUserData() {
        viewModelScope.launch {
            // 获取当前用户uid
            authUseCases.currentUser()?.uid?.let { userId ->
                // 复用已有的getUserData UseCase
                userUseCases.getUserData(userId).collectLatest { response ->
                    handleUserDataResponse(response)
                }
            }
        }
    }

    // 统一处理响应逻辑,可复用至其他ViewModel
    private fun handleUserDataResponse(response: Result<UserData>) {
        when(response) {
            is Success -> {
                _profileEditUiState.update {
                    it.copy(userData = response.data)
                }
            }
            is Failure -> {
                // 这里可以统一处理失败逻辑,比如记录日志、发送错误事件等
                Log.d("PROVES","GET DATA FAILURE RESPONSE")
            }
        }
    }

    // 处理编辑提交逻辑
    fun onProfileSubmit(updatedUserData: UserData) {
        viewModelScope.launch {
            userUseCases.updateUserData(updatedUserData).collectLatest { response ->
                // 同样复用统一的响应处理逻辑
                when(response) {
                    is Success -> { /* 处理成功,比如弹出提示 */ }
                    is Failure -> { /* 处理失败 */ }
                }
            }
        }
    }
}

步骤2:优化响应逻辑复用(可选)

如果多个ViewModel都需要处理类似的Result响应,可以把响应处理封装成扩展函数,彻底消除重复代码:

// 扩展函数:统一处理Result类型的Flow响应
fun <T> Flow<Result<T>>.handleResult(
    viewModelScope: CoroutineScope,
    onSuccess: (T) -> Unit,
    onFailure: (() -> Unit)? = null
) = viewModelScope.launch {
    collectLatest { response ->
        when(response) {
            is Success -> onSuccess(response.data)
            is Failure -> onFailure?.invoke() ?: Log.d("PROVES","OPERATION FAILURE")
        }
    }
}

然后在ViewModel中简化调用:

private fun loadUserData() {
    authUseCases.currentUser()?.uid?.let { userId ->
        userUseCases.getUserData(userId).handleResult(
            viewModelScope = viewModelScope,
            onSuccess = { data ->
                _profileEditUiState.update { it.copy(userData = data) }
            },
            onFailure = { /* 自定义失败处理 */ }
        )
    }
}

步骤3:简化Composable代码

现在Composable无需再处理ViewModel间的通信,直接依赖EditProfileViewModel即可:

fun ProfileEditContent(
    modifier: Modifier = Modifier,
    profileEditViewModel: ProfileEditViewModel = hiltViewModel(),
    snackbarHostState: SnackbarHostState,
    onSignInClick: () -> Unit,
) {
    val state by profileEditViewModel.profileEditUiState.collectAsStateWithLifecycle()
    
    // 直接使用state.userData渲染UI
    ...
}

备选方案:使用共享的User状态管理类

如果你的App中有大量屏幕需要实时获取UserData,可以创建一个单例的UserManager类(通过Hilt注入为Singleton),由MainViewModel负责更新它的状态,其他ViewModel直接依赖UserManager获取最新的UserData:

@Singleton
class UserManager @Inject constructor() {
    private val _userData = MutableStateFlow<UserData?>(null)
    val userData: StateFlow<UserData?> = _userData

    fun updateUserData(data: UserData?) {
        _userData.value = data
    }
}

然后修改MainViewModel,更新UserManager的状态:

class MainViewModel @Inject constructor(
    private val authUseCases: AuthUseCases,
    private val userUseCases: UserUseCases,
    private val userManager: UserManager
) : ViewModel() {
    // 移除原有的_userData,直接复用userManager的state
    val userData = userManager.userData

    private fun getUserData(userId: String){
        viewModelScope.launch {
            userUseCases.getUserData(userId)
                .collectLatest { response ->
                when(response) {
                    is Success -> {
                        userManager.updateUserData(response.data)
                    }
                    is Failure -> {
                        Log.d("PROVES","GET DATA FAILURE RESPONSE")
                    }
                }
            }
        }
    }
}

EditProfileViewModel直接依赖UserManager:

class EditProfileViewModel @Inject constructor(
    private val userManager: UserManager,
    private val userUseCases: UserUseCases
) : ViewModel() {
    private val _profileEditUiState = MutableStateFlow(ProfileEditUIState())
    val profileEditUiState: StateFlow<ProfileEditUIState> = _profileEditUiState.asStateFlow()

    init {
        viewModelScope.launch {
            userManager.userData.collectLatest { userData ->
                _profileEditUiState.update { it.copy(userData = userData) }
            }
        }
    }
}

这种方案适合需要全局共享用户状态的场景,但要注意避免过度使用单例导致状态管理复杂化。

总结
  • 最优方案:让EditProfileViewModel直接依赖getUserData UseCase获取数据,符合Clean Architecture的分层原则,职责清晰,复用性强,避免了ViewModel间的耦合。
  • 避免重复逻辑:通过封装统一的响应处理扩展函数,消除多个ViewModel中的重复代码。
  • 不推荐原方案:原有的LaunchEffect传递数据方式违反了MVVM分层原则,且无法处理数据更新的情况。

内容的提问来源于stack exchange,提问作者Alex Diaz Llagostera

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 00:45:02