Codementor Events

How to use Dagger 2.1x, MVVM with Kotlin: Important changes and pitfalls to avoid

Published Feb 21, 2018Last updated Aug 20, 2018
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.

Discover and read more posts from Ememobong Akpanekpo
get started
post comments1Reply
Idorenyin Obong
7 years ago

Awesome tips there!