Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: October 10, 2024
Switching from Dagger to Koin can simplify dependency injection (DI) for Kotlin developers by offering a more Kotlin-centric and lightweight approach. While Dagger is a powerful and robust DI framework commonly used in large-scale applications, Koin provides simplicity and ease of use, potentially making it a better fit for certain projects.
In this tutorial, we’ll explore the key differences between Dagger and Koin, then walk through the migration process, and finally, demonstrate how to replace Dagger components with Koin equivalents.
First, let’s cover the essential features of both Dagger and Koin. This will help us grasp the advantages and trade-offs when migrating between the two frameworks.
Dagger is a compile-time dependency injection framework that provides strong guarantees about dependency resolution. It uses annotations such as @Module, @Inject, and @Component to configure dependencies and generate code.
By resolving dependencies at compile time, Dagger offers type safety and potentially better runtime performance since it avoids the overhead of resolving dependencies at runtime. However, this approach can increase complexity due to the need for code generation and extensive configuration.
Koin is a runtime dependency injection framework designed specifically for Kotlin. It simplifies DI by leveraging Kotlin’s language features and avoids the need for code generation. This approach reduces the amount of boilerplate code required, potentially making it more straightforward to set up compared to other DI frameworks.
Before migrating, it’s important to understand the primary differences between the two frameworks. Let’s outline these key differences to help us make an informed decision:
Before diving into the code examples, let’s start by configuring the build system to use Dagger. Since Dagger relies on annotation processing to generate code at compile time, we need to add the necessary dependencies and annotation processors to the project’s build.gradle.kts file:
dependencies {
implementation("com.google.dagger:dagger:2.52")
kapt("com.google.dagger:dagger-compiler:2.52")
}
Let’s also ensure that the kapt (Kotlin Annotation Processing Tool) plugin is applied at the top of our build.gradle.kts:
plugins {
kotlin("kapt")
}
In Dagger, we define modules and components to manage our dependencies.
First, we need to define a repository class that we’ll inject:
class MyRepository {
fun getData(): String {
return "Repository data"
}
}
Let’s also create a service class that depends on the repository:
class MyService @Inject constructor(private val myRepository: MyRepository) {
fun performAction(): String {
return "Service is using: " + myRepository.getData()
}
}
We need to declare our classes in a module to make them available for injection:
@Module
class MyModule {
@Provides
fun provideMyRepository(): MyRepository {
return MyRepository()
}
@Provides
fun provideMyService(myRepository: MyRepository): MyService {
return MyService(myRepository)
}
}
Then, we define the component:
@Component(modules = [MyModule::class])
interface AppComponent {
fun inject(app: MyApplication)
}
In our application code, we use Dagger to inject dependencies:
class MyApplication {
@Inject
lateinit var myService: MyService
init {
DaggerAppComponent.create().inject(this)
}
fun run() {
val result = myService.performAction()
println(result)
}
}
In this example, Dagger is used to inject an instance of MyService into MyApplication. Meanwhile, MyService depends on MyRepository, which is provided by MyModule. This setup involves creating modules and components and using annotations like @Inject and @Provides.
Now that we’ve seen how Dagger is used in an application, let’s set up Koin to achieve the same functionality. We’ll need to add the correct dependencies to our build.gradle.kts file based on the modules required for our application.
To ensure that all Koin artifacts use compatible versions, we’ll include the Koin Bill of Materials (BOM):
implementation(platform("io.insert-koin:koin-bom:4.0.0"))
Next, let’s add the Koin core dependency:
implementation("io.insert-koin:koin-core")
If the project uses additional Koin extensions, we can include them as needed.
For testing Koin, we need to include the koin-test dependency. Furthermore, to use it with JUnit 5, let’s include koin-test-junit5:
testImplementation("io.insert-koin:koin-test")
testImplementation("io.insert-koin:koin-test-junit5")
Koin uses modules to declare how dependencies should be provided. Let’s define a Koin module equivalent to the Dagger module we had earlier:
val appModule = module {
single { MyRepository() }
factory { MyService(get()) }
}
This defines two types of dependencies: single() creates a singleton instance of MyRepository, and factory() creates a new instance of MyService each time it’s needed, injecting MyRepository into it via the get() function.
Now that we’ve set up Koin, let’s migrate Dagger’s components and annotations to Koin’s approach.
To migrate to Koin, we start by removing Dagger-specific annotations like @Inject from our classes. Koin handles injection using delegation functions, such as by inject(), eliminating the need for annotations and code generation. This simplifies our code and aligns it more closely with standard Kotlin practices.
To use Koin, we need to initialize it within our application. This is typically done in the main() function, where we define the modules to be used:
fun main() {
startKoin {
modules(appModule)
}
val myApplication = MyApplication()
myApplication.run()
}
In this setup, we initialize Koin in the main() function, and then, we create an instance of MyApplication, which subsequently uses Koin to inject MyService.
In the MyApplication class, we inject dependencies using Koin’s by inject():
class MyApplication : KoinComponent {
private val myService: MyService by inject()
fun run() {
val result = myService.performAction()
println(result)
}
}
Consequently, this eliminates the need for Dagger’s @Inject annotation and the component injection call.
Koin provides an easy way to inject mocks and also test components. Ultimately, this enables us to unit test our dependency tree without requiring a full application context. Let’s create a test using Koin’s test features with JUnit 5:
class MyServiceTest : KoinTest {
private val myService: MyService by inject()
private val testModule = module {
single { MyRepository() }
factory { MyService(get()) }
}
@BeforeEach
fun setUp() {
startKoin {
modules(testModule)
}
}
@AfterEach
fun tearDown() {
stopKoin()
}
@Test
fun `test my service returns expected value`() {
val result = myService.performAction()
assertEquals("Service is using: Repository data", result)
}
}
In the test code, we define a Koin test module using the module() function to provide instances of MyRepository and MyService.
The test is set up by calling startKoin() to initialize Koin with this module before each test, allowing dependencies to be injected using the inject() function. Furthermore, we call stopKoin() to clean up after each test and avoid conflicts between tests.
Migrating from Dagger to Koin can simplify the development process by reducing boilerplate and offering a more Kotlin-friendly approach to dependency injection. While Dagger provides compile-time safety and may be necessary for larger projects, Koin offers a simpler experience that can be easier to integrate.
The steps in this tutorial will help us smoothly transition to Koin and take advantage of its lightweight runtime dependency injection capabilities.