Pablo
06/13/2024, 9:43 AMStylianos Gakis
06/13/2024, 10:10 AMFlow
is collected and with using stateIn
it's turned into a StateFlow<T>
so inside your composable you have the ViewModel https://github.com/android/compose-samples/blob/050b6aefb12ae2b49ff6c73578b9cae6b4[…]/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt and you can do collectAsState()
on it and then you got compose state.
That's pretty much the way to do it, Flows do all the heavy lifting regarding getting the latest value etc, and you just need to turn them into StateFlow in your VM so that you can then get an initial value in your composable too.
For the first frame yes you will not get the list yet, since it will take at least a little bit of time to fetch the items, so you can decide if you show a loading indicator or something like that.Pablo
06/13/2024, 10:26 AMPablo
06/13/2024, 10:26 AMStylianos Gakis
06/13/2024, 10:27 AMList<T>
then. The collection should happen at the screen level composable which does in fact have access to VMPablo
06/13/2024, 10:29 AMPablo
06/13/2024, 10:29 AMPablo
06/13/2024, 10:30 AMStylianos Gakis
06/13/2024, 10:38 AMPablo
06/13/2024, 10:40 AMStylianos Gakis
06/13/2024, 10:42 AMPablo
06/13/2024, 10:47 AMPablo
06/13/2024, 10:47 AMStylianos Gakis
06/13/2024, 10:48 AMstateIn()
so that it's not hot when nobody is observing.
Then pass down that StateFlow to that child composable, and only inside that panel do collectAsStateWithLifecycle
so that you only actually make this flow hot while that panel is in fact showing.Stylianos Gakis
06/13/2024, 10:49 AM@Composable () -> Unit
down that sub sub sub sub sub composable.
It might make this easier to reason about, but ymmvPablo
06/13/2024, 10:49 AMPablo
06/13/2024, 10:50 AMPablo
06/13/2024, 10:51 AMPablo
06/13/2024, 10:52 AMPablo
06/13/2024, 10:53 AMStylianos Gakis
06/13/2024, 10:53 AMPablo
06/13/2024, 10:53 AMPablo
06/13/2024, 10:54 AMPablo
06/13/2024, 10:54 AMPablo
06/13/2024, 10:55 AMPablo
06/13/2024, 10:56 AMPablo
06/13/2024, 10:56 AMStylianos Gakis
06/13/2024, 10:56 AMi thought it was possible to pass for example a function that returns the airport details to the composable as a parameter, and call that function from the composable for each list item to get the detailsYeah that should be possible too, but you also want it to not get them once, but to listen to the DB changes right?
Stylianos Gakis
06/13/2024, 10:56 AMPablo
06/13/2024, 10:57 AMPablo
06/13/2024, 10:57 AMPablo
06/13/2024, 10:57 AMStylianos Gakis
06/13/2024, 10:57 AMPablo
06/13/2024, 10:58 AMPablo
06/13/2024, 10:58 AM@Entity
data class Airport(
@PrimaryKey
val id: Int,
@ColumnInfo(name = "iata_code")
val iataCode: String,
val name: String,
val passengers: Int
)
@Entity
data class Favorite(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "departure_code")
val departureCode: String,
@ColumnInfo(name = "destination_code")
val destinationCode: String
)
Pablo
06/13/2024, 10:58 AMPablo
06/13/2024, 10:59 AMPablo
06/13/2024, 10:59 AMoverride fun getAirport(iata: String): Flow<Airport> {
return flightsDao.getAirport(iata)
}
Pablo
06/13/2024, 10:59 AMPablo
06/13/2024, 10:59 AMPablo
06/13/2024, 11:16 AMdata class FavoriteWithAirportsData(
val depart: Airport,
val arrival: Airport
)
val favoritesWithAirportsData: Flow<List<FavoriteWithAirportsData>> = favorites.flatMapLatest {
val list = mutableListOf<FavoriteWithAirportsData>()
for (favorite in it) {
list.add(FavoriteWithAirportsData(
flightRepository.getAirport(favorite.departureCode).first(),
flightRepository.getAirport(favorite.destinationCode).first()
))
}
flowOf(list)
}
And I pass it to the composable as a parameter like this:
favoritesWithAirportsData = viewModel.favoritesWithAirportsData.collectAsStateWithLifecycle(
initialValue = emptyList()
).value,
Pablo
06/13/2024, 11:17 AMPablo
06/13/2024, 11:20 AMPablo
06/13/2024, 11:24 AM.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
emptyList()
)
The result is that it is still being executed even if the favorites are not being displayedPablo
06/13/2024, 11:24 AMMark Murphy
06/13/2024, 9:27 PMsealed interface FavoritesState {
data object Loading : FavoritesState
data class Content(...) : FavoritesState
data object Error : FavoritesState
}
data class ViewState {
// other good stuff goes here
val favorites: FavoritesState = FavoritesState.Loading
}
(where ...
in FavoritesState.Content
is filled in with whatever the data is that you need for the favorites)
Note that we start off with favorites
as holding Loading
.
Your panel composable for the favorites can then use an exhaustive when
to render those three possibilities as you see fit:
when (uiState.favorites) {
Loading -> TODO()
is Content -> TODO()
Error -> TODO()
}
Your viewmodel can have a function that updates your viewstate to fill in favorites
with either your Flow
(on success) wrapped in a Content
or Error
(if you have an exception), if favorites
is currently Loading
or Error
(i.e., you do not already have the content).
The last piece is to determine where and when to call that function. For early testing, you might just call it all the time. Later, as you look to optimize the work, you could call it when the user clicks on whatever it is that shows your panel.
The net effect is:
• You show some loading UI at the outset
• You show the content once the query completes and you get your detailed favorites data
• You show some sort of error message if you had a problem loading the dataPablo
06/14/2024, 6:19 AMPablo
06/14/2024, 6:19 AMPablo
06/14/2024, 6:21 AMdata class FavoriteAndAirports(
@Embedded val favorite: Favorite,
@Relation(
parentColumn = "departure_code",
entityColumn = "iata_code"
)
val departureAirport: Airport
@Relation(
parentColumn = "destination_code",
entityColumn = "iata_code"
)
val destinationAirport: Airport
)
assuming here that departure_code
and destination_code
are pointing towards the iata_code
of the Airport
entity. Then, you can access it in your DAO similar to this:
@Transaction
@Query("SELECT * FROM Favorite")
fun getFavorites(): Flow<List<FavoriteAndAirports>>
with this Flow you can access the data from your ViewModel:
val favorites = flightRepository
.getFavorites()
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
And in the compossable you can get it like this:
val favorites = flightViewModel.favorites.collectAsStateWithLifecycle()
Pablo
06/14/2024, 6:24 AMStylianos Gakis
06/14/2024, 10:16 AMSharingStarted.Lazily
instead of SharingStarted.WhileSubscribed
, if you stop showing the favorites "panel" as you say, you will keep that flow active and still collecting, even though you are no longer interested in the result.