How to use Dagger 2.1x, MVVM with Kotlin: Important changes and pitfalls to avoid
Originally published in February, 2018 here.
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.
publicclassViewModelFactoryimplementsViewModelProvider
.
Factory
{
privatefinal
Map<
Class
<?
extendsViewModel
>,
Provider
<
ViewModel
>>
creators
; @Inject
public
ViewModelFactory(Map<
Class
<?
extendsViewModel
>,
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
<?
extendsViewModel
>,
Provider
<
ViewModel
>>
entry
:
creators
.
entrySet
()) {
if
(modelClass.isAssignableFrom(entry.getKey())) { creator = entry.getValue(); break; } } }
if
(creator == null) {
thrownew
IllegalArgumentException("unknown model class " + modelClass); }
try
{
return
(T) creator.get(); }
catch
(Exception e) {
thrownew
RuntimeException(e); } } }
Can you guess where the annotation @JvmSuppressWildcards
goes in the Kotlin generated code? Here it is.
classViewModelFactory@Injectconstructor
(
privateval
creators: Map<Class<
out
ViewModel>,
@JvmSuppressWildcards
Provider<ViewModel>>) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")overridefun<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 @
interfaceViewModelKey
{
Class
<?
extendsViewModel
>
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:
@Providesstatic
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.