如何在 ViewModel 中处理 ApiResponse,并选择 Flow 或 suspend?
一次性查询可以使用 suspend fun,再由 ViewModel 通过 StateFlow 向界面提供状态。
如果保留现有的 Flow<ApiResponse<Day>>,可以直接映射它。映射结果需要表达加载、成功和错误三种情况,不要把每种响应都强行转换成只包含业务数据的 DayState。
为什么当前的转换会出错?
Success 分支返回的是 DayState,但两个空分支 {} 的结果是 Unit。末尾的 as DayState 只是强制类型转换,无法把 Unit 变成 DayState,所以处理加载或错误状态时会出现类型转换异常。
删除这个强制转换,并明确区分业务数据与界面状态。下面假设现有的 DayState 用于保存日期、时间、备注等数据。
保留 Flow 时,映射成完整的界面状态
可以在 DayState 外增加一层状态类型:
sealed interface DayUiState {
object Loading : DayUiState
data class Success(val day: DayState) : DayUiState
data class Error(val message: String?) : DayUiState
}
fun Day.toDayState(): DayState = DayState(
date = date,
activeTimeStart = activeTimeStart,
activeTimeEnd = activeTimeEnd,
actualActiveTimeStart = actualActiveTimeStart,
actualActiveTimeEnd = actualActiveTimeEnd,
note = note
)
fun toDayUiState(response: ApiResponse<Day>): DayUiState =
when (response) {
is ApiResponse.Success ->
DayUiState.Success(response.data.toDayState())
ApiResponse.Loading -> DayUiState.Loading
is ApiResponse.Error ->
DayUiState.Error(response.message)
}
每个分支都返回 DayUiState,不需要 as。
在 ViewModel 中,可以用 stateIn 将映射后的 Flow 转成供界面观察的 StateFlow:
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
// 放在现有 ViewModel 内
val uiState: StateFlow<DayUiState> =
daysRepository
.getDayStream(input.selectedDate.toString())
.map(::toDayUiState)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = DayUiState.Loading
)
这里沿用问题中的 getDayStream 调用;如果仓库实际使用的是 selectDayByDate,替换成对应方法即可。
界面根据状态显示进度、数据或错误提示。加载失败时没有新的 DayState 是正常情况,不需要创建空对象来凑齐返回值。
使用时要注意:
flow { ... }创建的是冷流,收集时才执行查询。上述共享策略在订阅停止超过指定时间后会停止上游,后续重新订阅可能再次查询。5_000是示例配置,需要按页面需求选择。- 这段代码在创建 Flow 时读取所选日期,之后日期发生变化,查询参数不会自动更新。如果需要随日期变化查询,可以将日期作为状态流,再使用
flatMapLatest切换请求。
一次性查询可以改成 suspend 函数
如果仓库只负责发起一次查询并返回结果,可以将接口简化为 suspend fun,由 ViewModel 管理加载和错误状态。
仓库实现可以沿用原有的 Supabase 查询:
suspend fun selectDayByDate(date: String): Day {
return supabaseClient.from("public.days").select() {
filter {
Day::date eq date
}
}.decodeSingle<Day>()
}
仓库成功时返回 Day,失败时抛出异常。ViewModel 使用前面的 DayUiState:
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
// 放在现有 ViewModel 内
private val _uiState =
MutableStateFlow<DayUiState>(DayUiState.Loading)
val uiState: StateFlow<DayUiState> = _uiState.asStateFlow()
private var loadJob: Job? = null
fun loadDay(date: String) {
loadJob?.cancel()
loadJob = viewModelScope.launch {
_uiState.value = DayUiState.Loading
try {
val day = daysRepository.selectDayByDate(date)
_uiState.value = DayUiState.Success(day.toDayState())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_uiState.value = DayUiState.Error(e.message)
}
}
}
页面需要加载或重试时调用 loadDay(date)。示例会取消上一次加载任务,适用于只关心最近一次日期选择的场景。初始化时是否调用它,由页面的加载流程决定。
这个方案中,仓库提供一次性结果,StateFlow 保存界面当前状态。使用 ViewModel 并不要求所有仓库方法都返回 Flow。
原有 Flow 代码还需要保留协程取消语义
原代码的 catch (e: Exception) 也可能捕获 CancellationException。协程取消应继续向上传播,不应转换成普通的 API 错误。
如果保留原来的 Flow 实现,需要调整异常处理:
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
emit(ApiResponse.Error(e.message))
}
这里需要导入 kotlinx.coroutines.CancellationException。如果能区分网络、解码等具体异常,也可以进一步缩小捕获范围。
应该选哪一种?
对于当前的一次性 Supabase 查询,可以让仓库返回 suspend fun ...: Day,由 ViewModel 暴露 StateFlow<DayUiState>。
现有的 Flow<ApiResponse<Day>> 也能使用,它发出了加载和查询结果两个阶段的事件,但不会因此持续监听数据库变化。如果仓库需要提供数据库观察、实时订阅或其他持续更新,可以使用 Flow 表达这些后续数据。
示例中的互斥状态会在加载或失败时替换成功状态。如果希望刷新时继续展示旧数据,可以改用包含 day: DayState?、isLoading 和 errorMessage 等字段的状态对象,让旧数据与刷新状态同时存在。
备注:内容仅供参考。
