Setup Dagger 2 For Android Kotlin

March 7, 2018
Architecture Components: Activity and ViewModel

Why use Dependency Injection?

If you find yourself passing around too many dependencies through the constructor (passing Context to Database -> passing Database to DAO -> passing DAO to ViewModel, etc.), you might want to inject these dependencies automatically rather keep writing code to pass the dependency parameters around. In a way, it simplified your code (though it doesn’t reduce code).

Another benefit of Dependency Injection is Unit Test, where you can easily swap the dependency implementation.

Dagger 2 uses a static, compile-time implementation, so it has a very minimal impact on runtime CPU and Memory cost.

If you want to learn about progression of Depedency Injection from Spring -> Guice -> Dagger -> Dagger 2, watch this video.

Why don’t use Dependency Injection?

As someone who has never written Dependency Injection code before, reading Dependency Injection code is pretty confusing. It is not easy to figure where the dependency parameters came from.

Coding Dependency Injection is not exactly simple or easy. Dagger 2 simplified a lot of boilerplate code, but it still involves quite a bit of code to setup and quite a few concepts and annotations to grasp. The learning curve is moderate (not easy though).

My advice is don’t use Dependency Injection until you think you need it. Though it simplifies code writing, it adds additional setups and configurations.

How to learn Dagger 2

The best way to learn is from the official documentation. Focus on the User’s Guide to get a basic understanding of the concept (Component & Module) and annotations (@Inject, @Provide).

To learn about using Dagger 2 in Android, read the Dagger & Android section. Setup Dagger 2 on Android is different from standard Java application.

I found Dagger 2 has too many concepts and annotations (you can achieve the same thing with many different ways, each with its own pros and cons). Don’t worry about learning everything at one go, just the basic is enough at this point.

It should still be confusing at this stage, so let’s just dive into the code.

Android Dagger 2 Kotlin Code

In this sample code, I am using Dagger 2 to inject Architecture Component’s Room and ViewModel into Activity.

Android Dagger2 Gradle Dependencies

Setup dependencies in Module (app/build.gradle) Gradle file ($dagger_version=2.14.1, check for latest Dagger version).

apply plugin: 'kotlin-kapt'

kapt {
    generateStubs = true
}

dependencies {
    // dagger
    kapt "com.google.dagger:dagger-compiler:$dagger_version"
    implementation "com.google.dagger:dagger:$dagger_version"

    // dagger android
    kapt "com.google.dagger:dagger-android-processor:$dagger_version"
    implementation "com.google.dagger:dagger-android:$dagger_version"
    implementation "com.google.dagger:dagger-android-support:$dagger_version"
}

Application Class

Make sure you setup Android Application Class.

NOTE: remember to specify the android:name property in the the <application> node in AndroidManifest.xml.

Add the following code to application class.

class LuaApp : Application(), HasActivityInjector {
    @Inject
    lateinit var activityInjector: DispatchingAndroidInjector<Activity>

    override fun onCreate() {
        super.onCreate()
        // initialize Dagger
        DaggerAppComponent.builder().application(this).build().inject(this)
    }

    // this is required to setup Dagger2 for Activity
    override fun activityInjector(): AndroidInjector<Activity> = activityInjector
}

NOTE: DaggerAppComponent is auto-generated from AppComponent, which we shall create later.

AppComponent

I prefer to store all Dagger 2 setup and configuration code under an injection package.

Create the AppComponent class under injection package. A component is used to initialize modules. Dagger 2 will auto-generate DaggerAppComponent which is used for initialization at Application.

@Singleton
@Component(modules = arrayOf(AndroidInjectionModule::class, AppModule::class, ActivityModule::class))
interface AppComponent {
    @Component.Builder
    interface Builder {
        // provide Application instance into DI
        @BindsInstance
        fun application(application: Application): Builder

        fun build(): AppComponent
    }

    // this is needed because LuaApp has @Inject 
    fun inject(luaApp: LuaApp)
}

NOTE: AndroidInjectionModule is a class of Dagger 2, we shall create AppModule and ActivityModule later.

AppModule

AppModule shall be used initialize objects used across our application, such as Room(Database), Retrofit, Shared Preference, etc.

@Module
internal class AppModule {
    @Singleton
    @Provides
    fun provideDatabase(app: Application): AppDatabase {
        return Room.databaseBuilder(app,
                AppDatabase::class.java, "LuaPass.db")
                .build()
    }

