45

How can we pass parameter to viewModel in Jetpack Compose?

This is my composable

    @Composable
    fun UsersList() {
      val myViewModel: MyViewModel = viewModel("db2name") // pass param like this
    }

This is viewModel

    class MyViewModel(private val dbname) : ViewModel() {
        private val users: MutableLiveData<List<User>> by lazy {
            MutableLiveData<List<User>>().also {
                loadUsers()
            }
        }
    
        fun getUsers(): LiveData<List<User>> {
            return users
        }
    
        private fun loadUsers() {
            // Do an asynchronous operation to fetch users.
        }
    }
1

9 Answers 9

61

you need to create a factory to pass dynamic parameter to ViewModel like this:

class MyViewModelFactory(private val dbname: String) :
    ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T = MyViewModel(dbname) as T
}

then use your factory like this in composable functions:

@Composable
fun UsersList() {
    val myViewModel: MyViewModel =
        viewModel(factory = MyViewModelFactory("db2name")) // pass param like this
}

and now you have access to dbname parameter in your ViewModel:

class MyViewModel(private val dbname:String) : ViewModel() {
    // ...rest of the viewModel logics here
}
Sign up to request clarification or add additional context in comments.

6 Comments

why this not documented in android website?
This is actually pretty tough to find. If you play with the DataStore Preferences codelabs, of which there are two, one of them (Working with Preferences DataStore, not Preferences DataStore) has an end solution that does make use of this implementation in tying a UserRespository class to a ViewModel but the codelab itself doesn't teach you anything about it and full of discrepencies (codelab not repo). I guess we're expected to know this from the Android View side as its strongly recommended in the View system to always use ViewModelProvider to manage ViewModel instances.
Thanks for this, I created multiple activities in my app assuming its not possible to call viewModel Factory classes in composable functions....
I used this and my app crashes randomly when going in the background and foreground, don't use this.
What if one needs Hilt with this. Or any other type of Di? And what if I need to access a parent (i.e. sharedviewmodel from the backstackentry)?
|
28

If you use Navigation component, you get this for free in SavedStateHandle for view model.

Pass the argument to the composable that calls the view model and retrieve it with the same name on view model from saved state handle.

Like this:

On NavHost:

NavHost(
(...)
    composable(
            route = [route string like this $[route]/{$[argument name]}],
            arguments = listOf(
                navArgument([argument name]) { type = NavType.[type: Int/String/Boolean/etc.] }
            )
        ) {
            // access your viewModel in your composable
            val viewModel: MyViewModel = viewModel()
        }
    )
)

On view model:

class ViewModel(savedStateHandle: SavedStateHandle) {

    private val argument = checkNotNull(savedStateHandle.get<[type]>([argument name]))
}

Your argument will magically appear without having a view model factory.

2 Comments

Best answer, thank you! Pro Android Dev article to explain
Although the concept is stupid because whoever is developing the ViewModel needs to guess that are parameters coming from a savedState, this seems the correct way of doing it when navigation is involved.
22

With latest library androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2 is simple as this:

val myViewModel = viewModel {
  MyViewModel(param)
}

Comments

10

The other solutions work, but you have to create a factory for each ViewModel which seems overkill.

The more universal solution is like this:

inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
    object : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(aClass: Class<T>):T = f() as T
    }

And use it like this:

@Composable
fun MainScreen() {
    val viewModel: MyViewModel = viewModel(factory = viewModelFactory {
        MyViewModel("Test Name")
    })
}

For ViewModel like this:

class MyViewModel(
  val name: String
):ViewModel() {}

2 Comments

An Android app without at least three Unchecked Cast is considered a dull app.
In that @Composable function, I lack access to a 'viewModel()' function with a factory argument. Unsure how to use this proposed solution.
4

There is no need anymore to create the viewModelFactory by ourselves.

Now it is included inside androidx.lifecycle.viewmodel

/**
 * Creates an [InitializerViewModelFactory] with the initializers provided in the builder.
 */
