Kevin Worth
02/28/2023, 3:08 PMStateFlow
via stateIn(…)
how would one “restart” the subscription after an exception is caught?
val searchQuery = MutableStateFlow("")
val scope = viewModelScope
val uiState: StateFlow<ItemUiState> = searchQuery
.flatMapLatest { query ->
when {
query.isBlank() -> itemRepository.items
else -> itemRepository.search(query, scope)
}
}
.map<List<String>, ItemUiState> {
Success(it)
}
// This "works" but it loops indefinitely
// .retryWhen { cause: Throwable, attempt: Long ->
// emit(Error(cause))
// true
// }
.catch {
emit(Error(it))
}
.stateIn(
scope,
SharingStarted.WhileSubscribed(5000),
Loading)
This comes from slightly altering this project https://github.com/android/architecture-templates/tree/base where uiState
starts out simply defined as (see MyModelViewModel.kt):
val uiState: StateFlow<MyModelUiState> = myModelRepository
.myModels.map(::Success)
.catch { Error(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Loading)
flatMapLatest
to continually react to updates on the searchQuery (after user types in a new query and hits the search button).catch
, it stops listening to changes to the searchQuery because, the catch
cancelled everything, right? So, how to clear it out and start over?.stateIn
so that I can keep hold of a Job
from launchIn
. Then on certain events I check to see if the Job
is cancelled, and restart it when necessary. Shouldn’t there be a way to get it to work with stateIn
?wasyl
03/07/2023, 2:51 PMResult
Kevin Worth
03/07/2023, 2:54 PMSuccess
and Error
classes are extended classes of the sealed ItemUiState
class. So, my ItemUiState
is analogous to your Result
, yes?wasyl
03/07/2023, 2:55 PMKevin Worth
03/07/2023, 2:57 PMitemRepository.items
• itemRepository.search(query, scope)
wasyl
03/07/2023, 2:58 PMKevin Worth
03/07/2023, 3:02 PMwasyl
03/07/2023, 3:16 PMcatch
inside the flatMapLatest
— so the map below is .map<Result<List<String>>, ItemUiState>
Result.failure
and nothing else, but if searchQuery
receives another emission then code inside flatMapLatest
will be called againKevin Worth
03/07/2023, 3:20 PM...
query.isBlank() -> itemRepository.items
.catch {
println("boom: $it")
emit(emptyList())
}
else -> itemRepository.searchByTitle(query, viewModelScope)
.catch {
println("pow: $it")
emit(emptyList())
}
...
And sure enough, that did keep things listening. Now I just need to decide how to get this actually functional (since emitting an empty list was only for debugging).wasyl
03/07/2023, 4:01 PMflatMap
so it propagated to the outer flow and canceled it tooKevin Worth
03/07/2023, 4:03 PMval uiState: StateFlow<ItemUiState> = searchQuery
.flatMapLatest { query ->
when {
query.isBlank() -> itemRepository.items
.map<List<String>, ItemUiState> { Success(it) }
.catch { emit(Error(it)) }
else -> itemRepository.search(query, scope)
.map<List<String>, ItemUiState> { Success(it) }
.catch { emit(Error(it)) }
}
}
.catch {
emit(Error(RuntimeException("Uncaught exception in ViewModel", it)))
}
.stateIn(
scope,
SharingStarted.WhileSubscribed(5000),
Loading)
Now, for the outer catch
which we really don’t expect to hit, there will need to be some extra handling to recover, since that will still cancel. But for the typical, expected network errors, etc. they are handled in the inner `catch`es and things keep trucking nicely.