2

I have a view and its ViewModel consists of UI state for a search query and results for the query.

The search query is a string and I can hold its state in the ViewModel like this:

data class SearchUiState(
    val query: String = ""
)
class SearchViewModel(
    private val resultsRepository: ResultsRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(SearchUiState())
    val uiState = _uiState.asStateFlow()
}

Now, the search results come as a Flow like this: Flow<List<Result>>.

I read you should collect a flow in the UI (source: What is the proper way to wait for a Flow to collect?)

So, I can do that by converting the Flow into a StateFlow in the ViewModel.

class SearchViewModel(
    private val resultsRepository: ResultsRepository
) : ViewModel() {

    // ...

    val resultsUiState: StateFlow<resultsUiState> =
        resultsRepository.getResultsStream(_queryUiState.value.query).map { ResultsUiState(it) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
                initialValue = ResultsUiState()
            )
}

Then, in the UI, I can collect the StateFlow.

@Composable
fun SearchScreen(
    viewModel: SearchViewModel
) {
    val resultsUiState = viewModel.resultsUiState.collectAsState()

    // ...
}

But then now, I need to also collect the UI state of the search query separately from the same ViewModel.

@Composable
fun SearchScreen(
    viewModel: SearchViewModel
) {
    val resultsUiState = viewModel.resultsUiState.collectAsState()

    val queryUiState = viewModel.queryUiState.collectAsState()

    // ...
}

Is this a good practice to have different UI states held separately in a single ViewModel?

This post, Android - Best Practices for ViewModel State in MVVM?, suggests to put all UI state into a single state.

How can I achieve this with the queryUiState and resultsUiState when collecting a Flow from the UI?

Thanks in advance.

woxiangqiu
  • 125
  • 1
  • 6
  • Excuse me, I am not sure I got your question right. In general you indeed a single source inside your ```ViewModel```. You can put here your *searchQuery* and your *data*. From the UI side you just listen changes in the data layer. When I need to distinguish different scenarios, I add a flag to the data source, so in the UI/Controller layer I just compare which flag is set, e.g. ```when (state.value.flag) Flags.SEARCH_QUERY -> { /* search query */ } Flags.DEFAULT -> { /* common flow */ }``` – Gleichmut Feb 27 '23 at 06:25
  • @Gleichmut I see what you mean but I need to access both states. I need to find a way to combine both the query and results into a single state when collecting `Flow` from the view. – woxiangqiu Feb 27 '23 at 12:01
  • Is it something Compose specific? There are many offers on SO how to combine results of several flows into the one, e.g. https://stackoverflow.com/questions/71037219/merging-flow-with-one-shot-data – Gleichmut Feb 27 '23 at 13:33
  • In my opinion, it is best to keep a separate state for each of these and collect them separately in the UI, since they don't change at the same time. But this is an opinion-based question, so it's off-topic for StackOverflow. That post you linked advocates putting all the state in one class to avoid bloating the ViewModel with multiple LiveData (or StateFlow in your case), but then that data class is bloated and if you use Compose then you have to collect the same thing in multiple places and pick out the part you need in each spot, or pass that item down to all your independent Composables. – Tenfour04 Feb 27 '23 at 14:35

0 Answers0