Search code examples
kotlinevent-handlingandroid-jetpack-composedesktop-application

Handling buttons deep in the compose hierarchy to switch views


What's the recommended way of handling button clicks in the compose hierarchy? I've got a list with items that have an edit-button next to them. When the user hits the button, the application should change the view to an editor for that item.

I guess I could pass the click handler function down through each composable, but this doesn't seem to be right as other composables would just relay it to deeper ones.

Is there an established pattern for such cases?

@Composable
fun App() {
    
    var isBrowsing = remember { mutableStateOf(true) }
    if (isBrowsing)    
        Browser() // <-- somewhere inside there is the "Edit" button
    } else {
        Editor() // <-- somewhere inside there is the "Exit" button
    }
    
}

Solution

  • Please have a look at this section in the documentation about CompositionLocal. It aims to make certain values available down the whole Composable hierarchy without the need to explicitly pass those values down with each Composable.

    In the samples, this is primarily used for variables that are related to theming. But you can adapt it to work with a callback function instead. However, please be aware that I am not sure whether this is a recommended approach:

    First, place this variable somewhere outside of a Composable function:

    val EditorCallback = compositionLocalOf<(Boolean) -> Unit> {}
    

    Then add this code to your Composables:

    @Composable
    fun App() {
        
        var isBrowsing by remember { mutableStateOf(true) }
    
        CompositionLocalProvider(EditorCallback provides { edit -> isBrowsing = !edit}) {
            if (isBrowsing)    
                Browser()
            } else {
                Editor()
            }
        }
    }
    
    @Composable
    Editor() {
        var editorCallback = EditorCallback.current
        Button(onClick = { editorCallback(true) }) {
            Text(text = "EDIT")
        }
    }
    

    The safer approach would be inversion of control. Create a ViewModel that holds a Boolean, and insert this ViewModel into the deeply nested Composable as well as the top-level Composable like this:

    @Composable
    fun App(myViewModel: MyViewModel = viewModel()) {
        
        if (myViewModel.isBrowsing)    
            Browser() // <-- somewhere inside there is the "Edit" button
        } else {
            Editor() // <-- somewhere inside there is the "Exit" button
        }
        
    }
    
    @Composable
    Editor(myViewModel: MyViewModel = viewModel()) {
        Button(onClick = { myViewModel.isBrowsing = true }) {
            Text(text = "EDIT")
        }
    }
    

    Besides those suggestions, I think it really is okay in Compose to have a quite verbose number of parameters that are passed around. At the end, this improves testability of individual Composables.