Gradle configuration changes

In order to use Dagger 2 w/ Kotlin it’s necessary to use kapt instead of annotationProcessor (which is what works in Java).

  1. How to setup Kotlin w/ Android and Dagger 2.
  2. Dagger 2 setup for Android using Java, not Kotlin.

So, in build.gradle:

  1. kapt has to be added as a plugin
  2. annotationProcessor has to be replaced by kapt
apply plugin: 'kotlin-kapt'

dependencies {
    ext.dagger2_version = '2.17'
    // Basic Dagger 2 (required)
    implementation "com.google.dagger:dagger:$dagger2_version"
    kapt "com.google.dagger:dagger-compiler:$dagger2_version"

    // dagger.android package (optional)
    implementation "com.google.dagger:dagger-android:$dagger2_version"
    kapt "com.google.dagger:dagger-android-processor:$dagger2_version"

    // Support library support (optional)
    kapt "com.google.dagger:dagger-android-support:$dagger2_version"
}

Custom scopes

  • Tutorial

    • This tutorial does a great job going over how to use scopes and makes it clear by showing how simple the mechanism really is.

    • A component that’s marked w/ @Scope annotation can have its module’s providers marked w/ the same scope.

    • There’s nothing special about @Singleton. Just as w/ any other scope, when you mark a component w/ a scope annotation, you also have to have to mark the provider(s) as well in the module(s) w/ the same one. In other words, you can’t mix scopes. For @Singleton this means creating the component in an Application, so all the objects provided by modules marked w/ this scope are available everywhere. It’s just convention.

    • The idea behind using custom scopes shifts the complexity on object creation and management, to the component/module/provider side. And simplicity is provided to the client/consumer side, where you simply mark the fields you need injected using @Inject and it “just works”!

  • Tutorial

    • This article is a deep dive into the code generated by @Scope and what it is really doing under the covers. Scope simply creates a cached provider.
  • Tutorial

    • This article provides a complex example of multiple scopes and subcomponents.
  • SO

    • This stackoverflow discussion is great about creating more sophisticated subcomponents.

Creating and using a custom scope

Here’s an example of a custom scope called @ActivityScope. The syntax for Java is quite different than Kotlin.

@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope

Here’s the Java equivalent.

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}

We use custom scope when we want to reuse dependencies for a custom amount of time. For example, the @ActivityScope above can be used for dependencies that only should be available for the lifecycle of an Activity. Or @UserScope for all dependencies tied to a user session.

It’s however important to understand that custom scopes work exactly like @Singleton, which is simply a Dagger 2 defined scope annotation. Any dependency annotated with any scope will be reused (and injected into clients) as long as we use the corresponding component. There is no magic involved, we must manually create and throw away component in accordance with our desired lifecycle.

We could even use @Singleton on all dependencies and still have custom lifecycle. Dagger will however throw helpful build exceptions if we mix different scopes. So, custom scopes will be valuable both as documentation and to find mistakes early.

We can use components directly or use the concept of subcomponents to deal with our wanted lifecycle. Subcomponents can be done using dependencies=[] or @Subcomponent annotation. The main difference between them is that you have to export all the objects that you would like dependent-components to have explicitly in the component interface (as methods). Here’s an example.

Approach 1 - Component and dependency

Top level component with @Singleton scope

Here’s the top level ApplicationComponent which has @Singleton scope and is created by the Android Application class.

@Singleton
@Component(modules = [ApplicationModule::class, GMSClientsModule::class])
interface ApplicationComponent {
    // Expose objects created by modules to any other components
    // dependent on this component.
    fun placeDetectionClient(): PlaceDetectionClient
    fun geoDataClient(): GeoDataClient
    fun fusedLocationProviderClient(): FusedLocationProviderClient
}

@Module
class ApplicationModule(private val application: Application) {
    @Singleton
    @Provides
    fun provideContext(): Context {
        return application
    }
}

@Module
class GMSClientsModule {
    @Singleton
    @Provides
    fun providesPlaceDetectionClient(context: Context): PlaceDetectionClient {
        return Places.getPlaceDetectionClient(context)
    }

    @Singleton
    @Provides
    fun providesGeoDataClient(context: Context): GeoDataClient {
        return Places.getGeoDataClient(context)
    }

    @Singleton
    @Provides
    fun providesLocation(context: Context): FusedLocationProviderClient {
        return LocationServices.getFusedLocationProviderClient(
                context)
    }
}

Component with @ActivityScope that depends on the component above

Here’s another component w/ scope of @ActivityScope that depends on the component above.

@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope

