Dependency Injection with Koin

Advanced April 2025 Mohsen Mashkour
Dependency Injection with Koin

Introduction

Koin is a lightweight dependency injection library for Kotlin that uses pure Kotlin DSL and functional programming principles. Unlike Hilt, which is built on top of Dagger and uses annotation processing, Koin is a pure Kotlin solution that provides a simple and pragmatic approach to dependency injection.

This comprehensive guide will take you from basic Koin setup to advanced features like custom scopes, property injection, and multi-module architecture. Whether you're looking for an alternative to Hilt or want to explore pure Kotlin DI solutions, Koin provides an excellent option with its simplicity and powerful features.

Why Choose Koin?

Before diving into implementation, let's understand the benefits of using Koin:

  • Pure Kotlin: No annotation processing or code generation
  • Lightweight: Small runtime footprint with minimal overhead
  • Simple DSL: Intuitive Kotlin DSL for dependency definitions
  • No Reflection: Compile-time verification without reflection
  • Easy Testing: Simple mocking and testing capabilities
  • Android Integration: Built-in support for Android lifecycle
  • Multiplatform Support: Works with Kotlin Multiplatform
  • Learning Curve: Easier to learn compared to Dagger/Hilt

Setting Up Koin

Let's start by setting up Koin in your Android project.

Dependencies
// app/build.gradle.kts
dependencies {
    // Koin Core
    implementation("io.insert-koin:koin-core:3.5.0")
    implementation("io.insert-koin:koin-android:3.5.0")
    
    // Koin for Android
    implementation("io.insert-koin:koin-androidx-compose:3.5.0")
    implementation("io.insert-koin:koin-androidx-compose-navigation:3.5.0")
    
    // Koin for ViewModel
    implementation("io.insert-koin:koin-androidx-viewmodel:3.5.0")
    
    // Koin for testing
    testImplementation("io.insert-koin:koin-test:3.5.0")
    testImplementation("io.insert-koin:koin-test-junit4:3.5.0")
}
Application Class
class MyApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
        
        // Start Koin
        startKoin {
            androidContext(this@MyApplication)
            modules(appModule)
        }
    }
}

// Define your modules
val appModule = module {
    // Module definitions will go here
}
AndroidManifest.xml

    
    
        
        
        
    

Basic Dependency Injection

Let's start with basic dependency injection concepts and implementations.

Simple Dependencies
// Data class for user
data class User(
    val id: Int,
    val name: String,
    val email: String
)

// Service interface
interface UserService {
    suspend fun getUsers(): List
    suspend fun getUserById(id: Int): User?
}

// Service implementation
class UserServiceImpl : UserService {
    override suspend fun getUsers(): List {
        // Implementation
        return listOf(
            User(1, "John Doe", "john@example.com"),
            User(2, "Jane Smith", "jane@example.com")
        )
    }
    
    override suspend fun getUserById(id: Int): User? {
        // Implementation
        return User(id, "User $id", "user$id@example.com")
    }
}

// Koin module
val appModule = module {
    
    // Single instance (Singleton)
    single { UserServiceImpl() }
    
    // Factory (new instance each time)
    factory { UserRepository(get()) }
    
    // Scoped instance
    scope {
        scoped { MainViewModel(get()) }
    }
}

// Repository class
class UserRepository(private val userService: UserService) {
    suspend fun getUsers(): List = userService.getUsers()
    suspend fun getUserById(id: Int): User? = userService.getUserById(id)
}
Activity Injection
class MainActivity : AppCompatActivity() {
    
    // Inject dependencies
    private val userRepository: UserRepository by inject()
    private val viewModel: MainViewModel by viewModel()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Dependencies are automatically injected
        loadUsers()
    }
    
    private fun loadUsers() {
        lifecycleScope.launch {
            val users = userRepository.getUsers()
            // Update UI with users
        }
    }
}