    @Singleton
    @Provides
    fun providePasswordDao(db: AppDatabase): PasswordDao {
        return db.passwordDao()
    }

    @Singleton
    @Provides
    fun provideCategoryDao(db: AppDatabase): CategoryDao {
        return db.categoryDao()
    }
}

NOTE: @Provides is used when we don’t want to create a instantance through constructor, else an @Inject at the class constructor would be sufficient.

ActivityModule

All activities intended to use Dagger2 @Inject should be listed here.

@Module
abstract class ActivityModule {
    @ContributesAndroidInjector
    internal abstract fun contributeMainActivity(): MainActivity

    @ContributesAndroidInjector
    internal abstract fun contributePasswordActivity(): PasswordActivity

    // Add bindings for other sub-components here
}

NOTE: we avoid writing @Subcomponent code for each activity by using @ContributesAndroidInjector. If your activity subcomponents need additional methods, then you need to write your own subcomponents.

Activity

In your activity class, implement the following code. Now you can inject PasswordDao into activity (without the need to pass in Context and Database object manually).

class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var passwordDao: PasswordDao
    
    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
        ...
        
        passwordDao.fetchAll().observe(this, Observer<List<Password>> { list ->
            ...
        })
    }
}

NOTE: PasswordDao is provided by AppModule.

NOTE: If you need to inject into Fragment, refer to this sample code.

Inject ViewModel into Activity

Usually, a ViewModel will require a DataSource (Room DAO is my case). Add @Inject code into ViewModel constructor.

class PasswordViewModel @Inject constructor(private val dataSource: PasswordDao) : ViewModel() {

}

ViewModel is created by ViewModelProvider.Factory, so we need to inject it as well.

@Singleton
class ViewModelFactory(): ViewModelProvider.Factory {
    @Inject
    lateinit var passwordProvider : Provider<PasswordViewModel>

    @Inject
    lateinit var mainProvider : Provider<MainViewModel>    

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(PasswordViewModel::class.java)) {
            return passwordProvider.get() as T
        } else if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return mainProvider.get() as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

NOTE: Provider<T> is available for all injectable class, using it to create an injectable object at a later time (Provider is not necessary if immediate instantiation is required).

Now we can inject ViewModelFactory into Activity.

class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var viewModelFactory: ViewModelFactory

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
        ...
        
        viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
    }
}

NOTE: notice we didn’t create any @Provide code in AppModule for ViewModelFactory. The reason is ViewModelFactory can be instantiated normally using the constructor without using static methods like build().

Inject ViewModel Map into ViewModelFactory

Every time we added a new ViewModel, we need to edit the ViewModelFactory. There is a nice Dagger 2 trick to auto-generate a Class to Provider Map.

Edit ViewModelFactory.

@Singleton
class ViewModelFactory @Inject constructor(private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>): ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        var creator: Provider<out ViewModel>? = creators.get(modelClass)
        if (creator == null) {
            for (entry in creators.entries) {
                if (modelClass.isAssignableFrom(entry.key)) {
                    creator = entry.value
                    break
                }
            }
        }
        if (creator == null) {
            throw IllegalArgumentException("unknown model class $modelClass")
        }

        try {
            return creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

NOTE: @JvmSuppressWildcards is required

To provide for Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>> injection, we need to create ViewModelModule.

@Module
internal abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(PasswordViewModel::class)
    abstract fun bindPasswordViewModel(viewModel : PasswordViewModel) : ViewModel

    @Binds
    @IntoMap
    @ViewModelKey(MainViewModel::class)
    abstract fun bindMainViewModel(viewModel : MainViewModel) : ViewModel
}

For dagger_version = '2.21', use the following code for ViewModelModule.

@Documented
@Target(
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

NOTE: the purpose of using @Binds here is so that we could use @IntoMap.

Code for ViewModelKey.

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

Edit AppModule to include ViewModelModule

@Module(includes = arrayOf(ViewModelModule::class))
internal class AppModule {

}

Now we had moved the code of mapping class from ViewModelFactory to ViewModelModule.

If bump into error Unresolved reference: DaggerAppComponent, make sure

  • kapt generateStubs = true is set
  • include kapt dagger-compiler and implementation dagger in dependencies

NOTE: As you can see in this example, Dagger 2 (Dependency Injection) doesn’t reduce code, but it does make it more configurable for unit testing. It simplifies application logic by moving the code to Dagger 2 configuration.

NOTE: Refer to Android Dagger 2 Injection For Fragment (Kotlin).

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.