[英]refresh data in viewmodel when navigating back, android
So I have the following setup.所以我有以下设置。 I have a screen with a list of items (PlantsScreen).我有一个包含项目列表的屏幕 (PlantsScreen)。 when clicking on an item in the list i will be navigated to another screen (AddEditPlantScreen).单击列表中的项目时,我将导航到另一个屏幕 (AddEditPlantScreen)。 after editing and saving the item and navigating back to the listScreen i want to show the updated list of items.编辑并保存项目并导航回 listScreen 后,我想显示更新的项目列表。 But the list is not displaying the updated list but the list before the edit of the item.但是列表显示的不是更新后的列表,而是项目编辑前的列表。
In order to have a single source of truth i am fetching the data from a nodeJS backend and then saving it to the local repository(room).为了拥有单一的真实来源,我从 nodeJS 后端获取数据,然后将其保存到本地存储库(房间)。 I think i need to refresh the state in the viewModel to fetch the updated list from my repository.我想我需要刷新 viewModel 中的状态以从我的存储库中获取更新的列表。
I know i can use a Job to do this but it throws me an error.我知道我可以使用 Job 来执行此操作,但它会抛出一个错误。 is this the correct approach when returning a Flow?这是返回 Flow 时的正确方法吗? If yes how can i achieve this.如果是,我该如何实现。 If not what alternative approach do i have?如果不是,我有什么替代方法?
// 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,
)
}
}
}
}
}
}
here my repository where i fetch the items for the list from这是我的存储库,我从中获取列表的项目
// 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))
}
}
full code for reference can be found here: https://gitlab.com/fiehra/plants完整的参考代码可以在这里找到: https ://gitlab.com/fiehra/plants
You actually have two sources of truth: One is the room database, the other the _state
object in the view model.您实际上有两个真实来源:一个是房间数据库,另一个是视图模型中的_state
对象。
To reduce this to a single source of truth you need to move the collection of the flow to the compose function where the data is needed.要将其简化为单一事实来源,您需要将流的集合移动到需要数据的组合函数。 You will do this using the extension function StateFlow.collectAsStateWithLifecycle()
from the artifact androidx.lifecycle:lifecycle-runtime-compose
.您将使用工件androidx.lifecycle:lifecycle-runtime-compose
中的扩展函数StateFlow.collectAsStateWithLifecycle()
来执行此操作。 This will automatically subscribe and unsubscribe the flow when your composable enters and leaves the composition.当您的可组合项进入和离开组合时,这将自动订阅和取消订阅流程。
Since you want the business logic to stay in the view model you have to apply it before the flow is collected.由于您希望业务逻辑保留在视图模型中,因此您必须在收集流之前应用它。 The idea is to only transform the flow in the view model:这个想法是只转换视图模型中的流:
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,
)
// ...
}
If you want other values for fetchFromBackend
and query
you just need to update the variables;如果你想要fetchFromBackend
和query
的其他值,你只需要更新变量; the flow will automatically recalculate the state object.流将自动重新计算状态对象。 It can be as simple as just calling something like this:它可以像这样调用一样简单:
fun requestPlant(fetchFromBackend: Boolean, query: String) {
this.fetchFromBackend = fetchFromBackend
this.query = query
}
The logic to create a PlantsState
from a result can then be done somewhere else in the view model.然后可以在视图模型中的其他地方完成从结果创建PlantsState
的逻辑。 Replace your PlantsViewModel.getPlants()
with this and place it at file level outside of the PlantsViewModel class:用这个替换你的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
}
}
With the PlantsState
class replaced by this:将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
}
Then, wherever you need the state (in PlantsScreen fe), you can get a state object with然后,无论您在哪里需要状态(在 PlantsScreen fe 中),您都可以使用
val state by viewModel.state.collectAsStateWithLifecycle()
Thanks to kotlin flows state
will always contain the most current data from the room database, and thanks to the compose magic your composables will always update when anything in the state
object updates, so that you really only have one single source of truth.多亏了 kotlin flows, state
将始终包含来自房间数据库的最新数据,多亏了 compose 魔法,当state
对象中的任何内容更新时,您的可组合项将始终更新,因此您实际上只有一个单一的真实来源。
Additionally:此外:
PlantRepository.getPlants()
should not be marked as a suspend function because it just creates a flow and won't block; PlantRepository.getPlants()
不应标记为挂起函数,因为它只是创建一个流而不会阻塞; long running data retrieval will be done in the collector.长时间运行的数据检索将在收集器中完成。androidx.compose.runtime.getValue
and the androidx.compose.runtime.setValue
for some of the delegates to work.您需要手动导入androidx.compose.runtime.getValue
和androidx.compose.runtime.setValue
使某些委托工作。 After @Leviathan was able to point me in the right direction i refactored my code by changing the return types of my repository functions, implementing use cases and returning a Flow<List<Plant>>
instead of Flow<Resource<List<Plant>>>
for simplicity purposes.在@Leviathan 能够为我指出正确的方向之后,我通过更改我的存储库函数的返回类型、实现用例并返回Flow<List<Plant>>
而不是Flow<Resource<List<Plant>>>
>> 来重构我的代码Flow<Resource<List<Plant>>>
为简单起见。
Further removed the suspend marker of the functions in the PlantDao.kt
and PlantRepository.kt
as pointed out by Leviathan.正如 Leviathan 所指出的,进一步删除了PlantDao.kt
和PlantRepository.kt
中函数的挂起标记。
// PlantRepositoryImplementation.kt
override fun getPlants(
fetchFromBackend: Boolean,
query: String
): Flow<List<Plant>> {
return flow {
val localPlants = dao.searchPlants(query)
localPlants.collect { plants ->
emit(plants.map { it.toPlant() })
val isDbEmpty = plants.isEmpty() && query.isBlank()
val shouldLoadFromCache = !isDbEmpty && !fetchFromBackend
if (shouldLoadFromCache) {
return@collect
}
val response = plantApi.getPlants().plants
dao.clearPlants()
dao.insertPlants(
response.map { it.toPlantEntity() }
)
emit(response)
}
}
}
I started using a Job and GetPlants usecase in my viewModel like this:我开始在我的 viewModel 中使用 Job 和 GetPlants 用例,如下所示:
// PlantsViewModel.kt
private fun getPlants(plantOrder: PlantOrder, fetchFromBackend: Boolean, query: String) {
getPlantsJob?.cancel()
getPlantsJob = plantUseCases.getPlants(plantOrder, fetchFromBackend, query)
.onEach { plants ->
_state.value = state.value.copy(
plants = plants,
plantOrder = plantOrder
)
}.launchIn(viewModelScope)
I also had to remove the suspend in the PlantDao.kt
我还必须删除PlantDao.kt
中的暂停
// PlantDao.kt
fun searchPlants(query: String): Flow<List<PlantEntity>>
This is the code for my GetPlants usecase:这是我的 GetPlants 用例的代码:
// GetPlantsUsecase.kt
class GetPlants
(
private val repository: PlantRepository,
) {
operator fun invoke(
plantOrder: PlantOrder = PlantOrder.Name(OrderType.Descending),
fetchFromBackend: Boolean,
query: String
): Flow<List<Plant>> {
return repository.getPlants(fetchFromBackend, query).map { plants ->
when (plantOrder.orderType) {
is OrderType.Ascending -> {
// logic for sorting
}
}
is OrderType.Descending -> {
// logic for sorting
}
}
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.