Niklas Wintrén
10/14/2025, 7:05 AMinit{} function for a long time; but with the not so latest chatter; it's "obvious" that we should use .stateIn() instead (one source, I've seen pleanty). And I buy the premise! I like the constructor not tied to logic part for testability. The argument that if there have been no one listening for over 5 seconds then, you can do another fetch (because Android ANR) etc.
But, I've failed to see any larger working example! It's all nice and well to present the logic on a Read-only screen with a pokemons data, but I am writing more advanced interaction than that.
The issue that I've found is that I don't always have an incoming flow (UseCase/repo) that I can manipulate to update UI State (I prefer a single state). I want to be able to update the state, ie it should be an available MutableStateFlow<State>. I want to be able to click the checkbox and have the ViewModel update the UI so the button is now enabled, but not having to have an entire state flow or use case just to combine a bunch of flows in order to update the state. I "need" flow.update{ it.copy(buttonEnabled = true)} .
Abstracted ViewModel code:
val stateFlow: StateFlow<S> = mutableStateFlow
.onStart { onStateFlowStart() }
.stateIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(TIME_BEFORE_RESTART),
initialValue = state
)
If I do a single fetch to get the data; then this works fine. Every time the stateFlow val gets collected for the first time in 5 seconds - then I can set onStateFlowStart() to fetch and update the mutableStateFlow value with .update{}.
But if I need to observe to a UseCase, or two (but combine could solve that), that returns a Flow - then I can't collect that in "on start" and update the state flow; I would start collecting multiple times.
So, how can I achieve: (all or some of the points below)
• "Proper handling" that have a lot of benefits (I'm not saying all, because it might no be applicable at all times?)
• An available mutable state flow to update only the UI state
• The benefits of stateIn with it's WhileSubscribed logic.
• Readable and understandable code.
How are you handling things in actual, complex ViewModels?Niklas Wintrén
10/14/2025, 7:27 AMLittle addition: Depending on your situation you may want to use SharingStarted.Lazily instead of WhileSubscribed, especially if you want to keep the value of the flow when the app goes in the background
So maybe it's just that, stateIn Whilesubscribed is not meant for all cases 🤷♂️Guillaume B
10/14/2025, 7:28 AMprivate val isCheckedFlow = MutableStateFlow<Boolean?>(null)
val viewState: StateFlow<ScreenViewStateUiModel> =
combines(
observeDataAUseCase(),
observeDataBUseCase(),
isCheckedFlow,
).mapLatest { (dataA, dataB, isChecked) ->
toScreenViewState(
dataA = dataA,
dataB = dataB,
isChecked = isChecked,
)
}.onStart {
fetchDataAUseCase()
}
.stateIn(...)
fun onChecked(isChecked: Boolean) {
isCheckedFlow.value = isChecked
}Niklas Wintrén
10/14/2025, 7:30 AMupdateState {copy() } function I have in my ViewModels, but maybe it's holding me back 🤔Niklas Wintrén
10/14/2025, 7:31 AMGuillaume B
10/14/2025, 7:56 AMFernando
10/14/2025, 9:31 AMcombine limitation of the max amount of flows. But I noticed that you use a combines that I'm not aware of. Is that some kind of util function?Guillaume B
10/14/2025, 9:35 AMGuillaume B
10/14/2025, 9:39 AMzhuinden
10/14/2025, 9:41 AMzhuinden
10/14/2025, 9:42 AMzhuinden
10/14/2025, 9:45 AMWell, to be honest, the updateState{copy()} becomes a nightmareidk why anyone did
updateState(copy() and is also the primary reason why i have historically always advocated against each and every form of "MVI"zhuinden
10/14/2025, 9:45 AM(State) -> State that would be then passed to like, updateState(updaterFunction) in a queue and that was "The Architecture" like, guys this is not rocket science or at least it wasn't until you made it that way lolGuillaume B
10/14/2025, 9:46 AMzhuinden
10/14/2025, 9:46 AMzhuinden
10/14/2025, 9:47 AMzhuinden
10/14/2025, 9:47 AMzhuinden
10/14/2025, 9:48 AMGuillaume B
10/14/2025, 9:48 AMzhuinden
10/14/2025, 9:49 AMzhuinden
10/14/2025, 9:50 AMzhuinden
10/14/2025, 9:52 AMcombineMap(a, b, c, d) { MyOwnClass(a, b, c, d) } so it is not a tuple inbetweenGuillaume B
10/14/2025, 9:52 AMNiklas Wintrén
10/14/2025, 9:53 AMstateFlow.update{}Guillaume B
10/14/2025, 10:25 AMGuillaume B
10/14/2025, 10:26 AMzhuinden
10/14/2025, 10:26 AMGuillaume B
10/14/2025, 10:32 AMGuillaume B
10/14/2025, 10:32 AMzhuinden
10/14/2025, 10:36 AMNiklas Wintrén
10/15/2025, 6:30 AMstateIn function that always use Eagerly SharingStarted. What's the difference between Eagerly and Lazily - I mean from a practical standpoint, why did you go with Eagerly? Does that mean it start the flow as soon as it's created - which would be the same (~ish) as doing a bunch of stuff in the init?
(And why not WhileSubscribed?)Guillaume B
10/15/2025, 7:48 AMzhuinden
10/15/2025, 7:56 AMzhuinden
10/15/2025, 7:58 AMby lazy {} accessor triggering it, it's rather rare to actually see itzhuinden
10/15/2025, 7:59 AMLaunchedEffect { blah.initialize() } but that design is honestly almost worse than just putting it in an init {} blockdorche
10/15/2025, 8:21 PM.stateIn() historically but have a chance to start fresh now and wondering if I'm missing out - how does WhileSubscribed behave when you navigate "back" to a screen+vm that uses it? I was under the impression that you'd lose the state that the VM was and it would have to go through loading/fetching the data again?zhuinden
10/16/2025, 12:36 AMzhuinden
10/16/2025, 12:37 AMGuillaume B
10/16/2025, 6:27 AMGuillaume B
10/16/2025, 6:30 AMGuillaume B
10/16/2025, 6:31 AMdorche
10/16/2025, 8:21 AMval state = MutableStateFlow(UiState.Initial)
fun refresh() {
viewModelScope.launch {
// fetchData is your usecase call or whatever
val response = fetchData()
state.update {
...
}
}
}
And letting my Compose code call refresh on (first) screen view.
This has a couple benefits imo
• assuming you can get away with it, usecase and repository layer can be simple suspend functions and not flows. Makes them simpler and easier to test.
• In cases where I want the screen to not refresh/retrigger the network calls when I navigate back to it it's fairly easy - controlled by how often you call refresh() from the compose side. Same if you're consuming this ViewModel from SwiftUI.
I appreciate the design of having to trigger this function from Compose is not ideal but it seems the most flexible. As I said my very quick look into stateIn() (this was a while ago so I could be wrong) led me to believe that it will force you to retrigger onStart regardless if you're navigating back or for the first time and also my presumption was that WhileSubscribed(5000) will "destroy" (for lack of better word) the current value in the StateFlow.zhuinden
10/16/2025, 8:24 AMdorche
10/16/2025, 8:24 AMzhuinden
10/16/2025, 8:26 AMprivate val response = MutableStateFlow<Response>(null)
private val otherFlow = MutableStateFlow<...>(...) // some might be coming from savedStateHandle.getStateFlow()
private val etc = MutableStateFlow<...>(...)
val state = combineTuple(response, otherFlow, etc).map { (response, other, etc) ->
when {
response == null || response.isLoading -> UiState.Loading
else -> UiState.Content(
response = response,
other = other,
etc = etc,
)
}
}.stateIn(viewModelScope, WhileSubscribed(5000L), UiState.Loading)Guillaume B
10/16/2025, 8:28 AMGuillaume B
10/16/2025, 8:30 AMdorche
10/16/2025, 8:34 AMstateIn(WhileSubscribed(5000) + onStart {} indeed has it's own quirks that one might need to work around depending on their requirements.Guillaume B
10/16/2025, 8:37 AMzhuinden
10/16/2025, 8:41 AMNiklas Wintrén
10/17/2025, 4:46 AMoverride val stateFlow: StateFlow<State> = viewModelState(
data = {
combines(
flowOf("one"),
flowOf("two"),
flowOf("three"),
)
},
state = { (one, two, three) ->
State(
title = one,
text = two,
buttonLabel = three
)
},
onStarted = {
fetchThatUpdatesSources123()
}
)
So we're probably going to stick with this and figure out any issues.
But once I am getting into the mindset I have a huge screen in another project that would become so much easier by just introducing a shared flow state; instead of the current "make sure that the sub-viewmodel have access to the updateState{} function but in a smaller scope". Can't wait to put it in there!Saurabh Arora
10/17/2025, 10:57 AMval refreshChannel = Channel<Unit>()
val dataChannel = Channel<Data>
val uiStateFlow = combineTuple(dataChannel,...).stateIn(WhileSubscribed, UiState.Loading)
init {
scope.launch {
refreshChannel.collect {
dataChannel.emit(repo.fetchData())
}
}
refreshChannel.trySend(Unit)
}
fun userRequestedRefresh() = refreshChannel.trySend(Unit)
This also helps me avoid Data being nullable and I can request refresh whenever I wantGuillaume B
10/17/2025, 11:03 AMGuillaume B
10/17/2025, 11:04 AMSaurabh Arora
10/17/2025, 11:15 AMOk but when do you call userRequestedRefresh() ?
Whenever the user wants to refresh the data, for example pull to refresh, retry button in error state, etc.
Guillaume B
10/17/2025, 11:15 AMGuillaume B
10/17/2025, 11:16 AMGuillaume B
10/17/2025, 11:17 AMSaurabh Arora
10/17/2025, 11:17 AMGuillaume B
10/17/2025, 11:17 AMGuillaume B
10/17/2025, 11:18 AMGuillaume B
10/17/2025, 11:19 AMSaurabh Arora
10/17/2025, 11:20 AMGuillaume B
10/17/2025, 11:21 AMGuillaume B
10/17/2025, 11:22 AMzhuinden
10/17/2025, 11:22 AMLaunchedEffect(Unit) { viewModel.actuallyInitializeMeow() } being better, because it's extremely easy to accidentally not call it as it's a non-intuitive non-idiomatic api; even if Google says sozhuinden
10/17/2025, 11:23 AMzhuinden
10/17/2025, 11:23 AMzhuinden
10/17/2025, 11:24 AMSaurabh Arora
10/17/2025, 11:24 AMzhuinden
10/17/2025, 11:25 AMOnServiceRegistered to move unit init logic to a proper "start-up place"zhuinden
10/17/2025, 11:25 AMGuillaume B
10/17/2025, 11:42 AMSaurabh Arora
10/17/2025, 11:44 AM