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

如何合理编写Android ViewModel并迁移其中的业务逻辑?

问题描述

我正在Android/Kotlin中使用MVVM、ViewModel、ViewBinding和Retrofit2开发。我不知道如何将业务逻辑从ViewModel中迁移出去——这些逻辑运行在viewModelScope中,结果还要存入ViewModel里的可观察对象(比如LiveData)。但或许其实是可以做到的?

举个例子,我有一个用于ListView展示的ArrayList:

// items ArrayList
private val _stocktakings = 
    MutableLiveData<ArrayList<InventoryStocktakingWithCountsDto?>>(ArrayList())
val stocktakings : LiveData<ArrayList<InventoryStocktakingWithCountsDto?>> get() = _stocktakings

// selected item
private val _selected_stocktaking = MutableLiveData<Int>()
val selected_stocktaking : LiveData<Int> get() = _selected_stocktaking 

还有一个从Fragment中调用的函数:

public fun loadStocktakings() {
    viewModelScope.launch {
        Log.d(TAG, "Load stocktakings requested")
        clearError()
        try {
            with(ApiResponse(ApiAdapter.apiClient.findAllStocktakings())){
                if (isSuccessful && body != null){
                    Log.d(TAG, "Load Stocktakings done")
                    setStocktakings(body)
                } else {
                    val e = "Can't load stocktakings, API error: {${errorMessage}}"
                    Log.e(TAG, e)
                    HandleError("Can't load stocktakings, API error: {${e}}") // 将错误信息存入lastError这个MutableLiveData...
                }
            }
        } catch (t : Throwable) {
            Log.e(TAG, "Can't load stocktakings, connectivity error: ${t.message}")
            HandleError("Can't load stocktakings, API error: {${e}}") // 将错误信息存入lastError这个MutableLiveData...
        }
    }
}

现在我想添加另一个函数来修改stocktakings中的某个字段,大概是这样:

public fun setSelectedStocktakingComplete() {
    stocktakings.value[selected_stocktaking.value].isComplete = true;

    // 调用某个API函数... 又要写15行代码?
}

正确的做法是什么?

我感觉自己看了错误的教程,这样下去ViewModel会变得臃肿不堪,全是viewModelScope.launch、错误处理代码,我甚至不敢想象添加数据/表单验证后会变成什么样……


解决方案

1. 用Repository层封装数据操作

把所有和API、本地数据相关的逻辑抽离到独立的Repository类中,ViewModel只负责调用Repository的方法,不直接处理Retrofit请求或数据解析。

示例StocktakingRepository:

class StocktakingRepository(private val apiClient: ApiClient) {
    suspend fun loadAllStocktakings(): Result<List<InventoryStocktakingWithCountsDto>> {
        return try {
            val response = apiClient.findAllStocktakings()
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("API错误: ${response.errorMessage}"))
            }
        } catch (t: Throwable) {
            Result.failure(Exception("网络连接错误: ${t.message}"))
        }
    }

    suspend fun markStocktakingAsComplete(stocktakingId: Int): Result<Unit> {
        // 这里实现调用API标记完成的逻辑,用Result统一封装结果
        return try {
            val response = apiClient.markStocktakingComplete(stocktakingId)
            if (response.isSuccessful) {
                Result.success(Unit)
            } else {
                Result.failure(Exception("标记完成失败: ${response.errorMessage}"))
            }
        } catch (t: Throwable) {
            Result.failure(Exception("网络连接错误: ${t.message}"))
        }
    }
}

2. 用UseCase封装单一业务逻辑(可选但推荐)

如果业务逻辑复杂(比如表单验证、多数据源组合),可以再加一层UseCase,把每个独立的业务逻辑封装成单独的UseCase类,ViewModel调用UseCase,UseCase再调用Repository。

示例UseCase:

class LoadStocktakingsUseCase(private val repository: StocktakingRepository) {
    suspend operator fun invoke(): Result<List<InventoryStocktakingWithCountsDto>> {
        // 这里可添加额外业务逻辑,比如数据过滤、缓存校验等
        return repository.loadAllStocktakings()
    }
}

class MarkStocktakingCompleteUseCase(private val repository: StocktakingRepository) {
    suspend operator fun invoke(stocktakingId: Int): Result<Unit> {
        // 前置验证逻辑,比如检查ID是否合法
        if (stocktakingId < 0) {
            return Result.failure(Exception("无效的库存盘点ID"))
        }
        return repository.markStocktakingAsComplete(stocktakingId)
    }
}

3. ViewModel简化为协调者角色

ViewModel只负责以下核心工作:

  • 持有LiveData,向View层暴露状态
  • 调用Repository/UseCase获取数据
  • 根据返回结果更新LiveData和错误状态

改造后的ViewModel:

class StocktakingViewModel(
    private val loadStocktakingsUseCase: LoadStocktakingsUseCase,
    private val markCompleteUseCase: MarkStocktakingCompleteUseCase
) : ViewModel() {
    private val _stocktakings = MutableLiveData<List<InventoryStocktakingWithCountsDto>>(emptyList())
    val stocktakings: LiveData<List<InventoryStocktakingWithCountsDto>> get() = _stocktakings

    private val _selectedStocktaking = MutableLiveData<Int>()
    val selectedStocktaking: LiveData<Int> get() = _selectedStocktaking

    private val _lastError = MutableLiveData<String?>()
    val lastError: LiveData<String?> get() = _lastError

    fun loadStocktakings() {
        viewModelScope.launch {
            _lastError.value = null
            when (val result = loadStocktakingsUseCase()) {
                is Result.Success -> _stocktakings.value = result.data
                is Result.Failure -> _lastError.value = result.exception.message
            }
        }
    }

    fun setSelectedStocktakingComplete() {
        viewModelScope.launch {
            _lastError.value = null
            val selectedId = _selectedStocktaking.value ?: return@launch
            when (val result = markCompleteUseCase(selectedId)) {
                is Result.Success -> {
                    // 更新本地LiveData中的数据,注意用不可变集合保证数据安全
                    val currentList = _stocktakings.value?.toMutableList()
                    currentList?.find { it?.id == selectedId }?.isComplete = true
                    _stocktakings.value = currentList
                }
                is Result.Failure -> _lastError.value = result.exception.message
            }
        }
    }
}

4. 封装通用逻辑减少重复

把viewModelScope中的重复逻辑(比如清空错误、处理Result)封装成扩展函数,进一步简化ViewModel代码:

fun <T> ViewModel.launchWithErrorHandling(
    errorLiveData: MutableLiveData<String?>,
    block: suspend () -> Result<T>,
    onSuccess: (T) -> Unit
) {
    viewModelScope.launch {
        errorLiveData.value = null
        when (val result = block()) {
            is Result.Success -> onSuccess(result.data)
            is Result.Failure -> errorLiveData.value = result.exception.message ?: "未知错误"
        }
    }
}

使用扩展函数后,ViewModel的方法可以简化为:

fun loadStocktakings() {
    launchWithErrorHandling(_lastError, loadStocktakingsUseCase) { data ->
        _stocktakings.value = data
    }
}

5. 优化LiveData的数据结构

把ArrayList换成不可变的List,避免外部直接修改数据,确保所有数据更新都通过ViewModel的方法触发,保证数据流的可控性和一致性。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 17:15:43