Which is the correct approach to query Room from a...
# compose
p
Which is the correct approach to query Room from a composable? should composables receive the viewmodel as parameter to be able to use viewmodel functions that access the room repo to query data? in this case the uistate only have id's to paint a list of items, and those Id's must be used to query other tables to get full data using these ids, so the data isn't available in memory when the composable is painted. The data must be queried to the database.
s
For such questions, do take a look at these https://github.com/android/compose-samples , they got plenty of examples on how to do many things. And in particular, Jetcaster https://github.com/android/compose-samples/tree/main/Jetcaster is using room, so you can look in there for room related stuff. With that in mind, inside Jetcaster itself, room exposes flows https://github.com/android/compose-samples/blob/main/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/CategoriesDao.kt And on the ViewModel end, the source of the data which is a
Flow
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.
p
I didn't explain myself correctly then, The composable is a minor composable, it doesn't have the viewmodel. How can it access the Room database without the viewmodel?
should it receive the viewmodel as parameter? I don't think so
s
No, it should just receive a
List<T>
then. The collection should happen at the screen level composable which does in fact have access to VM
p
but what if that is not possible? imagine this situation. You have a screen with many options and subpanels, one of the panels has a button to display favorites, and you have favorites table on the uistate, it's ok, but... favorites only contains the ID of the airport, and you don't have the full table of airports in the uistate nor memory, because they are thousands of airports, so your composable needs to use those id's to get info from the database querying it
you can't get all that stuff on the main screen in memory, you must ask for it only if favorites panel is being shown
but the favorites panel can't retrieve the data, so, how to achieve this?
s
What is a "panel"
p
I mean a composable which represents a panel with favorites
s
So it's part of your home screen then, right?
p
yes, it's a sub sub sub sub composable
contained in some other composables
s
If it's part of the same screen, it has to come from the same ViewModel as it's the source of truth for that screen. If you don't want to always collect that long list, you could theoretically have your ViewModel expose another StateFlow made with
stateIn()
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.
Also it may be applicable at your case to use a slot API for that part of the screen, https://chrisbanes.me/posts/slotting-in-with-compose-ui/ so that you can just define all this at a high level in your composable, and just pass down the
@Composable () -> Unit
down that sub sub sub sub sub composable. It might make this easier to reason about, but ymmv
p
a stateflow of what? there is not a class that contains two favorites details
favorite class only contains the id's of the airports
you mean creating a class called "favoritesForComposable" which contains two airports and fullfill that class on the viewmodel requesting room all the favorites airports when the screen is opened?
it seems to be a bad practice, what happens if those favorites are never necessary because the panel is not openned? that work (requesting hundreds of airports from the database with their id's stored on the favorites table) should be done only if the favorites composable is being displayed on the screen
also it forces to create a new class that maybe will be never used
s
A StateFlow of the type that your panel needs to render the thing. And as I said, you can not consume that StateFlow if there is no need to, which wouldn't query the database at all, if you use .stateIn() inside your ViewModel https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html
p
maybe does exists a better and simpler way to achieve this?
well that is too complex for me, I don't understand how to achieve that
i 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 details
p
i don't understand how to transform that into my needs
what do you think about my proposal?
i 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 details
s
i 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 details
Yeah that should be possible too, but you also want it to not get them once, but to listen to the DB changes right?
You need to at some point have a "cold" source of truth, and then at some point turn it "hot" when you actually want to start listening on new changes
p
those concepts are too advanced
and I don't know how to merge the two id's into two real airports inside a flow
each favorite have two id's and I need to recover two airports, for each favorite, and there are a lot of favorites
s
I am sorry, I don't know of another way to achieve the fact that you want to have your information ready to fetch, but not fetch until you have someone looking at it without having a cold flow. I don't know how else I can help you here.
p
maybe can you help me merging those id's into two airports and fit them inside the flow?
Copy code
@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
)
as you can see favorite onlu have the iata of each airport
and I have this function into my room repo
Copy code
override fun getAirport(iata: String): Flow<Airport> {
    return flightsDao.getAirport(iata)
}
to get the airport for an iata
how to transform that into a flow?
I did this in the viewmodel:
Copy code
data 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:
Copy code
favoritesWithAirportsData = viewModel.favoritesWithAirportsData.collectAsStateWithLifecycle(
    initialValue = emptyList()
).value,
It seems to work, but, is it doing the job even when the composable is not being displayed? the fact to have the flow on the viewmodel will initialize the flow and do the job?
the documentation says that stateIn produces a hot flow, so I avoided that, instead, flatMapLatest doesn't specify if it's hot or cold
well, I added a log to know if it is being called even if it doesn't displays the favorites, and in fact, it's being called... so I tryed adding your stateIn proposal, and I added this to the end of the flow:
Copy code
.stateIn(
    viewModelScope,
    SharingStarted.WhileSubscribed(5_000),
    emptyList()
)
The result is that it is still being executed even if the favorites are not being displayed
do you know how to solve this?
m
Your overarching problem is "I don't want to load a piece of my viewstate until it is needed". That really suggests that you need finer-grained viewstates, but, we can ignore that for the moment. You want to only load favorites-with-airport-info if the user views the favorites panel. To do that: First, define your viewstate, using the "loading/content/error" pattern for the favorites, such as:
Copy code
sealed 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:
Copy code
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 data
p
thank you, but it didn't solve the main issue that is how to get the data
I finally did it using room relationships, I hope this is the correct way to achieve it
the first step is to model the two airports belonging to one favorite by using two one-to-one relationships.´Creating a data class like this:
Copy code
data 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:
Copy code
@Transaction
@Query("SELECT * FROM Favorite")
fun getFavorites(): Flow<List<FavoriteAndAirports>>
with this Flow you can access the data from your ViewModel:
Copy code
val favorites = flightRepository
        .getFavorites()
        .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
And in the compossable you can get it like this:
Copy code
val favorites = flightViewModel.favorites.collectAsStateWithLifecycle()
it seems that doing it this way, you can make the work only if favorites is presented on screen, not always, and it seems to be the correct way to achieve it
s
Note that with
SharingStarted.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.
👍 1