Advanced Dagger 2 w/ Android and Kotlin
- Gradle configuration changes
- Custom scopes
- Creating and using a custom scope
- Real example
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).
So, in build.gradle
:
kapt
has to be added as a pluginannotationProcessor
has to be replaced bykapt
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 #
-
-
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”!
-
-
- 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.
- This article is a deep dive into the code generated by
-
- This article provides a complex example of multiple scopes and subcomponents.
-
- 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.
- The Application class needs to create the
ApplicationComponent
- 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 theplus()
method in theApplicationComponent
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 theApplicationComponent
interface whereplus()
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 anExecutorModule
object had to be passed in order to get a reference to theActivityComponent
which is very different than what we saw in Approach ## 1, but the idea is similar (sincePlacesAPI.kt
is what calls this method in both Approach 1 and 2). -
The
MyApplication
class allows theactivityComponent
field to be accessed by other classes. This is to allow the Activity (or Activity lifecycle observer, likePlacesAPI.kt
) that creates this subcomponent, to be able to destroy it later. Also if this fieldactivityComponent
isnull
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, anactivityComponent
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 check out this repo
👀 Watch Rust 🦀 live coding videos on our YouTube Channel.
📦 Install our useful Rust command line apps usingcargo install r3bl-cmdr
(they are from the r3bl-open-core project):
- 🐱
giti
: run interactive git commands with confidence in your terminal- 🦜
edi
: edit Markdown with style in your terminalgiti in action
edi in action