A Dagger to Remember

April 22, 2018

Story Time!

Kotlin and Java annotations have a complicated relationship. This goes from the syntax to the toolchain.

Does anybody remember that at the beginning of time Kotlin annotations were placed in square brackets? In 2015 the syntax was changed to the current form. Before that, it was [Inject] instead of @Inject. Yep.

The syntax is fine at this point, but kapt (Kotlin annotation processing tool) from the Kotlin toolchain can be… cruel.

kapt: denial

The project I’m working on started using Kotlin from the very beginning — in 2015. At the beginning of 2017, there was the following situation.

When you are working on a constantly growing codebase and your builds take up to 5 minutes, even if you’ve changed only a single line… It is a horrible experience and a plain bad development environment which leads to decreased productivity and to potential financial losses.

How do you solve this? There is a nice method — throw more hardware at it! That’s a short story about how Mainframer was born.

kapt: anger

Using Mainframer was fine for a while. Yes, the issue was solved using quantity over quality, but it was a good idea and it scaled well. If you don’t have enough expertise to change kapt, you can at least change the environment it is being run in. Besides, the Kotlin toolchain became better over time, so there was a pretty good chance to get an incremental build.

Unfortunately, kapt struck again in Kotlin 1.2.21. Increasing kapt processing time for unit tests by a magnitude of 3 was too much, especially if you are running tests more often than the project executable itself. I’ve asked myself a million dollar question.

Do you even need kapt?

Turns out there was only a single annotation-processing dependency in the project. I think you already know which one. Yep, it was Dagger. Down the rabbit hole we go.

Adventure Time!

Do You Even Need kapt Dagger?

I’m going to talk about the Google Dagger. It was forked from the Square Dagger. The Square one did some lookups at runtime but generated code as well using the annotation processing, just like the Google one. At the same time, the Google version doesn’t use reflection and generates everything beforehand. This is actually great. No reflection usage — better runtime performance and compile-time Context validation.

There might be a confusion among Android developers about the Context naming. I’m going to use it as a more broad term than a framework class name. The Context is a dependencies container (or just a container of sorts). You can observe this naming in different environments, such as Go and Spring. You can associate it with Google Dagger @Component or a Square Dagger ObjectGraph.

There is a downside though. The Square version had a small but extensive API. In my opinion, it covered almost everything you need from a dependency injection. The Google version has grown up big and sometimes not in a good way. The API includes Android support module (let’s just forget about DaggerActivity which… exists), multibindings, reusable dependencies, components and subcomponents, modules and producer modules…

The project I’m working on didn’t use anything magical. Hell, most likely Dagger was used wrong!

Basically, the DI glue is separated from the main codebase.

// Model

interface Repository {
    val content: Observable<RepositoryContent>

    class Impl : Repository {
        override val content = Observable.just(RepositoryContent())
    }
}

interface Service {
    val content: Observable<ServiceContent>

    class Impl(repository: Repository) : Service {
        override val content = repository.content.map { it.toServiceContent() }
    }
}

// Presentation

class ViewModel(context: Context) {

    @Inject lateinit var service: Service

    init {
        context.inject(this)
    }
}

// Dagger

@Module
class RepositoryModule {
    @Provides @Singleton
    fun provideRepository(): Repository = Repository.Impl()
}

@Module
class ServiceModule {
    @Provides @Singleton
    fun provideService(repository: Repository): Service = Service.Impl(repository)
}

interface Context {
    fun inject(vm: ViewModel)
}

@Singleton
@Component(modules = [RepositoryModule::class, ServiceModule::class])
interface ContextComponent : Context

I know, I know, that’s not how you do it, but it is just a use case I have on hand.

How do you test things using this structure? Well, it is pretty simple.

Decisions, Decisions

All of the above got me thinking.

Do you need complex tools to solve simple problems?

As you can see, the setup is pretty simple. Yes, potentially Dagger could give some benefits, but is it worth it increasing build time for every developer on the team dozens of times per day? And taking into an account the fact that this setup worked for years without any change at all?

Do you need to keep using tools designed for different conditions?

Let’s face it — the annotation processing is a nice idea but meant for special environments. It is too verbose to declare everything by hand using Java, so here we go, there is a code generator which does this for us. Is it a good fit if you are using Kotlin for the entire codebase? I have no idea, it is your codebase and your call. I did mine.

Back to the Roots

Having a library isn’t cool. You know what’s cool? Not having a library.

Let’s go crazy and use Kotlin to make our own inversion of control implementation. Not Koin, not Kodein, not Kapsule — just some patterns and language features.

I highly suggest reading a Martin Fowler article about inversion of control (IoC) containers. It contains almost everything you need to know about IoC, so I’m going to talk about practice only.

Modules

What is a module? It is a registry of dependencies. What properties a module has? Dependencies on other modules. Sounds simple enough.

interface RepositoryModule {
    val repository: Repository

    class Impl : RepositoryModule {
        override val repository by lazy { Repository.Impl() }
    }
}

