GETTING STARTED TO DAGGER MULTIBINDINGS WITH ANDROID VIEW MODEL FROM JETPACK
Dagger is a great solution for handling Dependency Injection, Until now we used to provide a single dependency to our client but what about providing a collection of dependencies to our client at once. Now, the Dagger multibindings concept comes to the scene. We can provide a collection of dependencies as bulk to our client using Set or map. If you did not read the first two articles in this series you can find them here
After finishing this tutorial you will be able to understand the following:
- Understanding Dagger Multibindings
- Use Multibindings with Android Architecture View Model
Prerequisites :
To be able to get the most out of this chapter you will need:
- Android Studio 3.2.1 or higher
- Emulator of phone
- Kotlin Basics
- Intermediate level as Android Developer
Understanding Dagger Multibindings :
Dagger allows you to bind several objects into a collection even when the objects are bound in different modules using multibindings. Dagger assembles the collection so that application code can inject it without depending directly on the individual bindings.
You could use multibindings to implement a plugin architecture, for example, where several modules can contribute individual plugin interface implementations so that a central class can use the entire set of plugins. Or you could have several modules contribute individual service providers to a map, keyed by name.(Dagger Decumentation)Dagger Documentation Website
Let’s start with a simple example to be able to understand the idea behind the scene. the idea is, we need to provide a set of strings so we can do the same result with three different ways like the following:
package com.daggerudemy.di.multibindingsimport dagger.Provides
import dagger.multibindings.ElementsIntoSet
import dagger.multibindings.IntoSet@Module
class SimpleMultibindings { @Provides
fun provideSetOfStrings():Set<String>{
return setOf("A")
}
@Provides
@IntoSet
fun provideUsingIntoSet():String{
return "A"
} @Provides
@ElementsIntoSet
fun provideUsingElementIntoSet():Set<String>{
return setOf("A")
}
}
As you can see in Figure1, @Provides would inject a new set of strings but for @InfoSet and @ElementIntoSet you would inject in the same data set
If you understand the idea of @IntoSet and @ElementIntoSet correctly, you can get the idea of @IntoMap, you just Inject Items in a map where you define the key and the value. As you can see in the figure2 you have two maps the first map has string keys A, B and values Apple and Boy. For the second map, you have Class key Type as a key and String values. I hope you get the idea now.
Multibindings with Android Architecture View Model:
View Model component is one of the Jetpack and you can use it for handling different scenarios like configuration change when the device rotates also for set data that comes from when services into LiveData objects and you can use it separate the business outside the view. Now let’s talk about how to integrate it with Dagger using Multibindings but let’s first define the problem and why we need to use multibindings with it. To be able to use view model in your app you should first add the following dependency in your build.gradle file
def lifecycle_version = "1.1.1"
// live data with view model
implementation "android.arch.lifecycle:extensions:$lifecycle_version"
Then you need to define your Custom View model class by extending the ViewModel() like the following:
package com.daggerudemy.di.multibindingsimport android.arch.lifecycle.ViewModel
import javax.inject.Injectclass CustomViewModel @Inject constructor(): ViewModel() {
}
Now the question is how can I get an Instance from this Custom view model?. To get Instance from CustomViewModel do the following :
val customViewModel = ViewModelProviders.of(this).get(CustomViewModel::class.java)
but most of the time we need to pass parameters to our custom view model like UseCase Instance or Repository Instance for example so we need to use ViewModelFactory you need to get Instance from ViewModelFactory because of view model which implements an interface called ViewModelProvider.factory like this
public interface Factory {
/**
* Creates a new instance of the given {@code Class}.
* <p>
*
* @param modelClass a {@code Class} whose instance is requested
* @param <T> The type parameter for the ViewModel.
* @return a newly created ViewModel
*/
@NonNull
<T extends ViewModel> T create(@NonNull Class<T> modelClass);
}
When you call get() method of ViewModelProvider it will call the create() method of ViewModelFactory and by using the class type it can return an instance of required view model like the following:
/**
* Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
* an activity), associated with this {@code ViewModelProvider}.
* <p>
* The created ViewModel is associated with the given scope and will be retained
* as long as the scope is alive (e.g. if it is an activity, until it is
* finished or process is killed).
*
* @param modelClass The class of the ViewModel to create an instance of it if it is not
* present.
* @param <T> The type parameter for the ViewModel.
* @return A ViewModel that is an instance of the given type {@code T}.
*/
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
} /**
* Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
* an activity), associated with this {@code ViewModelProvider}.
* <p>
* The created ViewModel is associated with the given scope and will be retained
* as long as the scope is alive (e.g. if it is an activity, until it is
* finished or process is killed).
*
* @param key The key to use to identify the ViewModel.
* @param modelClass The class of the ViewModel to create an instance of it if it is not
* present.
* @param <T> The type parameter for the ViewModel.
* @return A ViewModel that is an instance of the given type {@code T}.
*/
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key); if (modelClass.isInstance(viewModel)) {
//noinspection unchecked
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
} viewModel = mFactory.create(modelClass);
mViewModelStore.put(key, viewModel);
//noinspection unchecked
return (T) viewModel;
}
So if we need to create a new view model we will edit the following custom view model factory and this is very bad practice, you need a generic and clean solution
class CustomViewModelFactory @Inject constructor(private val repository: DataRepository): ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(CustomViewModel::class.java!!)) {
CustomViewModel(this.repository) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}}
The solution:
We are going to use Dagger multi binding to solve this issue by using @IntoMap annotation, actually, this will tell Dagger to generate a map in compile-time and pass it to our view model factory. This map has key and value, the key is our ViewModel Type(CustomViewModel::class) and the value is the instance of our CustomViewModel like the following:
@Suppress("UNCHECKED_CAST")
class CustomViewModelFactory @Inject constructor(private val viewModelsMap: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>) :
ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = viewModelsMap[modelClass] ?:
viewModelsMap.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)
}
}}
Also, we need to create a new module for ViewModelFactory like this
@Module
abstract class CustomViewModelFactoryModule {
@Binds
abstract fun bindViewModelFactory(viewModelFactory: CustomViewModelFactory): ViewModelProvider.Factory}
As we learned from the previous section about @IntoMap, we learned it is a map of key and value and if you do not primitive key you need to define the custom type and because view model is not so we need to define a new custom type key like this
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
finally, we need a new module for providing the CustomViewModel like this
@Module
abstract class CustomViewModelModule {
@Binds
@IntoMap
@ViewModelKey(CustomViewModel::class)
abstract fun bindMyViewModel(customViewModel: CustomViewModel): ViewModel
}
Now by using @Binds, @IntoMap and @ViewModelKey we inserted a new item in the map with a type of (CustomViewModel::class) , let’s create a component to use this in our client which is the main activity in this case like this
@ActivityScope
@Component(dependencies = [...], modules = [CustomViewModelFactoryModule::class, CustomViewModelModule::class])
interface MyComponent {
fun inject(mainActivity: MainActivity)
}@Inject
lateinit var viewModeFactory: ViewModelFactoryoverride fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
.....
myViewModel = ViewModelProviders.of(this, this.viewModeFactory).get (MyViewModel::class.java)
}
so this solution is very scalable because if we create a new ViewModel for a new section of our application we will need only to create a Component with the same ViewModelFactoryModule and the new Module that will provide the new ViewModel.
Conclusion:
In this tutorial, we talked about the role of Dagger Multibindings for injecting collection at once and we learned how to use it with Jetpack Android View Model to be able to inject view model in our activity, in the next tutorial we will talk about the use of multi binding in Delegate Adapter and third-party libraries. Finally, if you like this tutorial please share it with your friends