https://kotlinlang.org logo
#flow
Title
# flow
k

Kevin Worth

02/28/2023, 3:08 PM
Given the use of
StateFlow
via
stateIn(…)
how would one “restart” the subscription after an exception is caught?
Copy code
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):
Copy code
val uiState: StateFlow<MyModelUiState> = myModelRepository
        .myModels.map(::Success)
        .catch { Error(it) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Loading)
So, we want to use
flatMapLatest
to continually react to updates on the searchQuery (after user types in a new query and hits the search button).
But after hitting the
catch
, it stops listening to changes to the searchQuery because, the
catch
cancelled everything, right? So, how to clear it out and start over?
At the moment I’ve given up on using
.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
?
w

wasyl

03/07/2023, 2:51 PM
You pretty much just shouldn't throw and have a need for a catch in the first place, and instead use a wrapper like
Result
k

Kevin Worth

03/07/2023, 2:54 PM
I think I’m doing that, if I’m understanding you correctly. My
Success
and
Error
classes are extended classes of the sealed
ItemUiState
class. So, my
ItemUiState
is analogous to your
Result
, yes?
w

wasyl

03/07/2023, 2:55 PM
Yes but as far I see you still throw in the flow? Not sure which call exactly here throws exceptions
k

Kevin Worth

03/07/2023, 2:57 PM
There are two flows which are the most likely culprits •
itemRepository.items
itemRepository.search(query, scope)
w

wasyl

03/07/2023, 2:58 PM
Then both would need to return wrapper classes instead of throwing, or I think if you not let the error out of flatMap it would be OK too - you'd catch and cancel the inner flows, but outer one would still be active
k

Kevin Worth

03/07/2023, 3:02 PM
Forcing the repo to return a wrapper around the data doesn’t feel right. That would be leaking implementation details from one layer to the next, no?
I really appreciate your time @wasyl and I wonder if you could drop in a very short sample to convey your meaning on not letting the error out of flatMap…?
w

wasyl

03/07/2023, 3:16 PM
I mean to have a
catch
inside the
flatMapLatest
— so the map below is
.map<Result<List<String>>, ItemUiState>
I'm not 100% sure but I believe that way if repository fails then it will
Result.failure
and nothing else, but if
searchQuery
receives another emission then code inside
flatMapLatest
will be called again
k

Kevin Worth

03/07/2023, 3:20 PM
Hmmm, maybe I can throw that together quickly…
Just to prove it out, here’s what was easiest to do quickly:
Copy code
...
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).
Thanks again @wasyl for the idea. It might just work. We’ll see.
w

wasyl

03/07/2023, 4:01 PM
👍 good luck
In general the principle is that once a flow throws, it's done 🤷 At the same time `flatMap`s (and other operators that return inner flows) are kind of detached from the outer flow, so as long as you catch the error, you would only cancel the inner flow. In your first snippet, you didn't catch the error within
flatMap
so it propagated to the outer flow and canceled it too
And yes it's kind of annoying to have to wrap items but that's how it is. Even established libraries are moving towards no exceptions within flow approach (e.g. https://github.com/apollographql/apollo-kotlin/issues/4711)
k

Kevin Worth

03/07/2023, 4:03 PM
Interesting, will take a look at that
For those playing along at home, here’s what I ended up with which I don’t think is too bad (but I’ll take any suggestions):
Copy code
val 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.
75 Views