Binding ViewModels with non-empty constructors

May 1, 2020

forrest-hills.JPG

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.

 
Previous
Previous

Using SQLDelight in Kotlin Multiplatform Project

Next
Next

Injecting Coroutine Dispatcher with Dagger or Hilt