// ViewModel with Koin
class MainViewModel(
    private val userRepository: UserRepository
) : ViewModel() {
    
    private val _users = MutableLiveData>()
    val users: LiveData> = _users
    
    init {
        loadUsers()
    }
    
    private fun loadUsers() {
        viewModelScope.launch {
            try {
                val userList = userRepository.getUsers()
                _users.value = userList
            } catch (e: Exception) {
                // Handle error
            }
        }
    }
}

Network Dependencies

Setting up network dependencies with Retrofit and OkHttp.

Network Module
val networkModule = module {
    
    // OkHttp Client
    single {
        OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = if (BuildConfig.DEBUG) {
                    HttpLoggingInterceptor.Level.BODY
                } else {
                    HttpLoggingInterceptor.Level.NONE
                }
            })
            .addInterceptor(AuthInterceptor())
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .build()
    }
    
    // Retrofit
    single {
        Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(get())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    // API Service
    single {
        get().create(ApiService::class.java)
    }
    
    // Repository
    single {
        UserRepositoryImpl(get(), get())
    }
}

// API Service interface
interface ApiService {
    @GET("users")
    suspend fun getUsers(): List
    
    @GET("users/{id}")
    suspend fun getUserById(@Path("id") id: Int): User
    
    @POST("users")
    suspend fun createUser(@Body user: CreateUserRequest): User
}

// Repository implementation
class UserRepositoryImpl(
    private val apiService: ApiService,
    private val userDao: UserDao
) : UserRepository {
    
    override suspend fun getUsers(): List {
        return try {
            val users = apiService.getUsers()
            userDao.insertUsers(users)
            users
        } catch (e: Exception) {
            userDao.getAllUsers()
        }
    }
    
    override suspend fun getUserById(id: Int): User? {
        return try {
            val user = apiService.getUserById(id)
            userDao.insertUser(user)
            user
        } catch (e: Exception) {
            userDao.getUserById(id)
        }
    }
}

Scopes and Lifecycles

Understanding Koin scopes and how they manage object lifecycles.

Koin Scopes
val appModule = module {
    
    // Singleton scope (default)
    single { UserServiceImpl() }
    
    // Factory scope (new instance each time)
    factory { UserRepositoryImpl(get(), get()) }
    
    // Scoped to activity
    scope {
        scoped { MainViewModel(get()) }
        scoped { ActivityScopedServiceImpl() }
    }
    
    // Scoped to fragment
    scope {
        scoped { UserFragmentViewModel(get()) }
        scoped { FragmentScopedServiceImpl() }
    }
    
    // Custom scope
    scope(named("user_scope")) {
        scoped { UserManagerImpl(get()) }
        scoped { UserPreferencesImpl(get()) }
    }
}

// Using scoped dependencies
class MainActivity : AppCompatActivity() {
    
    private val viewModel: MainViewModel by currentScope.inject()
    private val activityService: ActivityScopedService by currentScope.inject()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Dependencies are scoped to this activity
    }
}

class UserFragment : Fragment() {
    
    private val viewModel: UserFragmentViewModel by currentScope.inject()
    private val fragmentService: FragmentScopedService by currentScope.inject()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // Dependencies are scoped to this fragment
    }
}
Custom Scopes
// Define custom scope
val userScope = module {
    scope(named("user_scope")) {
        scoped { UserManagerImpl(get()) }
        scoped { UserPreferencesImpl(get()) }
        scoped { UserRepositoryImpl(get(), get()) }
    }
}

// Using custom scope
class UserActivity : AppCompatActivity() {
    
    private val userScope = getKoin().createScope("user_123", named("user_scope"))
    
    private val userManager: UserManager by userScope.inject()
    private val userPreferences: UserPreferences by userScope.inject()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        
        // Use scoped dependencies
    }
    
    override fun onDestroy() {
        super.onDestroy()
        userScope.close()
    }
}

Property Injection

Koin provides flexible property injection options.