interface ServiceModule {
    val service: Service

    class Impl(repositoryModule: RepositoryModule) {
        override val service by lazy { Service.Impl(repositoryModule.repository) }
    }
}

Notice the lazy delegate. It makes our properties lazy singletons, just like Dagger would do it for you! In other words, creating a module would not create all dependencies in it at once, but will do it only on the first access.

Context

Talking about Context… It is just a composition of modules, right?

interface Context :
    RepositoryModule,
    ServiceModule {

    class Impl(
        repositoryModule: RepositoryModule,
        serviceModule: ServiceModule
    ) : Context,
        RepositoryModule by repositoryModule,
        ServiceModule by serviceModule
}

We are using Kotlin delegation here. The Context will be translated to something like this to the end user.

interface Context {
    val repository: Repository
    val service: Service
}

You have to create it by hand though, creating all modules first. Dagger would’ve done it for you, but it is no biggie.

fun createContext(): Context {
    val repositoryModule = RepositoryModule.Impl()
    val serviceModule = ServiceModule.Impl(repositoryModule)

    return Context.Impl(repositoryModule, serviceModule)
}

Yep, it is a bit verbose, but you are in a total control because it is your code.

Tricks

You can define non-lazy dependencies which will be created at the same time as a module.

interface RepositoryModule {
    val repository: Repository

    class Impl : Module {
        override val repository = Repository.Impl()
    }
}

You can define non-singleton dependencies.

interface RepositoryModule {
    val repository: Repository

    class Impl : Module {
        override val repository: Repository
            get() = Repository.Impl()
    }
}

You can define scopes using the same delegation approach as with the Context.

interface UserContext :
    Context,
    UserModule {

    class Impl(
        context: Context,
        userModule: UserModule
    ) : UserContext,
        Context by context,
        UserModule by userModule
}

interface Context {
    fun plus(userModule: UserModule): UserContext

    class Impl : Context {
        override fun plus(userModule: UserModule) = UserContext.Impl(this, userModule)
    }
}

You can define multiple dependencies with the same interface.

interface ServiceModule {
    val yinService: Service
    val yangService: Service
}

You can move from lateinit var to private val.

class ViewModel(context: Context) {
    private val service = context.service
}

Results

Seems like it is possible to do the inversion of control without Dagger, who knew?

Pros

Cons

Jokes aside though — it works in real life.

Exploration Time!

Since I’m trying to advocate a more broad-minded approach to the development process, let’s take a look at other languages and how people try to achieve the inversion of control in their code without using frameworks for that purpose.

Scala

The research I’ve made for framework-less IoC brought me to frequent mentions of the Cake and Thin Cake patterns originated from Scala. I highly suggest reading the explanation article since it covers everything you need to know about the Cake pattern. There is also a great presentation on the topic comparing Cake, Thin Cake and a couple of other approaches, including Guice.

I don’t think it is possible to do the exact Thin Cake translation to Kotlin, but here is an attempt to do so anyway.

interface RepositoryModule {
    fun repository() = Repository.Impl()
}

interface ServiceModule : RepositoryModule {
    fun service() = Service.Impl(repository())
}

interface Context : RepositoryModule, ServiceModule

The code is not identical to the original approach though since we don’t have traits and self-types in Kotlin. I think the closest true match is actually using the delegation, just like I’ve described above. So I guess the Kotlin approach is a rough adaptation of the Thin Cake pattern!

Swift

Passing Context to ViewModel in the described approach is actually messy.

class ViewModel(context: Context) {
    private val service = context.service
}

The Context is a dependency container and passing it basically means a complete access to all dependencies, whether you like it or not.

Swift has a nice method to isolate the scope of the Context via declaring child Context using protocol composition. This approach is described here and there.

A rough Kotlin translation seems to be something like this.

class ViewModel(context: ViewModel.Context) {
    interface Context : ServiceModule
}

fun create() {
    ViewModel(createGlobalContext()) // Error: type mismatch.
}

Unfortunately for this to work all child Context variations should be declared at the top-level Context.

interface Context : RepositoryModule, ServiceModule, ViewModel.Context

fun create() {
    ViewModel(createGlobalContext()) // It works!
}

A more sane approach actually can be just passing all dependencies in ViewModel constructors directly, without a Context instance. Plus, this requires declaring a module per dependency to be able to construct Context from singular dependencies instead of their enumerations. But, at the same time, it would be nice to have protocol composition for such cases as an alternative.

Fin

Developers tend to search for a silver bullet all the time. Do you parse JSON? You absolutely have to use the most performant parser available on this planet, otherwise… Have you played Doom? Well, it goes exactly this way. Oh, you parse it only once and it is an object with two fields? You need the most performant parser, remember!

Don’t let a tool to become a MacGuffin — pick it based on your needs and do not adapt your needs to a tool. We develop things to solve issues, not to create them.


PS Bonus points to everyone who got the Futurama reference 😉


Thanks to Artem Zinnatullin and Danny Preussler for the review!