kotlinandroid-jetpack-compose

How to pass MutableState<Boolean> to a Composable in Jetpack Compose


In an app I'm working on, I'm having trouble passing a MutableState<Boolean> from a parent composable to a child composable. The reason I want to use MutableState is that this variable will be changed in the child composable.

Here’s a simplified example of what I'm trying to accomplish:

@OptIn(UnstableApi::class)
@Composable
fun ParentComposable(
    screenHeight: Dp,
    barHeight: Dp,
) {
    var scrollEnabled by remember { mutableStateOf(true) }

    Column(
        modifier = Modifier
            .padding(top = barHeight)
            .height(screenHeight - barHeight)
            .fillMaxSize()
    ) {
        UserButtons(
            scrollEnabled = scrollEnabled // the error occurs here 
        )
    }
}


@Composable
fun UserButtons(
    scrollEnabled: MutableState<Boolean>
) {
    IconButton(
        onClick = {
            scrollEnabled.value = false
            println("Scroll has been deactivated")
        }) {}
}

The error message I get is:

Type mismatch: inferred type is Boolean but MutableState was expected

I'm guessing that the issue is when passing scrollEnabled (which is the MutableState<Boolean>) to the child composable UserButtons; for some reason it's detected as a normal Boolean even if it's declared as a MutableState<Boolean>.

How can I make sure that the parameter is correctly detected as an MutableState?


Solution

  • The reason I want to use MutableState is that this variable will be changed in the child composable.

    You should not do that.
    Jetpack Compose uses the unidirectional data flow pattern. That means that

    That means that you should not pass a MutableState<T> to a child Composable, because when the child Composable modifies data from the parent Composable, the data flows upwards. Instead, use a callback function to notify the parent Composable to update the value of the scrollEnabled variable:

    @Composable
    fun UserButtons(
        scrollEnabled: Boolean,
        updateScrollEnabled: (Boolean) -> Unit
    ) {
        IconButton(
            onClick = {
                updateScrollEnabled(false)
                println("Scroll has been deactivated")
            }
        ) {
            //...
        }
    }
    

    In your ParentComposable, call the UserButtons Composable like this:

    UserButtons(
        scrollEnabled = scrollEnabled,  // data flowing downwards
        updateScrollEnabled = { updatedValue ->
            scrollEnabled = updatedValue  // event flowing upwards
        }
    )
    

    Side Note

    The reason you were getting the type mismatch is because you were using the by keyword, which transforms a MutableState<T> into T by calling the getValue function of MutableState.
    So, doing

    val scrollEnabled = remember { mutableStateOf(false) }
    Text(text = "scrollEnabled: ${scrollEnabled.value})
    

    is the same as

    var scrollEnabled by remember { mutableStateOf(false) }
    Text(text = "scrollEnabled: ${scrollEnabled})
    

    only that it is shorter and more convenient to use.