How to use Dagger 2.1x, MVVM with Kotlin: Important changes and pitfalls to avoid
At work, I have really amazing colleagues. So we set out to build a fairly complex app and, as usual, we decided to use Dagger for our dependency injection, MVVM architecture, and, of course, made it Kotlin only.
We were familiar with using MVVM and Dagger so we were pretty confident it wouldn't be a big deal. Yes of course — if we'll also be using Android Studio's auto-convert Java to Kotlin feature.
But some things weren't immediately obvious for us. So, if you are like us and intend to use Dagger, MVVM, and Kotlin, this article outlines common pitfalls we ran into and gives you a good head start with fewer errors.
I'll start with the straw that made me put up this post. This article is really intended for those who have a fair knowledge of using Dagger, MVVM, and Kotlin. Brace yourself — this may be a slightly long post, but I'll try my best to keep it simple for even beginners.
ViewModelFactory
Remember to add @JvmSuppressWildcards to suppress Java wildcards.
ViewModelFactory class basically helps you dynamically create ViewModels for your Activities and Fragments. Wildcards in Java code converted to Kotlin generate an error. This is mainly becuse the lifecycle component codebase is still in Java . This is the sample ViewModel code in Java.
public class ViewModelFactory implements ViewModelProvider.Factory {
private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;
@Inject
public ViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
this.creators = creators;
}
@SuppressWarnings("unchecked")
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
Provider<? extends ViewModel> creator = creators.get(modelClass);
if (creator == null) {
for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
if (modelClass.isAssignableFrom(entry.getKey())) {
creator = entry.getValue();
break;
}
}
}
if (creator == null) {
throw new IllegalArgumentException("unknown model class " + modelClass);
}
try {
return (T) creator.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Can you guess where the annotation @JvmSuppressWildcards
goes in the Kotlin generated code? Here it is.
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 {
var creator: Provider<ViewModel>? = creators[modelClass]
if (creator == null) {
for ((key, value) in creators) {
if (modelClass.isAssignableFrom(key)) {
creator = value
break
}
}
}
if (creator == null) throw IllegalArgumentException("unknown model class " + modelClass)
try {
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
The @JvmSuppressWildcards
annotation makes Kotlin suppress using wildcards in generics.
ViewModelKeys
Avoid the Java to Kotlin conversion for generating your ViewModelKey classes in Android Studio.
ViewModelKeys helps you map your ViewModel classes so ViewModelFactory
can correctly provide/inject them. Though this one is a bit trivial, in Java, the ViewModelKey is given as this:
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MapKey
@interface ViewModelKey {
Class<? extends ViewModel> value();
}
Using the generated code from the Java to Kotlin converter would make us blindly use our favorite Ctrl+Alt+Enter or Cmd+Alt+Enter. Doing this, however, would import java.lang.annotation.*
classes, and will make your code fail to compile. We should be careful to import the kotlin.reflect.KClass
only. Here is an equivalent Kotlin code:
@MustBeDocumented
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
Annotation Processing
No more need for
kapt {generateStubs = true }
When using libraries like Dagger and Butterknife, which depend on annotations to function properly, you always need to add the annotationProcessor dependency. In Java:
annotationProcessor 'com.google.dagger:dagger-compiler:$dagger_version'
Using Kotlin, we would Kotlin's annotation Processor kapt
: This will enable the compiler to generate the stub classes required for interoperability between Java and Kotlin.
kapt 'com.google.dagger:dagger-compiler:$dagger_version'
Then apply the kotlin-kapt
plugin in your app level build.gradle file at the top:
apply plugin: 'kotlin-kapt'
Kotlin versions less than 1.2 require that you also include generateStubs
in the configuration of your app build.gradle
. You can be sure that not adding it may not be the reason you app is not compiling.
Android Tests and JUnits Tests
If using the jack tool chain for Android test, your build.gradle
would have:
// Android Test
androidTestAnnotationProcessor 'com.google.dagger:dagger-compiler:$dagger_version'
// JUnit test
testAnnotationProcessor 'com.google.dagger:dagger-compiler:$dagger_version'
But using the kotlin-kapt
plugin, this should change to:
// Android Test
kaptAndroidTest 'com.google.dagger:dagger-compiler:$dagger_version'
// JUnit test
kaptTest 'com.google.dagger:dagger-compiler:$dagger_version'
Providing static objects using Dagger
Use @JvmStatic to provide static methodss
Using static methods would increase its invocation speed by 15-20%. This defeats the purpose of Dagger providing objects through constructors and may even cause memory leaks. In Java, providing static objects:
@Provides static SampleObject returnObject(...) {
}
However, Kotlin does not have the static
property but gives us the companion object
to use. Using Dagger, we still need to add an additional @JvmStatic
annotation to make it work.
@Module
class SampleModule {
@Module
companion object {
@JvmStatic
@Provides
fun providesObject(): SampleObject = SampleObject()
}
}
Injecting field using Dagger:
Never use the
internal
modfier for Injected fields
The internal
visibility modifier makes a field visible everywhere in the same module
. The Kotlin official documentation says any client inside this module who sees the declaring class sees its internal members
.
However Dagger
needs package private/public
access in order to access annotated fields. In Kotlin, internal
modifier is not a substitution for Java's package-private
access modifier.
Conclusion
I feel you are ready to take on Dagger, MVVM, and Kotlin and deploy it in your app straight away.
If you feel lost on the basics of using Dagger and MVVM, I'll recommend you take a look at Android's architecture components and its samples.
Awesome tips there!