<https://proandroiddev.com/loading-initial-data-in...
# compose
r
https://proandroiddev.com/loading-initial-data-in-launchedeffect-vs-viewmodel-f1747c20ce62 This post suggests that we should not load initial data in LaunchEffect or ViewModel the problem is clear, and the recommended approach is understandable but if I use MVI like single uiState in ViewModel, how should I update the UiState only instead of another extra state?
Copy code
data class UiState (
    val isLoading: Boolean = false,
    val posts: List<Post> = emptyList(),
    val errorMessage: String? = null
)
Suggested approach in the post:
Copy code
val pokemon = savedStateHandle.getStateFlow<Pokemon?>("pokemon", null)
val pokemonInfo: StateFlow<PokemonInfo?> =
    pokemon.filterNotNull().flatMapLatest { pokemon ->
        detailsRepository.fetchPokemonInfo(
            ..
        )
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = null,
    )
s
If
detailsRepository
responds with a new value, it will be emitted from this StateFlow. What is the problem you're facing here, are you concerned that you are creating a whole new object of
Pokemon
every time even one small thing may change?
r
No, Actually, I have only one uiState for the whole UI in viemodel, I want to update the posts properties of this uiState instead of creating another state variable like pokemonInfo here. In my case it is posts.
Copy code
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
s
Then you probably want something more like:
Copy code
val pokemon = savedStateHandle.getStateFlow<Pokemon?>("pokemon", null)
val otherState = MutableStateFlow(OtherState())
val uiState: StateFlow<LocationsUiState> = combine(
 pokemon.filterNotNull().flatMapLatestToPokemonInfo(),
 otherState,
) { pokemonInfo, otherState ->
 LocationsUiState(
  pokemonInfo = pokemonInfo,
  whateverElse = otherState.whateverElse
 )
}.stateIn(
 scope = viewModelScope,
 started = SharingStarted.WhileSubscribed(5_000),
 initialValue = null,
)
And so on. You'd need to merge your sources of information in a combine, and then map them to your one UiState model, all before you do the
stateIn
so that you still get the right behavior where if there are no listeners anymore, the flows stop being collected for no reason.
r
Ok, I have to update the uiState on some user events as well. I need to fetch other data depending on users' events. In that case, I have to keep multiple states for all these events and combine them with the uiState at the end?
s
Yeah, that sounds like what you can do there.
r
Thanks, got it. I just don't like the fact that I have to do this kind of breaking uiState into multiple states and then combine and everything just to avoid a single call in the init block. I wish there were some better approaches.
Copy code
init {
    savedStateHandle.getStateFlow<String?>("id", null)
        .filterNotNull()
        .onEach { id ->
            getSomething(id)
        }
        .launchIn(viewModelScope)
}
@skydoves @Ian Lake could you guys provide any suggestions, please? If I summarize the requirements: Single uiState, initially loads some data into the UiState and different data will be fetched by user events later into the uiState.
s
If your state can be derived from one thing in your saved state handle then you can just do what you show in that init, but outside of the init, and use stateIn again. Doing it in the init will mean that this StateFlow is hot and working for the entire time this ViewModel is alive for, which means it will be working forever as long as the screen which uses that is in the backstack.
r
I have the same usecase. If you go with MVI then usually you'll have a single state.
combine
should work or even
merge
will work when the operations are independent. So on user events, you will end up updating the
_uiState
. Basically, instead of multiple states, you now have only 2 things -
loadInitialData
for the initial data and then
_uiState
for every other update.
Copy code
val loadInitialData: () -> Flow<STATE> = someFlow()
val _uiState: MutableStateFlow<STATE> = MutableStateFlow(initialState)

val uiState: StateFlow<STATE> = merge(loadInitialData(), _uiState).stateIn(
 scope = viewModelScope,
 started = SharingStarted.WhileSubscribed(5_000),
 initialValue = someInitialState
)