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.

    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 which uses equals for unstable params too. By default unstable params use referential equality(===) to determine if that param has changed if there is a composition in a scope they are in or parent of Composable but with strong skipping if new value is equal to previous one recomposition does not get scheduled.

    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 = rememer{ viewModel.handleButtonClick() }
    

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