androidkotlinandroid-jetpack-composeandroid-viewmodel

Jetpack Compose: Should I pass ViewModel down the component tree or use callbacks?


I have a nested component structure in Jetpack Compose where a deeply nested component needs to interact with a ViewModel. I'm trying to determine the best approach for handling this interaction.

Here's my component structure:

class MainViewModel : ViewModel() {
    fun handleButtonClick() {
        // Do something
    }
}

// Approach 1: Passing ViewModel down
@Composable
fun ComposableA(viewModel: MainViewModel) {
    ComposableB()
    ComposableC(viewModel = viewModel)
}

@Composable
fun ComposableC(viewModel: MainViewModel) {
    ComposableD(viewModel = viewModel)
}

@Composable
fun ComposableD(viewModel: MainViewModel) {
    Button(onClick = { viewModel.handleButtonClick() }) {
        Text("Click Me")
    }
}

// Approach 2: Using Callbacks
@Composable
fun ComposableA(viewModel: MainViewModel) {
    val onButtonClick = { viewModel.handleButtonClick() }
    
    ComposableB()
    ComposableC(onButtonClick = onButtonClick)
}

@Composable
fun ComposableC(onButtonClick: () -> Unit) {
    ComposableD(onButtonClick = onButtonClick)
}

@Composable
fun ComposableD(onButtonClick: () -> Unit) {
    Button(onClick = onButtonClick) {
        Text("Click Me")
    }
}

Which approach is considered better practice?

Solution

  • Passing ViewModel to Composables make them depend on that ViewModel which makes preview or testing Composable difficult, especially if there are injected params in ViewModel.

    To run preview you will need to lots of work if ViewModel has useCases with repositories, repositories or savedStateHandle just to run a Composable.

    Also this coupling with ViewModel is bad for reusability as well. If you need to use same Composable in another screen you will also need to pass the same ViewModel to that screen for that. Keeping Composable inputs as plain as possible makes them more usable. Coupling with class that can have complex inputs like ViewModel is not recommended.

    Stability is not main issue with ViewModel because you can use @Stable with ViewModel to inform compiler your ViewModel is stable, or you can enable strong-skipping for unstable classes to not have undesired recompositions.

    Also, if ViewModel.handleClick is unstable using approach below won't prevent unwanted recompositions by the way. You can check lambda-stability section of this answer.

    @Composable
    fun ComposableA(viewModel: MainViewModel) {
        val onButtonClick = { viewModel.handleButtonClick() }
        
        ComposableB()
        ComposableC(onButtonClick = onButtonClick)
    }
    

    You should either use remember for lambda

     val onButtonClick = remember{ viewModel.handleButtonClick() }
    

    or enable strong-skipping which wraps lambdas with remember by default.