Search code examples
androidandroid-jetpack-composeviewmodelkotlin-flow

How to collect Flows from viewModel in compose using StateIn


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
            )}
    }
}

Solution

  • 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,
            )
        }
    }