BenjyTec
BenjyTec

Reputation: 10595

Set Composable value parameter to result of suspend function

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

Answers (1)

Mark
Mark

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

Related Questions