Reputation: 10595
I am new to Compose and Kotlin. I have an application using a Room database. In the frontend, there is a Composable containing an Icon Composable. I want the Icon resource to be set depending on the result of a database operation that is executed within a suspend function.
My Composable looks like this:
@Composable
fun MoviePreview(movie : ApiMoviePreview, viewModel: ApiMovieViewModel) {
Card(
modifier = ...
) {
Row(
modifier = ...
) {
IconButton(
onClick = {
//...
}) {
Icon(
imageVector =
// This code does not work, as isMovieOnWatchList() is a suspend function and cannot be called directly
if (viewModel.isMovieOnWatchlist(movie.id)) {
Icons.Outlined.BookmarkAdded
} else {
Icons.Filled.Add
}
,
contentDescription = stringResource(id = R.string.addToWatchlist)
)
}
}
}
}
The function that I need to call is a suspend function, because Room requires its database operations to happen on a seperate thread. The function isMovieOnWatchlist()
looks like this:
suspend fun isMovieOnWatchlist(id: Long) {
return movieRepository.isMovieOnWatchlist(id)
}
What would the appropriate way be to achieve the desired behaviour? I already stumbled across Coroutines, but the problem is that there seems to be no way to just return a value out of the coroutine function.
Upvotes: 2
Views: 1835
Reputation: 9929
A better approach would be to prepare the data so everything you need is in the data/value class rather than performing live lookup per row/item which is not very efficient. I assume you have 2 tables and you'd probably want a LEFT JOIN however all these details are not included.
With Room it even includes implementations that use the Flow
api, meaning it will observe the data when information in either table changes and re-runs the original query to provide you with the new changed dataset.
However this is out of scope of your original question but should you want to explore this then here is a good start : https://developer.android.com/codelabs/basic-android-kotlin-training-intro-room-flow#0
To your original question. This is likely achievable with a LaunchedEffect
and some observed MutableState<ImageVector?>
object within the composable, something like:
@Composable
fun MoviePreview(
movie: ApiMoviePreview,
viewModel: ApiMovieViewModel
) {
var icon by remember { mutableStateOf<ImageVector?>(value = null) } // null or default icon until update by result below
Card {
Row {
IconButton(onClick = {}) {
icon?.run {
Icon(
imageVector = this,
contentDescription = stringResource(id = R.string.addToWatchlist))
}
}
}
}
LaunchedEffect(Unit) {
// execute suspending function in provided scope closure and update icon state value once complete
icon = if (viewModel.isMovieOnWatchlist(movie.id)) {
Icons.Outlined.BookmarkAdded
} else Icons.Filled.Add
}
}
Update
As pointed out in comments an easier way to do this (which removes some boilerplate) would be to use produceState.
Updated example :
@Composable
fun MoviePreview(
movie: ApiMoviePreview,
viewModel: ApiMovieViewModel
) {
val icon by produceState<ImageVector?>(initialValue = null) {
value = if (viewModel.isMovieOnWatchlist(movie.id)) {
Icons.Outlined.BookmarkAdded
} else Icons.Filled.Add
}
Card {
Row {
IconButton(onClick = {}) {
icon?.run {
Icon(
imageVector = this,
contentDescription = stringResource(id = R.string.addToWatchlist))
}
}
}
}
}
The implementation of produceState
abstracts the MutableState<T>
object and creates a LaunchedEffect
for you. The lambda is a suspend
function which gives you write access to MutableState<T>
value. The client will only ever have read access encapsulating its mutatbility to the lambda.
This is more concise and reduces repetitive boilerplate code in the original answer.
Upvotes: 3