public inline fun viewModelFactory(
    builder: InitializerViewModelFactoryBuilder.() -> Unit
): ViewModelProvider.Factory = InitializerViewModelFactoryBuilder().apply(builder).build()

And can be used as

 val viewModel: MyViewModel = viewModel(factory = viewModelFactory {
    initializer {
            MyViewModel(AuthRepoImpl(apiService), HomeRepoImpl(apiService))
    }
 })

2 Comments

the usage example isn't valid though. you have to call addInitializer() on context receiver in the lambda
@deviant I updated the example code to have the initializer block wrapping the constructor. This should work now.
3

Here's some Jetpack Compose/Kotlin-specific syntax for implementing the same:

ui/settings/SettingsViewModel.kt

class SettingsViewModel(
    private val settingsRepository: SettingsRepository
) : ViewModel() {
    /* Your implementation */
}

class SettingsViewModelFactory(
    private val settingsRepository: SettingsRepository
) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create( modelClass: Class<T> ): T {
        if( modelClass.isAssignableFrom( SettingsViewModel::class.java ) ) {
            @Suppress( "UNCHECKED_CAST" )
            return SettingsViewModel( settingsRepository ) as T
        }
        throw IllegalArgumentException( "Unknown ViewModel Class" )
    }    

}

Then:

MainActivity.kt


/* dataStore by preferencesDataStore */

class MainActivity : ComponentActivity() {
    private lateinit var settingsRepository: SettingsRepository
    
    // Here we instantiate our ViewModel leveraging delegates and
    // a trailing lambda
    private val settingsViewModel by viewModels<SettingsViewModel> {
        SettingsViewModelFactory(
            settingsRepository
        )
    }

    /* onCreate -> setContent -> etc */
}

Comments

2

Usually there is no common case where you need to do this. In android MVVM viewmodels get their data from repositories through dependency injection.

Here is the official documentation to the recommended android architecture: https://developer.android.com/jetpack/guide#recommended-app-arch

1 Comment

This is actually great, but I think that its also an overload for any small application. I believe a common case for doing what this question asks is for any small app. I think it'll only abstract beginners away from a wealth of understanding too in relieving them from understanding how to manage a small set of dependencies, etc. However, I do agree 100% that this is the way to go in any moderately-sized application, or when you've decided for yourself to implement this architecture in a one-all template you use for anything; good for small apps in such a case too.
2

Although most of solutions above work, their authors assume that the only parameters you will use in ViewModel are those provided manually.

So here is more up-to-date approach if you rather to use DI (Hilt):

  1. Add hilt-navigation-compose version >= 1.2.0
implementation "androidx.hilt:hilt-navigation-compose:1.2.0"
  1. Use AssistedFactory for your ViewModel
@HiltViewModel(assistedFactory = MyViewModel.AssistedFactory::class)
class MyViewModel @AssistedInject constructor(
    @Assisted customParameter: Int,
    private val someInjectedRepository: SomeRepository
) : ViewModel() {

...

    @dagger.assisted.AssistedFactory
    interface AssistedFactory {
        fun create(customParameter: Int): MyViewModel
    }
}
  1. Use special hiltViewModel implementation when requesting ViewModel from composable function
@Composable
fun SomeComposableScreen(
   customParameter: Int,
   navController: NavController,
   viewModel: MyViewModel = hiltViewModel<MyViewModel, MyViewModel.AssistedFactory> { factory ->
     factory.create(mediaIdSelected)
   }
) {
  ...
}

Now you have requested ViewModel with an argument from composable, while leaving other ViewModel parameters up to Hilt. Here is an article covering this aspect.

Comments

1

As it was mentioned by @Secret Keeper you need to create factory.

If your ViewModel has dependencies, viewModel() takes an optional ViewModelProvider.Factory as a parameter.

class MyViewModelFactory(
    private val dbname: String
) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
            return MyViewModel(dbname) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

To create your viewModel you will pass optional parameter. Inside your Composable you can do something like this.

val viewModel: MyViewModel = viewModel(
factory = MyViewModelFactory(
    dbname = "myDbName"
)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.