Binding ViewModels with non-empty constructors
May 1, 2020
The recommended way to bind a ViewModel in a Fragment is by using byViewModels extension.
To use the extension, add fragment-ktx dependency in your module build.gradle:
implementation "androidx.fragment:fragment-ktx:X.Y.Z"
Once you do that, binding a ViewModel in an Activity is easy:
import androidx.activity.viewModels
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
...
}
Binding a ViewModel in a Fragment is similar and provides both viewModels and activityVIewModels extensions on androidx.fragment.app.Fragment:
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
class MainFragment : Fragment() {
// fragment-scoped view model
private val viewModel: MainViewModel by viewModels()
// host activity-scoped view model
private val activityViewModel: MainViewModel by activityViewModels()
...
}
Introducing ViewModelFactory
The problem with the extensions above is that only ViewModels with empty (zero-parameter) constructors can be injected.
In order to bind a ViewModel with a non-empty constructor when using Dagger, you can use a ViewModelFactory to instantiate and bind your ViewModel. Let’s look at the code to accomplish it.
Approach 1 (Just OK)
Note: this option is not ideal—be sure to read on for a better approach below.
The same by viewModels delegate can be used and you could define a separate ViewModelFactory for each ViewModel you need to instantiate:
import androidx.fragment.app.viewModels
class MyFragment : BaseFragment() {
@Inject
lateinit var viewModelFactory: MyViewModelFactory
private val viewModel by viewModels<MyViewModel>
The factory itself has to be defined as well:
class MyViewModelFactory(
private val repo: MyRepository
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MyViewModel(repo) as T
}
}
Finally, you would need to instantiate MyViewModelFactory via @Provides
@Module
abstract class MyModule {
@Binds
abstract fun myRepository(myRepositoryImpl: MyRepositoryImpl): MyRepository
@Module
companion object {
@JvmStatic
@Provides
fun provideMyViewModelFactory(repo: MyRepository) =
AppDetailViewModelFactory(repo)
}
}
The above would work but the problem with this approach is that you need to define a separate ViewModelFactory which knows how instantiate the ViewModel it is responsible for. That’s a lot of boilerplate.
Approach 2 (Better)
A better option is to use Dagger Multibindings. This will allow creating a single ViewModelFactory only which can be used to produce your ViewModels.
We’ll be using the same by viewModels delegate similar to the above approach.
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import javax.inject.Inject
class MyFragment : Fragment() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel: MyViewModel by viewModels { viewModelFactory }
This time, both the ViewModelFactory and all the ViewModels can be defined in the same ViewModelModule:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Binds
import dagger.MapKey
import dagger.Module
import dagger.multibindings.IntoMap
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
import kotlin.reflect.KClass
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(MyViewModel::class)
abstract fun bindMyViewModel(view: MyViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(MyOtherViewModel::class)
abstract fun bindMyOtherViewModel(view: MyOtherViewModel): ViewModel
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
@Singleton
class ViewModelFactory @Inject constructor(
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull
?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
return creator.get() as T
}
}
Then each of the ViewModels can inject its dependencies by annotating its constructors with an @Inject annotation.
class MyViewModel @Inject constructor(
private val repo: MyRepository
) : ViewModel()
I hope you see the benefit of this approach especially for larger apps with many ViewModels. A single ViewModelFactory will be responsible for injecting all of your ViewModels. Less code means fewer bugs and we, developers, get to spend more time solving interesting problems rather than copy/pasting code.
See sample source code in this project
ViewModel Scopes
There is a problem with the last approach however—lack of scoping support. If that’s something that you’d like to support in your app, you may benefit from the approach above where each ViewModel has its own ViewModelFactory for a given scope. Not every app or screen needs it though.