31

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.
        }
    }
ArtixModernal
  • 661
  • 2
  • 8
  • 21

7 Answers7

45

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
}
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
Hasan Haghniya
  • 2,347
  • 4
  • 19
  • 29
  • 15
    why this not documented in android website? – ArtixModernal Jun 15 '21 at 08:08
  • 5
    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. – Rik Jan 16 '22 at 03:01
  • Thanks for this, I created multiple activities in my app assuming its not possible to call viewModel Factory classes in composable functions.... – Anudeep Ananth Apr 17 '22 at 03:19
16

If you use Hilt, 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.] }
            )
        ) {
            [Your composable]()
        }
    )
)

On view model:

class ViewModel @Inject constructor(savedStateHandle: SavedStateHandle) {

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

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

Pedro Sequeira
  • 311
  • 3
  • 4
  • 1
    Best answer, thank you! [Pro Android Dev article to explain](https://proandroiddev.com/passing-safe-args-to-your-viewmodel-with-hilt-366762ff3f57) – beyondtheteal Mar 09 '23 at 19:28
9

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() {}
Mike
  • 321
  • 4
  • 10
3

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

  • 4
    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. – Rik Jan 16 '22 at 03:33
2

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 */
}
Rik
  • 676
  • 7
  • 11
0

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"
)
ShadeToD
  • 403
  • 9
  • 22
0

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 {
            MyViewModel(AuthRepoImpl(apiService), HomeRepoImpl(apiService))
        })
Gastón Saillén
  • 12,319
  • 5
  • 67
  • 77