Our experience migrating from Dagger to Koin

Share This Post

Note: this was assembled with Koin version 2.0.1, more recent versions have changed some things. Refer to the official documentation for more information: https://insert-koin.io/

 

Migrating From Dagger To Koin

Context

We have a legacy project, started by a team from another company, with other standards, practices, experiences and so on. This project was initially set up with Dagger as a dependency injection mechanism and is not modularised. As the project grew, so did the compilation times. When it got to the point where compiling the project could take more than 10 minutes we decided to see what we could do about it.

Modularisation, a possible solution?

We first considered modularising the project, so that only the modified modules would have to be recompiled instead of the whole project. This would not solve the initial compilation time but the incremental builds would be much faster.

But given the length of time the project had been under development without following good guidelines to reduce coupling, trying to get modules out was tremendously complicated.

Being able to modularise the project required a refactor at a very deep level, decoupling essential parts of the application from each other. And we had to do all this while still delivering new functionality to the customer.

Dagger and annotation processing

Thanks to Android Studio’s build analysis tool, we were able to see that approximately 40% to 50% of the time in each build was taken up by the annotation processor. And practically all of that time was taken up by Dagger.

We had already worked on other projects using Koin and given that the project code was already more than 90% Kotlin, we thought it was a good idea to migrate from one library to the other to see what would happen. In the worst case scenario, we would end up with a dependency injection library that we already knew and were comfortable with.

  A simple implementation of Remote Configuration for SwiftUI

Initial Configuration

We started the migration bit by bit. The first step was to include the library in the project and configure it.

private fun initKoin() {
    startKoin {
      androidContext(this@MyApp)
      modules(koinModules)
    }
  }

Initially, the list of koinModules includes the instances of the most basic stateless common elements:

val koinModules = listOf(
  commonModule,
  networkModule,
  databaseModule
)

These modules include things like the ApiClient, the Room database, a label manager, or the analytics manager. Things that any project feature might need to a greater or lesser extent.

The next step was to add the test to make sure the module definitions are correct. The main drawback of moving from Dagger to Koin is that Dagger warns on every build if we have done something wrong, on the other hand, Koin will fail only at runtime, so it is especially important to have a way to ensure the correctness of our modules. Luckily the way to test this in Koin is quite easy and we have a CI that runs all the tests before letting us release a version (either test or production).

class KoinModulesTest : KoinTest {

  @get:Rule
  @ExperimentalCoroutinesApi
  val coroutineRule = CoroutineMainDispatcherRule()

  @get:Rule
  val rule: TestRule = InstantTaskExecutorRule()

  @get:Rule
  val mockProvider = MockProviderRule.create { clazz ->
    mockkClass(clazz, relaxed = true)
  }


  @Test
  fun testKoinDependencies() {
    startKoin {
      androidContext(mockk(relaxed = true))
      modules(koinModules)
    }.checkModules {
      //Here we can define the parameters for the ViewModels
      create<FeatureViewModel> { parametersOf(mockk<Foo>(), mockk<Bar>()) }
      //We can also declare mocks
      declareMock<MyService>()
     }
    }
  }
}

With this single test, we can test the entire dependency tree. The only thing that requires some attention is the dependencies that require external parameters to the tree itself, for example, a parameter that we pass from a fragment to its viewmodel. We may also need to declare other mocks, especially if there is a dependency that executes code at build time, for example:

val data = liveData {
    myService.getData(request)?.let { emit(it) }
  }

If we don’t mock the dependency the test will end up giving problems trying to call the real service.

The two rules that head the test class are to avoid problems with the coroutines, as in the previous example, if the getData method is suspendable, the test may end up failing even if the dependencies are well set up.
The third one is to define to koin which mocking framework to use, in our case we use Mockk, but you could use mockito or any other framework you want.

  What is Plop - File Generator Tool System

Gradual migration

We take advantage of new developments to use Koin for new features. It is easier to create the modules and dependencies in parallel, this can induce performance problems when we have for example the same ApiService instantiated twice, but except for the ViewModels, the rest of the classes are stateless so it doesn’t affect the performance of the project. And as new features require new ViewModels we don’t have the problem of having the same ViewModel injected in two different ways.

Each new feature will have a new module and this is added to the list of modules defined at the beginning. For example, let’s imagine we have a new feature whose ViewModel receives a Foo and a Bar from the fragment, and needs a FeatureService. The module would look like this:

val featureModule = module {
  single {
    FeatureService(get(), get())
  }
  //single<FeatureService>() if we can use reflection

  viewModel { (foo: Foo, bar: Bar) ->
    FeatureViewModule(foo, bar, get())
  }
}

By using an experimental koin feature we can save having to define the service parameters. This feature uses reflection so it may not be usable in all cases, but in our case, the impact on performance was not noticeable and we decided to keep it.

For existing features the migration is similar. We define a koin module equivalent to the one already defined in dagger, add it to the module list, and change the fragment injection from:

 @Inject
  lateinit var viewModel: LegacyViewModel

to

  private val viewModel: LegacyViewModel by viewModel { parametersOf(Foo()) }

Another advantage that koin offers us in this case is not having to declare the injected elements as lateinit var making them clearer and safer, using the by viewModel delegate the viewmodel will be instantiated in a lazy way, that is, only in case it is needed.

  Using console in JS for better testing

Once a module is migrated, we can remove the @Inject constructor from the injected classes so that our production code doesn’t need to know anything about how parameters are passed. Only the koin module definitions and our Android classes (Fragments, Activities) know anything about how dependencies are injected.

We can also stop inheriting from  DaggerFragment and DaggerAppCompatActivity since koin works by extensions, we don’t need to modify the parents of our classes.

Android Project CTA

Final Result

This migration took us some time, we started incrementally and we took advantage of a moment of a few features to finish the migration definitively. Once the migration was finished we were left with a more idiomatic code, without lateinit var and @Inject all over the place and just as robust, as the CI ran the test and warned us of any errors.

The number of lines of code in the whole project decreased by about 3000 lines of code (about 5% of the total). With 3MB less code generated, now the only code generated is from Room, BuildConfig, and Navigation Component.

Most importantly, the build time was more than halved: we went from builds of more than 10 minutes to builds of less than 5 minutes.

We have no regrets at all about the effort involved in this migration, at the time it was a significant time investment but the time we have saved day by day has been more than worth it.

Author

  • Eric Martori

    Software engineer with over 6 years of experience working as an Android Developer. Passionate about clean architecture, clean code, and design patterns, pragmatism is used to find the best technical solution to deliver the best mobile app with maintainable, secure and valuable code and architecture. Able to lead small android teams and helps as Android Tech Lead.

    View all posts

One Comment

  1. Ahmad Reza

    I just have one question. Does the CI pipeline that ensures the correct implementation of the DI, as trustworthy as the compile-time dependency injection?
    Did you write your tests in a specific manner that test injections too, or no they were regular tests?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Subscribe To Our Newsletter

Get updates from our latest tech findings

Have a challenging project?

We Can Work On It Together

apiumhub software development projects barcelona
Secured By miniOrange