I did a viewmodel
with Flow
and Room
that connected my variables on the UiState
with Room
using .first()
as the Codelabs showed me. I noticed that using .first()
is not a good idea because when I inserted something in the database, the screen didn't get refreshed automatically showing the new value.
So I learned that using collect{}
is the correct approach to continuously update the uistate variables automatically. After changing it now my screen is not showing any data and I can't discover why. How can I find the issue? I'm adding the viewmodel
here.
You can see the old code commented. I mean the code with .first()
that worked, but without updating automatically. The commented code is followed by the new code with collect{}
that didn't work at all.
data class UiState(
val searchResult: List<Airport> = listOf(),
val favorites: List<Favorite> = listOf(),
val selectedAirport: Airport? = null,
val flightsForSelectedAirport: List<Airport> = listOf()
)
class FlightsScreenViewModel(
private val flightRepository: FlightsRepository,
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
var uiState by mutableStateOf(UiState())
private set
var searchText by mutableStateOf("")
private set
init {
viewModelScope.launch {
// searchText = userPreferencesRepository.searchText.first()
userPreferencesRepository.searchText.collect{ searchText = it }
updateSearchResults()
}
}
private suspend fun updateSearchResults() {
// val searchResult = if (searchText != "")
// flightRepository.getAirportsByIatOrName(searchText).filterNotNull().first()
// else
// emptyList()
//
// val favorites = flightRepository.getFavorites().filterNotNull().first()
//
// uiState = uiState.copy(
// searchResult = searchResult,
// favorites = favorites
// )
if (searchText != "") {
flightRepository.getAirportsByIatOrName(searchText).filterNotNull().collect{ uiState = uiState.copy(searchResult = it) }
} else {
uiState = uiState.copy(favorites = emptyList())
}
flightRepository.getFavorites().filterNotNull().collect{ uiState = uiState.copy(favorites = it) }
}
fun updateSearchText(searchText: String) {
this.searchText = searchText
viewModelScope.launch {
userPreferencesRepository.saveSearchTextPreference(searchText)
updateSearchResults()
}
}
fun selectAirport(airport: Airport?) {
viewModelScope.launch {
// val flightsForSelectedAirport = if (airport == null) {
// emptyList()
// } else {
// flightRepository.getAllDifferentAirports(airport.id).first()
// }
//
// uiState = uiState.copy(
// selectedAirport = airport,
// flightsForSelectedAirport = flightsForSelectedAirport
// )
if (airport != null) {
flightRepository.getAllDifferentAirports(airport.id).collect{ uiState = uiState.copy(flightsForSelectedAirport = it) }
} else {
uiState = uiState.copy(flightsForSelectedAirport = emptyList())
}
uiState = uiState.copy(selectedAirport = airport)
}
}
fun insertFavorite(depart: String, arrive: String) {
if (!uiState.favorites.checkIfFavoriteExists(depart, arrive)) {
val favorite = Favorite(departureCode = depart, destinationCode = arrive)
viewModelScope.launch {
flightRepository.insertFavorite(favorite)
}
}
}
companion object {
val factory : ViewModelProvider.Factory = viewModelFactory {
initializer {
FlightsScreenViewModel(
FlightSearchApplication().container.flightRepository,
FlightSearchApplication().container.userPreferencesRepository
)
}
}
}
}
fun List<Favorite>.checkIfFavoriteExists(depart: String, arrive: String): Boolean{
for (favorite in this){
if (favorite.departureCode == depart && favorite.destinationCode == arrive)
return true
}
return false
}
This is the code that should show the content in the screen, but is not showing nothing after updating from .first()
to .collect{}
LazyColumn(
modifier = Modifier.padding(8.dp)
) {
items(uiState.searchResult) { airport ->
AirportDetail(airport, onAirportSelected)
}
}
uiState.selectedAirport?.let {
FlightsForAirport(
airport = uiState.selectedAirport,
arrivals = uiState.flightsForSelectedAirport,
favorites = uiState.favorites,
onFavoriteSelected = onFavoriteSelected
)
}
The code lab is outdated. This is how it should be done instead:
collect
them.This is how your view model should look like instead:
class FlightsScreenViewModel(
private val flightRepository: FlightsRepository,
private val userPreferencesRepository: UserPreferencesRepository,
) : ViewModel() {
private val searchResult: Flow<List<Airport>> =
userPreferencesRepository.searchText.flatMapLatest {
if (it.isEmpty()) flowOf(emptyList())
else flightRepository.getAirportsByIatOrName(it).filterNotNull()
}
private val favorites: Flow<List<Favorite>> =
flightRepository.getFavorites().filterNotNull()
private val selectedAirport = MutableStateFlow<Airport?>(null)
private val flightsForSelectedAirport: Flow<List<Airport>> =
selectedAirport.flatMapLatest {
if (it == null) flowOf(emptyList())
else flightRepository.getAllDifferentAirports(it.id)
}
val uiState: StateFlow<UiState> = combine(
searchResult,
favorites,
selectedAirport,
flightsForSelectedAirport,
::UiState,
).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UiState(),
)
fun updateSearchText(searchText: String) {
viewModelScope.launch {
userPreferencesRepository.saveSearchTextPreference(searchText)
}
}
fun selectAirport(airport: Airport?) {
selectedAirport.value = airport
}
fun insertFavorite(depart: String, arrive: String) {
val favorite = Favorite(departureCode = depart, destinationCode = arrive)
viewModelScope.launch {
flightRepository.insertFavorite(favorite)
}
}
companion object { /* ... */ }
}
The flows are never collected, they are just transformed. The new transformed searchResult
flow, for example, is created by using the userPreferencesRepository.searchText
flow and mapping it to the flightRepository.getAirportsByIatOrName
flow.
The new flightsForSelectedAirport
flow on the other hand is based on a new MutableStateFlow<Airport?>
that is used to store the currently selected airport. It can be used similar to a MutableState(*) in that its value
property can be directly set as seen in the function fun selectAirport()
. It is still a flow, though, so it can be transformed into a Flow<List<Airport>>
here.
Now that all values that are needed to create a UiState
are present in flows, these flows can be combine
d into a single Flow. That flow is then converted to a StateFlow (related to MutableStateFlow, not related to Compose State) and publicly exposed.
Any consumer of the view model can now collect that flow to get a UiState that will always be up to date. Use this in your composable:
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
This is the bridge between the Kotlin world of Flows and the Compose world of States. It converts the StateFlow into a Compose State object. By using the by
delegation uiState
is actually of type UiState
and the rest of your code will work the same as it previousy did.
You need the gradle dependency androidx.lifecycle:lifecycle-runtime-compose
for that.
Final thoughts:
You apply filterNotNull()
to the getAirportsByIatOrName
and getFavorites
flows. It implies that the flows can not only contain a List, they can also contain null
. That doesn't seem right, they should contain an emptyList()
instead of null
. Then you should remove filterNotNull()
.
I removed the checkIfFavoriteExists
from insertFavorite
. That should be done in the repository instead. The consistency of the data should not rely on what the view model does, that's the repository's reponsibility (or even that of the data source if you have that separated). Ideally that would be done before the data is even wrapped into a flow. It all depends on how this is actually implemented.
The function can also be streamlined a bit:
fun List<Favorite>.checkIfFavoriteExists(
depart: String,
arrive: String,
): Boolean = any {
it.departureCode == depart && it.destinationCode == arrive
}
Instead of writing an explicit view model factory you can automate it by using a dependency injection framework like Hilt.
(*): Although MutableStateFlow
and MutableState
have similar names they are completely different types that are entirely unrelated. The former is part of Kotlin's standard lib where the latter is part of the Compose framework. They share some semantics as they both store an observable value that can be accessed by the value
property, but that's all.