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
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.