Sharing Flows & Lifecycle-Safe Collection Interview Questions

KOTLIN › Flow

Your app re-queries the database and shows a loading spinner every time the user rotates the device. Walk me through diagnosing and fixing that.

What a strong answer covers: The symptom says the upstream is restarting on configuration change, which points at either a cold flow being collected directly or a sharing policy that does not survive the gap between the old subscriber leaving and the new one arriving. First I would check whether the ViewModel exposes a cold flow. If it does, every collector re-runs the producer, so a new Fragment after rotation genuinely means a new query. The fix is stateIn on viewModelScope. Second, if it already uses stateIn, I would look at the SharingStarted argument. WhileSubscribed() with no timeout stops the upstream the instant the last subscriber leaves, which during rotation is a real moment, so the new Fragment subscribing starts everything again. WhileSubscribed(5000) is the conventional fix precisely because the timeout is chosen to outlast a configuration change while still being far shorter than a realistic backgrounding, so rotation never actually stops the producer. Third, if the spinner still appears even though the data is cached, I would check replayExpirationMillis, because setting it to zero drops the cached value when sharing stops, so the returning subscriber gets the initial Loading value rather than last known state. The default keeps the cache indefinitely, which is what you want. Finally I would check the flow is a property rather than a function, since stateIn inside a function builds a fresh shared flow per call and reintroduces the original problem while looking correct.

Explain what actually goes wrong with launchWhenStarted, in enough detail that someone would not reach for it again.

What a strong answer covers: It suspends rather than cancels, and the distinction is the whole problem. When the lifecycle drops below STARTED, launchWhenStarted suspends the coroutine at its next suspension point. Your collect lambda stops receiving values, so from the UI side it looks like it worked. But the coroutine is still alive and the flow collection is still registered, which means the subscription never ends. Everything upstream keeps running: a Room query keeps observing the table, a location listener keeps delivering fixes and burning battery, a network poller keeps polling, for the entire time the user is in another app. Worse, if the ViewModel uses SharingStarted.WhileSubscribed, the subscriber count never reaches zero, so the timeout never fires and the shared upstream never stops either. The optimisation you thought you had is silently disabled by the thing you thought was safe. repeatOnLifecycle cancels the block on the way down and re-runs it on the way up, so the subscription genuinely ends, the subscriber count does drop, and WhileSubscribed can do its job. There is a second, subtler problem: because launchWhenStarted only suspends at suspension points, work already in progress between two suspension points continues, so it is not even a reliable pause. The rule I would leave someone with is that repeatOnLifecycle cancels the subscription so WhileSubscribed can stop the producer, and launchWhenStarted breaks the downstream half of that handshake.

When would you use shareIn rather than stateIn, and what goes wrong if you pick the wrong one?

What a strong answer covers: stateIn when there is always a meaningful current value, which is screen state: there is always some UiState, even if it is Loading. shareIn when there is not, which is events: navigation commands, snackbar messages, one-off errors. Using stateIn for events breaks in two ways that are both nasty. First, StateFlow conflates and compares with equals, so firing the same event twice in a row emits once. Show the same error snackbar twice and the second one silently never appears, which is a bug people spend hours on because the code looks obviously correct. Second, StateFlow always has a current value and replays it to new subscribers, so rotating the device re-delivers the last event and you navigate twice or show the dialog again. Using shareIn for state has the opposite problem: with replay 0 a new subscriber sees nothing until the next emission, so after a rotation the screen is blank until something changes, and the type does not carry an initial value so every collector has to invent one. The wider point I would make is that events are genuinely awkward in this model, and there is a real argument for a Channel with receiveAsFlow instead of a SharedFlow, because a Channel guarantees each event is delivered exactly once to exactly one collector rather than being dropped when nobody happens to be subscribed. Which you pick depends on whether losing an event when the screen is gone is acceptable.

Back to Sharing Flows & Lifecycle-Safe Collection