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

Android ViewModel链式UI状态实现及双文件加载异常排查

问题排查与解决方案

一、当前代码的核心问题

你遇到的UI只显示最后一个文件内容的原因是ViewModel的StateFlow维护的是单一状态,两次调用loadData会直接覆盖之前的状态:

  1. 第一次调用loadData(termsRequest)时,_uiState先被设为Loading,待请求完成后更新为Loaded(terms数据);
  2. 紧接着调用loadData(privacyRequest),_uiState被立即覆盖为Loading,直接覆盖了之前的Loaded(terms数据)状态;
  3. 当隐私文件请求完成后,_uiState更新为Loaded(privacy数据),而Fragment收集的是StateFlow的最新值,因此只会处理最后一次的Loaded状态,导致条款文件的内容从未被UI正确处理(即使数据层已加载成功)。

二、基于Clean Architecture的链式UI状态实现方案

按照Clean Architecture的分层原则(数据层→域层→UI层),我们需要让ViewModel维护聚合式的UI状态,同时追踪多个文件的加载状态,确保每个文件的加载结果都能被UI感知到。核心思路是:

  • 让UI状态支持多文件的独立状态追踪(加载中、成功、错误);
  • 每次加载请求更新对应文件的状态,而非覆盖整个状态;
  • UI层根据聚合状态中的各个文件状态,分别更新对应的TextView。

修改后的代码实现

1. 调整ViewModel的状态管理逻辑

@HiltViewModel
class FileViewModel @Inject constructor(
    private val getFileUseCase: GetFileUseCase,
    private val coroutineDispatcherProvider: CoroutineDispatcherProvider
) : ViewModel() {

    // 单个文件的状态枚举
    private sealed class FileItemStatus {
        object Loading : FileItemStatus()
        data class Loaded(val data: FileItemUiState) : FileItemStatus()
        data class Error(@StringRes val message: Int) : FileItemStatus()
    }

    // 用Map存储所有文件的状态,key为文件名,value为对应状态
    private val _uiState = MutableStateFlow<Map<String, FileItemStatus>>(emptyMap())
    val uiState: StateFlow<Map<String, FileItemStatus>> = _uiState

    fun loadData(fileRequest: FileRequest) {
        val fileName = fileRequest.fileName
        // 先标记该文件为加载中状态
        _uiState.update { currentState ->
            currentState + (fileName to FileItemStatus.Loading)
        }

        viewModelScope.launch(coroutineDispatcherProvider.IO()) {
            try {
                val result = getFileUseCase.execute(fileRequest)
                // 请求成功:更新该文件的成功状态
                _uiState.update { currentState ->
                    currentState + (fileName to FileItemStatus.Loaded(FileItemUiState(result)))
                }
            } catch (error: Exception) {
                // 请求失败:更新该文件的错误状态
                _uiState.update { currentState ->
                    currentState + (fileName to FileItemStatus.Error(ExceptionParser.getMessage(error)))
                }
            }
        }
    }
}

2. Fragment中处理聚合状态

// 发起加载请求
val termsRequest = FileRequest("raw/terms_202301.json", isNightMode, isVoiceOn = false, false)
mViewModel.loadData(termsRequest)

val privacyRequest = FileRequest("raw/privacy_202301.json", isNightMode, isVoiceOn = false, false)
mViewModel.loadData(privacyRequest)

fetchData()

// ....

private fun fetchData() {
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            mViewModel.uiState.collect { fileStates ->
                // 处理条款文件的状态更新
                when (val termsState = fileStates["raw/terms_202301.json"]) {
                    is FileViewModel.FileItemStatus.Loaded -> {
                        binding?.textTerms?.text = termsState.data.allData.text
                    }
                    is FileViewModel.FileItemStatus.Error -> {
                        showError(termsState.message)
                    }
                    FileViewModel.FileItemStatus.Loading -> {
                        // 可单独显示条款区域的加载动画
                    }
                    null -> {}
                }

                // 处理隐私文件的状态更新
                when (val privacyState = fileStates["raw/privacy_202301.json"]) {
                    is FileViewModel.FileItemStatus.Loaded -> {
                        binding?.textPrivacy?.text = privacyState.data.allData.text
                    }
                    is FileViewModel.FileItemStatus.Error -> {
                        showError(privacyState.message)
                    }
                    FileViewModel.FileItemStatus.Loading -> {
                        // 可单独显示隐私区域的加载动画
                    }
                    null -> {}
                }
            }
        }
    }
}

private fun showError(@StringRes stringRes: Int) {
    Toast.makeText(requireContext(), stringRes, Toast.LENGTH_SHORT).show()
}

方案符合Clean Architecture的要点

  • 域层隔离:文件加载逻辑封装在GetFileUseCase中,ViewModel仅负责调用用例并转换为UI状态,不包含业务逻辑;
  • 单向数据流:数据从数据层→域层→ViewModel→UI层单向流动,UI仅通过收集StateFlow接收状态更新;
  • 单一可信数据源:ViewModel维护聚合的文件状态Map,确保UI始终与最新状态同步;
  • 可扩展性:新增文件加载请求时,只需在UI层添加对应状态处理,无需修改核心状态管理逻辑。

内容的提问来源于stack exchange,提问作者A. Cedano

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 01:14:59