I am using Flows in my ViewModel and converting a cold Flow to a hot one using either stateIn or shareIn along with the parameter SharingStarted.WhileSubscribed()
. The idea is to make the Flow active only while there is an active collector and suspend it when there are no active collectors, thus reducing resource usage when the app is in the background.
However, I am facing an issue. When I use SharingStarted.WhileSubscribed()
, my Flow isn't being collected at all in the Composable which is supposed to collect from it. As a result, only the loading indicator is shown. However, if I switch to SharingStarted.Eagerly
, the Flow gets collected successfully. This is expected because the WhileSubscribed sharing policy is designed to cancel the upstream Flow when there are no collectors.
But I want to continue using SharingStarted.WhileSubscribed()
. My question is, how can I ensure that the Flow remains active until the Composable starts collecting, without wasting resources?
Here is the relevant code:
@HiltViewModel
class MainActivityViewModel @Inject constructor(
private val paintingsUseCase: PaintingsUseCases,
private val appDispatchers: AppDispatchers,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val _paintingsState = MutableStateFlow<PaintingsUiState<List<Painting>>>(PaintingsUiState.Loading)
val paintingsState: StateFlow<PaintingsUiState<List<Painting>>> = _paintingsState.asStateFlow()
init {
Log.d(TAG, "ViewModel Initialized: $this (hashCode: ${hashCode()})")
fetchPaintings()
}
fun fetchPaintings() {
paintingsUseCase
.getAllPaintings()
.onEach { result ->
Log.d(TAG, "CoroutineName $CoroutineName")
_paintingsState.update {
when (result) {
is Result.Error -> PaintingsUiState.Error(result.exception)
is Result.Loading -> PaintingsUiState.Loading
is Result.Success -> {
paintingsList = result.data
Log.d(TAG, "_paintingState.value: ${_paintingsState.value}")
PaintingsUiState.Success(paintingsList)
}
}
}
}
.onCompletion { Log.d(TAG, "Fetching paintings complete") }
.onStart {
Log.d(TAG, "Fetching paintings started")
}
.catch { exception ->
Log.d(TAG, "Exception: $exception")
emit(Result.Error(exception as Exception))
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = PaintingsUiState.Loading,
)
}
@Composable
fun PaintingsHomeScreen(
onPaintingSelected: (String) -> Unit = {},
viewModel: MainActivityViewModel = hiltViewModel(),
) {
val homeScreenState by remember(viewModel) {
viewModel.paintingsState
}.collectAsStateWithLifecycle(MainActivityViewModel.PaintingsUiState.Loading)
when (val state = homeScreenState) {
is Loading -> LoadingScreen()
is Error -> ErrorScreen(retryAction = { viewModel.fetchPaintings() })
is Success -> {
val paintingList = state.data
PaintingsItemList(
paintingsList = paintingList,
onPaintingSelected = onPaintingSelected
)}
}
}
You created a Flow, called stateIn
, but you never actually called collect
on that Flow, so nothing is actually doing anything since you've used WhileSubscribed
(you've never actually subscribed!).
Instead of creating one flow and then exposing a second flow, you instead want to expose the Flow you've called stateIn
on directly, using map
to transform the getAllPaintings
into your PaintingsUiState
class:
@HiltViewModel
class MainActivityViewModel @Inject constructor(
private val paintingsUseCase: PaintingsUseCases,
private val appDispatchers: AppDispatchers,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
init {
Log.d(TAG, "ViewModel Initialized: $this (hashCode: ${hashCode()})")
}
val paintingsState = paintingsUseCase
.getAllPaintings()
.map { result ->
Log.d(TAG, "CoroutineName $CoroutineName")
when (result) {
is Result.Error -> PaintingsUiState.Error(result.exception)
is Result.Loading -> PaintingsUiState.Loading
is Result.Success -> {
val paintingsList = result.data
Log.d(TAG, "_paintingState.value: ${_paintingsState.value}")
PaintingsUiState.Success(paintingsList)
}
}
.onCompletion { Log.d(TAG, "Fetching paintings complete") }
.onStart {
Log.d(TAG, "Fetching paintings started")
}
.catch { exception ->
Log.d(TAG, "Exception: $exception")
emit(Result.Error(exception as Exception))
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = PaintingsUiState.Loading,
)
}
}