我有以下设置。
我有一个带有项目列表的屏幕(PlantsScreen)。单击列表中的某个项目时,我将导航到另一个屏幕(AddEditPlantScreen)。编辑并保存项目并导航回列表屏幕后,我想显示更新的项目列表。但列表中显示的不是更新后的列表,而是项目编辑前的列表。
为了拥有单一的事实来源,我从 node.js 后端获取数据,然后将其保存到本地存储库 (Room)。我认为我需要刷新 ViewModel 中的状态才能从我的存储库中获取更新的列表。
我知道我可以使用作业来执行此操作,但它会引发错误。返回 Flow 时这是正确的方法吗?
如果是,我该如何实现这一点。
如果没有,我有什么替代方法?
plantsListViewModel.kt
private val _state = mutableStateOf<PlantsState>(PlantsState())
val state: State<PlantsState> = _state
init {
getPlants(true, "")
}
private fun getPlants(fetchFromBackend: Boolean, query: String) {
viewModelScope.launch {
plantRepository.getPlants(fetchFromBackend, query)
.collect { result ->
when (result) {
is Resource.Success -> {
result.data?.let { plants ->
_state.value = state.value.copy(
plants = plants,
)
}
}
}
}
}
}
这是我的存储库,我从中获取列表中的项目。
// plantsRepository.kt
override suspend fun getPlants(
fetchFromBackend: Boolean,
query: String
): Flow<Resource<List<Plant>>> {
return flow {
emit(Resource.Loading(true))
val localPlants = dao.searchPlants(query)
emit(
Resource.Success(
data = localPlants.map { it.toPlant() },
)
)
val isDbEmpty = localPlants.isEmpty() && query.isBlank()
val shouldLoadFromCache = !isDbEmpty && !fetchFromBackend
if (shouldLoadFromCache) {
emit(Resource.Loading(false))
return@flow
}
val response = plantApi.getPlants().plants
dao.clearPlants()
dao.insertPlants(
response.map { it.toPlantEntity() }
)
emit(Resource.Success(
data = dao.searchPlants("").map { it.toPlant() }
))
emit(Resource.Loading(false))
}
}
完整的引用代码可以在这里找到:
https://gitlab.com/fiehra/plants
谢谢!
最佳答案
您实际上有两个事实来源:一个是房间数据库,另一个是 View 模型中的 _state
对象。
为了将其减少为单一事实来源,您需要将流的集合移动到需要数据的组合函数。您将使用 Artifact androidx.lifecycle:lifecycle-runtime-compose
中的扩展函数 StateFlow.collectAsStateWithLifecycle()
来执行此操作。当您的可组合项进入和离开组合时,这将自动订阅和取消订阅流。
由于您希望业务逻辑保留在 View 模型中,因此您必须在收集流之前应用它。这个想法是只转换 View 模型中的流程:
class PlantsViewModel {
private var fetchFromBackend: Boolean by mutableStateOf(true)
private var query: String by mutableStateOf("")
@OptIn(ExperimentalCoroutinesApi::class)
val state: StateFlow<PlantsState> =
snapshotFlow { fetchFromBackend to query }
.flatMapLatest { plantRepository.getPlants(it.first, it.second) }
.mapLatest(PlantsState::of)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = PlantsState.Loading,
)
// ...
}
如果您想要 fetchFromBackend
和 query
的其他值,您只需更新变量即可;流程将自动重新计算状态对象。它可以像调用这样简单:
fun requestPlant(fetchFromBackend: Boolean, query: String) {
this.fetchFromBackend = fetchFromBackend
this.query = query
}
然后可以在 View 模型中的其他位置完成从结果创建 PlantsState
的逻辑。将此替换 PlantsViewModel.getPlants()
并将其放置在 PlantsViewModel 类之外的文件级别:
private fun PlantsState.Companion.of(result: Resource<List<Plant>>): PlantsState = when (result) {
is Resource.Success -> {
result.data?.let { plants ->
PlantsState.Success(
plants = plants,
)
} ?: TODO("handle case where result.data is null")
}
is Resource.Error -> {
PlantsState.Error("an error occurred")
}
is Resource.Loading -> {
PlantsState.Loading
}
}
将 PlantsState
类替换为:
sealed interface PlantsState {
object Loading : PlantsState
data class Success(
val plants: List<Plant> = emptyList(),
val plantOrder: PlantOrder = PlantOrder.Name(OrderType.Descending),
val isOrderSectionVisible: Boolean = false,
) : PlantsState
data class Error(
val error: String,
) : PlantsState
companion object
}
然后,无论您在哪里需要状态(在 PlantsScreen f.e. 中),您都可以使用以下命令获取状态对象
val state by viewModel.state.collectAsStateWithLifecycle()
感谢 kotlin flow state
将始终包含房间数据库中的最新数据,并且感谢 compose 的魔力,当 state
中有任何内容时,您的可组合项将始终更新对象更新,这样您实际上只有一个事实来源。
另外:
PlantRepository.getPlants()
不应标记为挂起函数,因为它只是创建一个流而不会阻塞;长时间运行的数据检索将在收集器中完成。- 您需要手动导入
androidx.compose.runtime.getValue
和androidx.compose.runtime.setValue
才能使某些委托(delegate)正常工作。
关于android - 返回时刷新 ViewModel 中的数据 - Android(Kotlin),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/74904530/