How to reproduce Dagger functions in Kotlin?

Marcin Jeleński

Marcin Jeleński

white icing coated cake

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. MyComponentinterface extends AxModuleBxModule 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 applicationcontext.

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.