As an Android developer, you must have heard of Dagger. Moreover, you’ve probably faced some difficulties with setting it up in Kotlin projects. What are the benefits of this library? Do we still need those Dagger’s annotation processors and code generators? Being inspired by Antonio Leiva great article on how to use Dagger 2 on Android With Kotlin and Elye’s Dagger 2 for Dummies in Kotlin, as well as relying on my own experience, I’ll try to convince you that actually, we don’t need them at all sometimes. (Well, Dagger is a pretty powerful tool).
In this article, I’ll show you how basic Dagger features can be easily replaced.
Setup
The concept of skipping Dagger in Kotlin deserves its own name. Double “g” for the similarity with “Dagger” and “K” as in “Kotlin”. I called it Kagger, and surprisingly, in Danish kagger means cakes 🍰. It’s not a library, so assuming you’ve already defined Kotlin dependencies, you have nothing to configure at all. Our solution will be pure-kotlin and therefore a piece of cake 🙂
Components and modules
Just like in Dagger, we are using “components” and “modules” terms. The main difference is we split them to their interfaces and implementations. The following comparison should be self-explanatory.
Dagger
In Dagger you would probably define module like:
@Module class AxModule(private val application: Application) {
@Provides
@Singleton
fun provideSomeSingleton() = application
.getSystemService(Context.POWER_SERVICE) as PowerManager
@Provides
fun provideSomeBean() = application
.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
@Singleton
annotation is used to mark single-instance beans, so function provideSomeSingleton()
will be called only once. The second one, provideSomeBean()
is not annotated as a singleton and will be invoked every time it’s being injected. Let’s imagine that we have another few modules, such as AxModule
, BxModule
and CxModule
.
When it comes to a component, you have to define a list of contained modules and specify which classes will be able to use that component:
@Singleton
@Component(modules = arrayOf(AxModule::class, BxModule::class, CxModule::class))
interface MyComponent {
fun inject(dummyActivity: DummyActivity)
}
Then you can build your component:
class MyApplication : Application() {
val myComponent: MyComponent by lazy {
DaggerMyComponent
.builder()
.axModule(AxModule(this))
.bxModule(BxModule())
.cxModule(CxModule())
.build()
}
}
And finally you are able to inject your dependencies:
class DummyActivity : AppCompatActivity() {
@Inject lateinit var powerManager: PowerManager // singleton
@Inject lateinit var locationManager: LocationManager // non-singleton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dummy)
// there may be another ways to access myComponent
(application as MyApplication).myComponent.inject(this)
}
}
PowerManager
and LocationManager
will be accessible after OnCreate
call.
Note: In the newest version of Dagger, it’s not necessary to access components by casting application property. I used it to make the example clear.
Kagger / Pure Kotlin
How to achieve the same only with Kotlin?
Firstly, define module interface:
interface AxModule {
val someSingleton: PowerManager
fun someBean(): LocationManager
}
Please notice I’ve replaced singleton declaration with a field. That’s the way we emphasize the difference between singletons and non-singletons to avoid misleadings in the future. It’s up to you if you follow this pattern or not. When the definition is done, let’s make an module implementation:
class AxModuleImpl(private val application: Application) : AxModule {
override val someSingleton by lazy {
application.getSystemService(Context.POWER_SERVICE) as PowerManager
}
override fun someBean() = application
.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
What is more, for singleton field, lazy property was used. It’s worth to know it’s thread-safe and we can be sure that object will be instantiated only once.
Let’s move on to component interface:
interface MyComponent: AxModule, BxModule, CxModule
It may seem familiar to Dagger component declaration. Again, apart from its name, we are declaring a list of modules that component should contain. It’s not necessary to specify which classes will be able to “inject” those objects.
For similarity with above Dagger example, let’s implement a component as an object expression:
class MyApplication : Application() {
val myComponent: MyComponent by lazy {
object : MyComponent,
AxModule by AxModuleImpl(this),
BxModule by BxModuleImpl(),
CxModule by CxModuleImpl() {}
}
}
Here we are using the true power of Kotlin — delegation. MyComponent
interface extends AxModule
, BxModule
and CxModule
. Thanks to the implementation by delegation we are keeping those modules apart and we don’t have to implement all methods and fields in one place. Each module can contain its own dependencies, as AxModule
which uses application
context.
You may create separate class for component implementation as well:
class MyComponentImpl(application: Application): MyComponent,
AxModule by AxModuleImpl(application),
BxModule by BxModuleImpl(),
CxModule by CxModuleImpl()
So MyApplication
class simplifies to:
class MyApplication : Application() {
val myComponent: MyComponent by lazy { MyComponentImpl(this) }
}
If you do so, the access to items looks like this:
class DummyActivity : AppCompatActivity() {
private val component by lazy {
// there might be a better way of accessing myComponent
(application as MyApplication).myComponent
}
private val someSingleton by lazy { component.someSingleton }
private val someBean by lazy { component.someBean() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dummy)
// nothing to declare in onCreate
}
}
As you can see above, we are “injecting” items without annotation or lateinit keyword. Vals remains vals. In case you want to parameterize non-singleton objects, just add a parameter to a module function:
interface AxModule {
val someSingleton: PowerManager
fun someBeanWithParams(param: String): LocationManager
}
For testing purposes, you can create a mocked version of MyComponent
and replace only necessary modules, methods or fields.
To help you with better understanding of this matter, I’ve put a compilable project to GitHub repository.
Why does it matter?
With the mechanism described above, we were able to remove simple Dagger dependencies in projects created for Indoorway. What’s more, Kagger may be used outside of Android projects. I’m looking forward for your opinions and thoughts on this.
Featured in Android Weekly #307. Originally posted on Medium.