Property Injection
val appModule = module {
    
    // Inject properties
    single { 
        val apiKey = getProperty("api_key")
        val baseUrl = getProperty("base_url")
        
        Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(get())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    // Inject with default values
    single {
        val timeout = getProperty("timeout", 30000L)
        // Use timeout in configuration
        get().create(ApiService::class.java)
    }
}

// Set properties when starting Koin
startKoin {
    androidContext(this@MyApplication)
    modules(appModule)
    
    // Set properties
    properties(mapOf(
        "api_key" to "your_api_key_here",
        "base_url" to "https://api.example.com/",
        "timeout" to 30000L
    ))
}

// Or set properties from file
startKoin {
    androidContext(this@MyApplication)
    modules(appModule)
    
    // Load properties from file
    fileProperties("koin.properties")
}
Environment-based Configuration
val appModule = module {
    
    // Environment-based configuration
    single {
        val environment = getProperty("environment", "development")
        val baseUrl = when (environment) {
            "production" -> "https://api.production.com/"
            "staging" -> "https://api.staging.com/"
            else -> "https://api.development.com/"
        }
        
        Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(get())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
    
    // Feature flags
    single {
        val features = getProperty>("features", emptyMap())
        FeatureManagerImpl(features)
    }
}

Multi-Module Architecture

Organizing Koin modules in a multi-module Android project.

Module Structure
app/
├── build.gradle.kts
├── src/main/java/com/example/app/
│   ├── MainActivity.kt
│   ├── MyApplication.kt
│   └── di/
│       └── AppModule.kt
│
data/
├── build.gradle.kts
├── src/main/java/com/example/data/
│   ├── repository/
│   ├── local/
│   ├── remote/
│   └── di/
│       └── DataModule.kt
│
domain/
├── build.gradle.kts
├── src/main/java/com/example/domain/
│   ├── repository/
│   ├── usecase/
│   └── di/
│       └── DomainModule.kt
│
ui/
├── build.gradle.kts
├── src/main/java/com/example/ui/
│   ├── main/
│   ├── detail/
│   └── di/
│       └── UiModule.kt
Module Implementation
// Domain Module
val domainModule = module {
    
    // Use cases
    factory { GetUsersUseCase(get()) }
    factory { GetUserByIdUseCase(get()) }
    factory { CreateUserUseCase(get()) }
}

// Data Module
val dataModule = module {
    
    // Repositories
    single { UserRepositoryImpl(get(), get()) }
    
    // API Services
    single { 
        get().create(ApiService::class.java) 
    }
    
    // Database
    single {
        Room.databaseBuilder(
            get(),
            AppDatabase::class.java,
            "app_database"
        ).build()
    }
    
    single { get().userDao() }
}

// UI Module
val uiModule = module {
    
    // ViewModels
    scope {
        scoped { MainViewModel(get()) }
    }
    
    scope {
        scoped { UserDetailViewModel(get()) }
    }
}

// App Module
val appModule = module {
    includes(domainModule, dataModule, uiModule)
    
    // App-specific dependencies
    single {
        get().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
    }
    
    single { TokenManagerImpl(get()) }
}

// Start Koin with all modules
startKoin {
    androidContext(this@MyApplication)
    modules(appModule)
}

Testing with Koin

Testing applications that use Koin dependency injection.

Unit Testing
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
    
    @get:Rule
    val koinTestRule = KoinTestRule.create {
        modules(testModule)
    }
    
    @Test
    fun testUserListDisplayed() {
        // Test implementation
    }
}

// Test module
val testModule = module {
    
    // Mock implementations
    single { MockUserRepository() }
    single { MockApiService() }
    single { MockUserDao() }
}

// Mock implementations
class MockUserRepository : UserRepository {
    override suspend fun getUsers(): List {
        return listOf(
            User(1, "Test User", "test@example.com"),
            User(2, "Test User 2", "test2@example.com")
        )
    }
    
    override suspend fun getUserById(id: Int): User? {
        return User(id, "Test User $id", "test$id@example.com")
    }
}

class MockApiService : ApiService {
    override suspend fun getUsers(): List {
        return listOf(
            User(1, "API User", "api@example.com")
        )
    }
    
    override suspend fun getUserById(id: Int): User {
        return User(id, "API User $id", "api$id@example.com")
    }
    
    override suspend fun createUser(user: CreateUserRequest): User {
        return User(999, user.name, user.email)
    }
}
Repository Testing
@RunWith(MockitoJUnitRunner::class)
class UserRepositoryTest {
    
    @Mock
    private lateinit var apiService: ApiService
    
    @Mock
    private lateinit var userDao: UserDao
    
    private lateinit var userRepository: UserRepository
    
    @Before
    fun setup() {
        userRepository = UserRepositoryImpl(apiService, userDao)
    }
    
    @Test
    fun `getUsers returns users from API when successful`() = runTest {
        // Given
        val users = listOf(
            User(1, "John Doe", "john@example.com"),
            User(2, "Jane Smith", "jane@example.com")
        )
        whenever(apiService.getUsers()).thenReturn(users)
        
        // When
        val result = userRepository.getUsers()
        
        // Then
        assertThat(result).isEqualTo(users)
        verify(userDao).insertUsers(users)
    }
    
    @Test
    fun `getUsers returns cached users when API fails`() = runTest {
        // Given
        val cachedUsers = listOf(
            User(1, "Cached User", "cached@example.com")
        )
        whenever(apiService.getUsers()).thenThrow(IOException("Network error"))
        whenever(userDao.getAllUsers()).thenReturn(cachedUsers)
        
        // When
        val result = userRepository.getUsers()
        
        // Then
        assertThat(result).isEqualTo(cachedUsers)
    }
}

Advanced Features

Advanced Koin features for complex dependency injection scenarios.

Qualifiers
val networkModule = module {
    
    // Named dependencies
    single(named("api_retrofit")) {
        Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(get())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    single(named("cache_retrofit")) {
        Retrofit.Builder()
            .baseUrl("https://cache.example.com/")
            .client(get())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    single(named("api_service")) {
        get(named("api_retrofit")).create(ApiService::class.java)
    }
    
    single(named("cache_service")) {
        get(named("cache_retrofit")).create(CacheService::class.java)
    }
}

// Using named dependencies
class MainActivity : AppCompatActivity() {
    
    private val apiService: ApiService by inject(named("api_service"))
    private val cacheService: CacheService by inject(named("cache_service"))
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Use different services
    }
}
Factory with Parameters
val appModule = module {
    
    // Factory with parameters
    factory { (userId: String) ->
        UserManager(userId, get(), get())
    }
    
    // Factory with multiple parameters
    factory { (baseUrl: String, apiKey: String) ->
        ApiService(baseUrl, apiKey, get())
    }
    
    // Factory with default parameters
    factory { (timeout: Long = 30000L) ->
        NetworkManager(timeout, get())
    }
}

// Using factory with parameters
class UserActivity : AppCompatActivity() {
    
    private val userManager: UserManager by inject { parametersOf("user_123") }
    private val apiService: ApiService by inject { 
        parametersOf("https://api.example.com/", "api_key_123") 
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        
        // Use injected dependencies with parameters
    }
}
Lazy Injection
class MainActivity : AppCompatActivity() {
    
    // Lazy injection
    private val userRepository by lazy { get() }
    private val apiService by lazy { get() }
    
    // Lazy injection with scope
    private val scopedService by lazy { 
        getKoin().getOrCreateScope("main_scope").get() 
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Dependencies are created when first accessed
    }
    
    private fun loadData() {
        lifecycleScope.launch {
            val users = userRepository.getUsers() // Repository created here
            // Use users
        }
    }
}

Koin with Compose

Integrating Koin with Jetpack Compose.

Compose Integration
val composeModule = module {
    
    // ViewModels for Compose
    factory { MainViewModel(get()) }
    factory { UserListViewModel(get()) }
    factory { UserDetailViewModel(get()) }
}

// Compose function with Koin
@Composable
fun MainScreen() {
    val viewModel: MainViewModel = koinViewModel()
    val users by viewModel.users.collectAsState()
    
    LazyColumn {
        items(users) { user ->
            UserItem(user = user)
        }
    }
}

@Composable
fun UserDetailScreen(userId: Int) {
    val viewModel: UserDetailViewModel = koinViewModel { 
        parametersOf(userId) 
    }
    val user by viewModel.user.collectAsState()
    
    user?.let { userData ->
        UserDetailContent(user = userData)
    }
}

// Custom Composable for Koin injection
@Composable
fun  rememberKoinInject(clazz: KClass): T {
    return remember {
        getKoin().get(clazz)
    }
}

@Composable
fun  rememberKoinInject(clazz: KClass, qualifier: Qualifier?): T {
    return remember {
        getKoin().get(clazz, qualifier)
    }
}
Compose Navigation with Koin
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    
    NavHost(navController = navController, startDestination = "main") {
        composable("main") {
            MainScreen()
        }
        composable("user/{userId}") { backStackEntry ->
            val userId = backStackEntry.arguments?.getString("userId")?.toIntOrNull() ?: 0
            UserDetailScreen(userId = userId)
        }
        composable("settings") {
            SettingsScreen()
        }
    }
}

@Composable
fun SettingsScreen() {
    val settingsViewModel: SettingsViewModel = koinViewModel()
    val settings by settingsViewModel.settings.collectAsState()
    
    Column {
        Text("Settings")
        // Settings UI
    }
}

Best Practices

Module Organization
  • Group related dependencies: Keep related providers in the same module
  • Use appropriate scopes: Choose the right scope for each dependency
  • Minimize module size: Keep modules focused and manageable
  • Document complex dependencies: Add comments for complex injection logic
Performance
  • Use singletons appropriately: Don't overuse singleton scope
  • Lazy initialization: Use lazy injection when appropriate
  • Memory management: Be aware of object lifecycles
  • Scope management: Close scopes when not needed
Testing
  • Mock external dependencies: Replace network and database dependencies
  • Use test modules: Create separate modules for testing
  • Test injection: Verify that dependencies are injected correctly
  • Isolate tests: Ensure tests don't depend on each other

Common Pitfalls

Avoiding Common Mistakes
  • Don't forget to start Koin: Always start Koin in Application class
  • Don't mix scopes: Be careful with scope mismatches
  • Don't create circular dependencies: Avoid dependency cycles
  • Don't ignore scope lifecycle: Close scopes when not needed
Debugging Tips
  • Enable Koin logging: Use Koin logger for debugging
  • Check module definitions: Verify module structure
  • Use Koin debug mode: Enable debug features
  • Check scope management: Verify scope creation and closure

Practice Exercises

Try these exercises to reinforce your Koin knowledge:

Exercise 1: Weather App
// Create a weather app with:
// - Weather API service
// - Local weather cache
// - Location service
// - Weather repository
// - Multiple ViewModels
// - Custom scopes for user preferences
Exercise 2: E-commerce App
// Build an e-commerce app with:
// - Product API service
// - Shopping cart management
// - User authentication
// - Payment processing
// - Order management
// - Multi-module architecture
Exercise 3: Social Media App
// Create a social media app with:
// - User management
// - Post creation and sharing
// - Real-time messaging
// - Image upload service
// - Push notifications
// - Complex dependency relationships

Next Steps

Now that you have a solid foundation in Koin, explore these advanced topics:

  • Koin Multiplatform: Using Koin with Kotlin Multiplatform
  • Koin with Ktor: Server-side dependency injection
  • Advanced Scoping: Complex scope management patterns
  • Performance Optimization: Optimizing Koin performance
  • Custom DSL: Creating custom Koin DSL
  • Migration from Hilt: Migrating from Hilt to Koin

Resources

Summary

Koin is a powerful and lightweight dependency injection library that provides a pure Kotlin solution for managing dependencies in Android applications. Its simple DSL, lack of annotation processing, and excellent testing support make it an attractive alternative to Hilt for many developers.

You've learned about Koin setup, basic and advanced dependency injection, scopes, property injection, multi-module architecture, testing, and integration with Jetpack Compose. Remember to always consider maintainability, testability, and performance when designing your dependency injection architecture.

Practice regularly with different project structures, experiment with advanced features, and stay updated with the latest Koin patterns and best practices. The more you work with Koin, the more you'll appreciate its simplicity and power for building scalable, maintainable Android applications.