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.