Understanding Dagger 2 Multibindings + ViewModel
Also: GithubViewModelFactory explained and rewritten in Kotlin.
Originally published on blog.kotlin-academy.com by Alex Facciorusso
Hi guys,
today we are going to explain how to use the fabulous Dagger 2 multibindings with new Architecture Components ’ ViewModel
class.
Many of us, when started learning how to use the new ViewModel
class, had a look at the GithubBrowserSample example on GitHub (this is a GitHub-ception!).
Maybe, the first class we looked in that bunch of useful code, is the so called GithubViewModelFactory
class (here’s a link to the file in the repository).
This class is a perfect example of when Dagger 2 multibindings can be real lifesavers.
Before to dive into code, I should make a little premise.
For building customViewModel
classes with argument-passing constructors (e.g. for passing custom data or @Inject
annotated constructors), we must provide a class that extends ViewModelProvider.Factory
, returning instances of our custom ViewModels into the create()
method.
So, the first thing we can notice is the constructor of the class, that takes the not very beautiful to see Map<Class<? extends ViewModel>, Provider<ViewModel>>
parameter. What is this monster?
Let’s read this huge parameter type with more attention: it is a map that has a Class
that extends ViewModel
as key, and a Provider
of ViewModel
(a Dagger 2-specific class that let us to provide — and so instantiate — a dependency-injected class) as value.
Ok ok, but what can I do with this object?
Well, we just said that the create
method of our custom ViewModelProvider.Factory
expects an instance of the ViewModel as return value. This method takes the type of the ViewModel that was requested from an Activity
or Fragment
as parameter. Pairing this type (our Class
object) to something that creates a ViewModel of the same type, we can, of course, instantiate and return that class to the system.
What is that “something” that can give us the ViewModel
of a determinate type? The answer is theProvider
value in the map that Dagger 2 injected us.
Let’s talk about Multibindings
The big big title on the official Multibindings documentation
Dagger 2 can associate a dependency Provider
to a given key, and inject it into a Map.
This is accomplished using the @IntoMap
annotation on a method that produces the value we want to associate with a given key.
The key is, on the other hand, specified using a custom annotation that is itself annotated with @MapKey
. Here it is the annotation used for creating a Map with, as key type, a Class<? extends ViewModel>
object.
@Retention(RetentionPolicy.RUNTIME)
@MapKey
@interface ViewModelKey {
Class<? extends ViewModel> value();
}
Our value parameter type will be the type of our map’s key.
So this Module
method:
@Binds
@IntoMap
@ViewModelKey(UserViewModel.class)
abstract ViewModel bindUserViewModel(UserViewModel
userViewModel);
says roughly: “inject this object into a Map
( @IntoMap
annotation) using UserViewModel.class
as key, and a Provider
that will build a UserViewModel
object (the parameter of the @Binds
annotation) as value”.
In this way, we can inject into a Dagger 2 managed object, a map of type… guessed: our famous Map<Class<? extends ViewModel>, Provider<ViewModel>>
.
Now we know that we can use the Class
object taken as parameter in the ViewModelProvider.Factory.create()
method to retrieve a provider for that ViewModel. This is exactly what googlers done in that method. Let’s examine the code piece by piece.
The possible provider for the given ViewModel is getted from the Map:
Provider<? extends ViewModel> creator = creators.get(modelClass);
if our Provider
s map hasn’t got that specific key, we will check if there is a subclass of the ViewModel
we must instantiate:
if (creator == null) {
for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
if (modelClass.isAssignableFrom(entry.getKey())) {
creator = entry.getValue();
break;
}
}
}
if all previous attempts of getting a valid Provider from the map will fail, we throw an exception:
if (creator == null) {
throw new IllegalArgumentException("unknown model class " +
modelClass);
}
finally, we can let Dagger to create our ViewModel
by invoking the get()
method on the Provider
object, as said before, and casting it to our final type:
try {
return (T) creator.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
We have injected successfully a ViewModel with Dagger 2!
The Kotlin translation and… a little extra!
Well, this class and its related classes are a must if you use Dagger 2 along with ViewModel, but I personally love Kotlin, so let’s convert it into this beautiful and concise language:
@Singleton
class DaggerViewModelFactory
@Inject constructor(
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?:
creators.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
?: throw IllegalArgumentException("unknown model class " + modelClass)
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
@Module
abstract class ViewModelFactoryModule {
@Binds
internal abstract fun bindViewModelFactory(factory: GithubViewModelFactory): ViewModelProvider.Factory
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
So if you are using Kotlin in your Android application (and if not, you really should 🙄) you can just copy and paste this code in your project or…
… or you can use the 🔥🔥 NEW AND FLAMING 🔥🔥 library I created!
You can find it at this GitHub link, along with the instructions to install and use it (see the README).
Thank you for reading this article! If you liked it, you can make me know pressing the *clap* button (and to ★ the GitHub repo to say me “thanks”) 😌. See you in comments! 😁
To be up-to-date with great news on Kotlin Academy, subscribe to the newsletter, observe Twitter and follow.