@ActivityScope
@Component(dependencies = [ApplicationComponent::class],
           modules = [ExecutorModule::class])
interface ActivityComponent {
    fun inject(placesAPI: PlacesAPI)
}

@Module
class ExecutorModule {
    @Provides
    @ActivityScope
    fun provideExecutor(): ExecutorWrapper {
        return ExecutorWrapper()
    }
}

class ExecutorWrapper {
    lateinit var executor: ExecutorService

    fun create() {
        executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
    }

    fun destroy() {
        executor.shutdown()
    }
}

Using these components

In order to use them, two things need to happen.

  1. The Application class needs to create the ApplicationComponent
  2. An activity (or lifecycle observer that is bound to its lifecycle) has to create and destroy the ActivityComponent.

Here’s code for what happens in the Application class:

class MyApplication : Application() {
    lateinit var applicationComponent: ApplicationComponent

    override fun onCreate() {
        super.onCreate()
        applicationComponent = DaggerApplicationComponent.builder()
                .applicationModule(ApplicationModule(this))
                .build()
    }

}

Here’s code for what happens in the lifecycle observer that is bound to Activity lifecycle:

    // Places API Clients.
    @Inject
    lateinit var currentPlaceClient: PlaceDetectionClient
    @Inject
    lateinit var geoDataClient: GeoDataClient
    // Background Executor.
    @Inject
    lateinit var executorWrapper: ExecutorWrapper

    // Lifecycle hooks.
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun connect() {
        // Dagger 2 component creation.
        with((app as MyApplication).applicationComponent) {
            DaggerActivityComponent.builder()
                    .applicationComponent(this)
                    .build()
                    .inject(this@PlacesAPI)
        }

        "ON_CREATE ⇢ Create Executor ✅".log()
        executorWrapper.create()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun cleanup() {
        "ON_DESTROY ⇢ PlacesAPI cleanup ✅".log()
        executorWrapper.destroy()
        "🚿 cleanup() - complete!".log()
    }

Note that all the objects that are correctly scoped and created by Dagger 2 are neatly injected into the fields where they’re required! This is the magic of Dagger 2. By worrying about scopes, modules, and components, we get @Inject for “free”.

Approach 2 - Subcomponents

Using subcomponents is very similar to Approach 1 with the major difference being that it is no longer necessary to export all the objects created by the top level component to the components that depend on it. However, you still have to create the scoped component at the appropriate time and handle any cleanup when it’s scope terminates.

Here’s what the Application class looks like when using subcomponents.

class MyApplication : Application() {
    // ApplicationComponent (scoped to life of entire app).
    lateinit var applicationComponent: ApplicationComponent

    override fun onCreate() {
        super.onCreate()
        applicationComponent = DaggerApplicationComponent.builder()
                .applicationModule(ApplicationModule(this))
                .build()
    }

    // ActivityComponent (scoped to just the lifetime of an Activity).
    var activityComponent: ActivityComponent? = null

    fun createActivityComponent(): ActivityComponent? {
        activityComponent = applicationComponent.plus(ExecutorModule())
        return activityComponent
    }

    fun destroyActivityComponent() {
        activityComponent = null
    }
}

Here’s a snippet of the component.

@Singleton
@Component(modules = [ApplicationModule::class, GMSClientsModule::class])
interface ApplicationComponent {
    fun plus(executorModule: ExecutorModule): ActivityComponent
}

Here’s a snippet of the subcomponent.

@ActivityScope
@Subcomponent(modules = [ExecutorModule::class])
interface ActivityComponent {
    fun inject(placesAPI: PlacesAPI)
}

Here’s a snippet of a lifecycle aware component (PlacesAPI.kt) using the subcomponent.

class PlacesAPI(val app: Application) : AndroidViewModel(app), LifecycleObserver {

    @Inject
    lateinit var currentPlaceClient: PlaceDetectionClient
    @Inject
    lateinit var geoDataClient: GeoDataClient
    @Inject
    lateinit var fusedLocationProviderClient: FusedLocationProviderClient
    @Inject
    lateinit var executorWrapper: ExecutorWrapper

    // Lifecycle hooks.
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun connect() {
        // Dagger 2 component creation.
        with((app as MyApplication)) {
            createActivityComponent().inject(this@PlacesAPI)
        }
        executorWrapper.create()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun cleanup() {
        executorWrapper.destroy()
        with((app as MyApplication)) {
            destroyActivityComponent()
        }
    }
}

Binding the subcomponent to the component

Notice the changes in the MyApplication class.

  • There’s a createActivityComponent() method which actually calls the plus() method in the ApplicationComponent object.

  • Notes on the plus() method.

    • This method (plus()) can be called anything, but we are adding the module(s) that this component will be comprised of. If there are multiple modules that need to be passed, then they can be passed as arguments to the same method call. This stackoverflow discussion sheds light on this.

    • plus(ExecutorModule) acts as the declarative glue telling Dagger 2 how the @Subcomponent and @Component are related, since this relationships isn’t really defined anywhere else. And this happens in the ApplicationComponent interface where plus() is declared. So the component states its relationship w/ the subcomponent declaratively.

    • This is actually what tells Dagger 2 to use this newly created ExecutorModule object as a scoped object. Note that an ExecutorModule object had to be passed in order to get a reference to the ActivityComponent which is very different than what we saw in Approach ## 1, but the idea is similar (since PlacesAPI.kt is what calls this method in both Approach 1 and 2).

    • The MyApplication class allows the activityComponent field to be accessed by other classes. This is to allow the Activity (or Activity lifecycle observer, like PlacesAPI.kt) that creates this subcomponent, to be able to destroy it later. Also if this field activityComponent is null this means that this object hasn’t been created yet, and is currently out of scope, ie, its scope isn’t currently active. When its scope becomes active again (ie, an activityComponent object is created, then any @Inject statements based on this subcomponent, will work, because they will get the objects provided by this subcomponent’s modules).

    • This means anyone calling activityComponent.inject(???) will be able to get the required objects injected into them when marked w/ @Inject. These objects are provided by the modules.

    • All of this ends up being a fancy way of allowing you to control the lifecycle of a set of objects (provided by the subcomponent’s module(s)), when they are created and destroyed, and associate a label with it (which is your custom @Scope annotation).

Creating the (scoped) subcomponent (manually) and destroying it (manually)

The following is a complete listing of PlacesAPI.kt (only snippets have appeared above).

class PlacesAPI(val app: Application) : AndroidViewModel(app), LifecycleObserver {

    // Places API Clients.
    @Inject
    lateinit var currentPlaceClient: PlaceDetectionClient
    @Inject
    lateinit var geoDataClient: GeoDataClient

    // Fused Location Provider Client.
    @Inject
    lateinit var fusedLocationProviderClient: FusedLocationProviderClient

    // Background Executor.
    @Inject
    lateinit var executorWrapper: ExecutorWrapper

    // Lifecycle hooks.
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun connect() {
        "ON_CREATE ⇢ PlacesAPI.connect() ✅".log()

        // Dagger 2 component creation.
        with((app as MyApplication)) {
            createActivityComponent().inject(this@PlacesAPI)
        }

        "💥 connect() - got GetDataClient, PlaceDetectionClient, FusedLocationProviderClient".log()

        "ON_CREATE ⇢ Create Executor ✅".log()
   
        executorWrapper.create()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun cleanup() {
        "ON_DESTROY ⇢ PlacesAPI cleanup ✅".log()
        executorWrapper.destroy()
        "🚿 cleanup() - complete!".log()
        with((app as MyApplication)) {
            destroyActivityComponent()
        }
    }

}

Here are the class definitions for the component and subcomponent.

Component listing

@Singleton
@Component(modules = [ApplicationModule::class, GMSClientsModule::class])
interface ApplicationComponent {
    fun plus(executorModule: ExecutorModule): ActivityComponent
}

@Module
class ApplicationModule(private val application: Application) {
    @Singleton
    @Provides
    fun provideContext(): Context {
        return application
    }
}

@Module
class GMSClientsModule {
    @Singleton
    @Provides
    fun providesPlaceDetectionClient(context: Context): PlaceDetectionClient {
        return Places.getPlaceDetectionClient(context)
    }

    @Singleton
    @Provides
    fun providesGeoDataClient(context: Context): GeoDataClient {
        return Places.getGeoDataClient(context)
    }

    @Singleton
    @Provides
    fun providesLocation(context: Context): FusedLocationProviderClient {
        return LocationServices.getFusedLocationProviderClient(
                context)
    }
}

Subcomponent listing

@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope

@ActivityScope
@Subcomponent(modules = [ExecutorModule::class])
interface ActivityComponent {
    fun inject(placesAPI: PlacesAPI)
}

@Module
class ExecutorModule {
    @Provides
    @ActivityScope
    fun provideExecutor(): ExecutorWrapper {
        return ExecutorWrapper()
    }
}

class ExecutorWrapper {
    lateinit var executor: ExecutorService

    fun create() {
        executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
    }

    fun destroy() {
        executor.shutdown()
    }
}

Real example

To see an example of Dagger 2 applied to Kotlin and Android in the context of a real Android application, please checkout this repo

Related Posts