Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why should viewModel() only be used at the root composable and the instance never passed?

This documentation about using ViewModel with Compose states:

Note: Due to their lifecycle and scoping, you should access and call ViewModel instances at screen-level composables, that is, close to a root composable called from an activity, fragment, or destination of a Navigation graph. You should never pass down ViewModel instances to other composables, pass only the data they need and functions that perform the required logic as parameters.

However, a ViewModel is scoped to the Activity or Fragment, so I would expect it to be OK if you call viewModel() wherever you like in the hierarchy, and even in multiple places in the same hierarchy. I would expect to be getting the same instance returned regardless.

  1. What is the drawback of calling viewModel() deeper in the hierarchy than the root composable of a screen?
  2. Why shouldn't a ViewModel instance be passed to a nested Composable?

The obvious drawback to following this advice is that you may have to do a lot of state hoisting, leading to Composables with many parameters that must be connected manually and correctly, making code less readable and maintainable.

like image 221
Tenfour04 Avatar asked Oct 29 '25 23:10

Tenfour04


1 Answers

It is actually completely fine to use viewModel() anywhere any deep in your hierarchy. There is no performance impact whatsoever since viewmodels are cached by design and are retrieved instantly with that call. Then what could go wrong? It just goes against some of the essential coding principals that would make your code a nightmare to read.

@Composable
fun UserProfileCard() {
    val viewModel = viewModel<UserViewModel>()
    
    Column {
        UserAvatar()  // Calls viewModel() internally
        UserName()    // Calls viewModel() internally
        UserBio()     // Calls viewModel() internally
        UserStats()   // Calls viewModel() internally
        EditButton()  // Calls viewModel() internally
    }
}

The above composable looks concise and right to the point. No bunch of parameters to pass down. viewModel() is supposed to save us both the effort and eye strain of passing down so many parameters. Right? Congratulations! You just:

  1. Made your code impossible to preview
@Preview
@Composable
fun UserAvatarPreview() {
    UserAvatar()  // CRASH - No ViewModelStoreOwner in preview mode
}
  1. Made your code impossible to test
@Test
fun testUserAvatar() {
    composeTestRule.setContent {
        UserAvatar()  //  Need to mock ViewModelProvider, ViewModelStore,
                      //    ViewModelStoreOwner, set up Hilt/Koin/whatever,
                      //    create the actual ViewModel with all dependencies...
    }
}
  1. Created hidden global dependencies everywhere

Someone reading UserAvatar() has literally no idea it depends on a ViewModel. They have to:

  • Open the file

  • Read the implementation

  • Find the viewModel() call

  • Figure out what UserViewModel does

  • Trace where that ViewModel comes from

  • Pray it's scoped correctly

Meanwhile, this is as clear as day and immediately readable and obvious (makes it a lot more maintainable?):

@Composable
fun UserAvatar(avatarUrl: String) {
    Image(avatarUrl)
}

If you're passing down too many parameters, then you're doing something wrong. Maybe break down your composables to smaller pieces. Someone else might work on your code and get himself in a quicksand of code mess that they can't get out off. Always prioritize maintainability and readability, it's not always about performance.

like image 85
Yurowitz Avatar answered Nov 01 '25 12:11

Yurowitz