Dagger2 And ViewModel Injection

In this post I'll go over how you can bring dagger2 injection to your android app's view model objects. I'll also briefly cover how to make this work in a multi module android project in case you've started to break your app into modules in preparation for a more dynamic future.

I'm going to assume that you are already familiar with Dagger2 and have set it up in your project. If you aren't familiar with Dagger2 then I would strongly suggest taking some time to read through their user guide to learn how and why you should use it in your android project.

The companion project to this post can be found here. It's been kept purposefully simple and lean to highlight just the important parts of viewmodel injection and multi module support.

Let's start by taking a look at how we would normally get a viewmodel in our
application's activities without dagger in the picture.


fun onCreate(savedInstanceState: Bundle?){
        val viewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java)

}

This would work if our viewmodel had no dependencies or failed to make use of its constructor to have all of its dependencies 'injected' into it. We don't want to code in either of those worlds. Luckily for us the lifecycle library has us covered and provides an overload that let's us pass a custom ViewModelProvider.Factory object. The android architecture team even provides an example of one that works with dagger in their GithubBrowser sample app! We'll be able to make use of that with only minor modification when we break our app into multiple modules.

Before we do that, let's look at what this factory is doing. It has a map as a constructor parameter that uses classes as the key and returns a Provider for a given viewmodel class as the value. The map is important because it is what allows the factory to do its job. It takes advantage of a feature in dagger called 'multibinding'.

Basically it allows us to tell dagger that we'd like to contribute objects of our choosing into a map that then becomes injectable anywhere in our app. How do we set this map up?
It's pretty easy, we just implement an abstract method in an @module class like this:

@Module
abstract class ViewModelsModule{
    @Binds
    @IntoMap
    @ViewModelKey(MyViewModel::class)
    abstract fun bindMyViewModel(myViewModel: MyViewModel) : ViewModel
}

The @Binds annotation combined with @IntoMap and @ViewModelKey will tell dagger that we'd like to take this implementation of the ViewModel class and make it available in an injectable map with MyViewModel::classas a key to that map. Now at runtime when dagger creates our view model factory it will be given a map that contains a MyViewModel object that it can then work with in its create() method that just so happens to take a Class variable as its argument :)

What does the @ViewModelKey annotation look like? and where did it come from?

@Documented
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

It is simply an annotation class that most importantly uses the @MapKey annotation to tell dagger that it can be used to determine keys in multi bound maps. It also narrows the range of classes that we might use to only those that inherit from ViewModel.

Dagger provides an @ClassKey annotation if you didn't want or need such a restriction on your multibound map or set.

Now back in our activity we can just inject our viewmodel factory and then use it when we call ViewModelProviders.of() like this:


class MyActivity {

    @Inject
    lateinit var viewModelFactory: ViewModelFactory


    fun onCreate(savedInstanceState: Bundle?){
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)

        val viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
    }


}

Our factory's create() method will now be invoked by the chain of events that gets kicked off when we call ViewModelProviders.of() and our factory's create method will make use of our multi bound map to return an instance of our viewmodel. That way instead of declaring viewmodels that look like this:

class MainActivityViewModel : ViewModel() {

    val itemsLiveData: MutableLiveData<Item[]> = MutableLiveData()

    init{
        Log.d("ViewModel", "Nothing useful or easily testable is going to happen here.")
    }
}

to something more like this:

class MainActivityViewModel 
@Inject constructor(
    private val itemsRepository: ItemsRepository,
    private val analyticsWrapper: AnalyticsWrapper,
    private val asyncWorkScheduler: Executor
)

: ViewModel() {

    val itemsLiveData: MutableLiveData<Item[]> = MutableLiveData()

    init{
        Log.d("ViewModel", "This viewmodel is useful and easy to test!")
    }
}

If we kept our app as a single large app module with only a single dagger component then our work would be done here.
We would just keep adding viewmodel's to our multi bound map by adding more and more @Binds and @IntoMap methods in our component's ViewModelsModule class.

If we want to break our app into multiple android library modules though and let each activity live in its own subcomponent we'll have a little more work to do.
Luckily for us though dagger's multi bound map feature will make this pretty easy. The hard part will be deciding how to split your monolithic app module into independent feature modules and I'm afraid there's no easy one size fits all tutorial that I could write on that subject.

Let's imagine we have features we could break out though and go execute on that.

The first thing we'd need to do is create a new android module named
"common" where we could stick things like the ViewModelKey annotation along with our ViewModelFactory instance. That way these things will be accessible in any feature modules that we add to the project.

Once we've created that common module and moved our "common" classes into it we could create a feature module named "alpha".
The alpha feature module would contain an activity, a viewmodel, and a dagger subcomponent along with all the other goodies we might need in order to implement this particular feature.

The next step would be to make sure that our AlphaViewModel made it into our ViewModelFactory's map of viewmodels.
As luck would have it the dagger library supports this by default. We can contribute an object from a subcomponent into a multibound map that comes from a parent component.

We would have something like this:


//AppComponent.kt in our app module
@Singleton
@Component(modules = [AndroidInjectionModule::class,
    MainActivityInjectionModule::class,
    AlphaActivityInjectionModule::class,
])
interface AppComponent : AndroidInjector<DaggerViewModelApp> {

    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<DaggerViewModelApp>() {}

}


//AlphaSubcomponent.kt in our alpha module

@Subcomponent(modules = [AlphaActivityModule::class])
interface AlphaActivitySubcomponent: AndroidInjector<AlphaActivity> {

    @Subcomponent.Builder
    abstract class Builder: AndroidInjector.Builder<AlphaActivity>() {}
}

@Module
abstract class AlphaActivityModule {

//here is where we contribute our viewmodel
    @Binds
    @IntoMap
    @ViewModelKey(AlphaActivityViewModel::class)
    abstract fun bindAlphaActivityViewModel(alphaActivityViewModel: AlphaActivityViewModel): ViewModel

}

@Module(subcomponents = [AlphaActivitySubcomponent::class])
abstract class AlphaActivityInjectionModule {

    @Binds
    @IntoMap
    @ActivityKey(AlphaActivity::class)
    abstract fun bindAlphaActivityInjectorFactory(builder: AlphaActivitySubcomponent.Builder)
            : AndroidInjector.Factory<out Activity>
}

The only thing we'd need to change from our single module setup is the @Singleton annotation that currently sits on our ViewModelFactory.
That annotation tells dagger to create one and only one of these objects and so when we inject it into our AlphaActivity it would be the same factory that was used in MainActivity and it would have been created with its map long before our AlphaActivity's subcomponent ever came to life and could contribute the AlphaViewModel.
Once we've removed that annotation our AlphaActivity will receive an instance of a ViewModelFactory that has been given a map containing its AlphaViewModel.

That's all there is to marrying dagger2 to your app's viewmodels. I've included a few helpful links below as always.


Helpful Links

https://github.com/bltuckerdevblog/DaggerViewModels

https://google.github.io/dagger/users-guide

https://google.github.io/dagger/multibindings

https://developer.android.com/jetpack/docs/guide