Skip to main content
Version: 4.2

Best Practices

This guide provides comprehensive best practices for using Koin effectively in Android applications.

Module Organization

Use includes() for Module Dependencies (Koin 4.2+)

info

Recommended: Use includes() to declare module dependencies explicitly. This prevents accidental overriding and makes module relationships clear.

// :core:network module
val networkModule = module {
single { OkHttpClient() }
single { Retrofit.Builder().client(get()).build() }
}

// :core:data module - includes network dependency
val dataModule = module {
includes(networkModule) // Explicit dependency

singleOf(::UserRepository)
singleOf(::OrderRepository)
}

// :feature:home module - includes data (and transitively network)
val homeModule = module {
includes(dataModule) // Automatically includes networkModule too

viewModelOf(::HomeViewModel)
}

// Application - only load top-level modules
startKoin {
modules(
homeModule, // Includes dataModule + networkModule
profileModule, // Includes its own dependencies
settingsModule // Includes its own dependencies
)
}

Benefits:

  • ✅ Prevents accidental definition override
  • ✅ Makes dependencies explicit and self-documenting
  • ✅ Reduces errors from missing dependency modules
  • ✅ Simplifies application module loading

Explicit Override (Koin 4.2+)

info

Koin 4.2+: Override protection is enabled by default. You must use override = true to intentionally override definitions.

// ✅ Good - Explicit override for testing
val testModule = module {
single<UserRepository>(override = true) {
FakeUserRepository() // Explicitly override production repository
}
}

// ❌ Error in Koin 4.2+ - Accidental override not allowed
val productionModule = module {
single<UserRepository> { UserRepositoryImpl() }
}

val featureModule = module {
single<UserRepository> { AnotherUserRepositoryImpl() } // ERROR: Already defined!
}

// ✅ Good - Use qualifiers for different implementations
val module1 = module {
single<UserRepository>(named("local")) { LocalUserRepository() }
single<UserRepository>(named("remote")) { RemoteUserRepository() }
}

Structure by Layer

Organize modules by architectural layer:

// :core:network module
val networkModule = module {
single { createOkHttpClient() }
single { createRetrofit(get()) }
}

// :core:data module
val dataModule = module {
singleOf(::UserRepository)
singleOf(::OrderRepository)
singleOf(::ProductRepository)
}

// :core:domain module
val domainModule = module {
factoryOf(::GetUserUseCase)
factoryOf(::PlaceOrderUseCase)
}

// :feature:home module
val homeModule = module {
viewModelOf(::HomeViewModel)
}

One Module Per Gradle Module

// ✅ Good - Clear module boundaries
// :feature:login/LoginModule.kt
val loginModule = module {
viewModelOf(::LoginViewModel)
factoryOf(::LoginUseCase)
}

// :feature:profile/ProfileModule.kt
val profileModule = module {
viewModelOf(::ProfileViewModel)
scopedOf(::ProfileRepository)
}

// ❌ Bad - Mixed concerns
val everythingModule = module {
// Login stuff
viewModelOf(::LoginViewModel)

// Profile stuff
viewModelOf(::ProfileViewModel)

// Settings stuff
viewModelOf(::SettingsViewModel)
}

Module Naming Convention

// ✅ Good - Descriptive names
val networkModule = module { /* ... */ }
val databaseModule = module { /* ... */ }
val authenticationModule = module { /* ... */ }

// ❌ Bad - Generic names
val module1 = module { /* ... */ }
val appModule = module { /* ... */ } // Too generic
object CoreModules {
val all = listOf(
networkModule,
databaseModule,
dataModule
)
}

object FeatureModules {
val all = listOf(
loginModule,
homeModule,
profileModule,
settingsModule
)
}

// Application.kt
startKoin {
modules(CoreModules.all + FeatureModules.all)
}

Performance Optimization

Use single for Expensive Objects

// ✅ Good - Reuse expensive objects
module {
single {
Room.databaseBuilder(androidContext(), AppDatabase::class.java, "app-db")
.build()
}

single {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
}

// ❌ Bad - Recreating expensive objects
module {
factory { // New database instance each time!
Room.databaseBuilder(androidContext(), AppDatabase::class.java, "app-db")
.build()
}
}

Lazy Module Loading

class MyApplication : Application() {
override fun onCreate() {
super.onCreate()

startKoin {
androidContext(this@MyApplication)

// Load essential modules immediately
modules(
networkModule,
databaseModule,
dataModule
)

// Load feature modules lazily
lazyModules(
premiumFeaturesModule,
debugToolsModule,
analyticsModule
)
}
}
}

// Load on-demand
class PremiumActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loadKoinModules(premiumFeaturesModule)
}
}

Avoid Unnecessary Scopes

// ✅ Good - Scope when needed
module {
single { Database() } // Singleton for long-lived object

activityScope {
scoped { CheckoutState() } // Activity-scoped for shared state
}

factory { EmailValidator() } // Factory for stateless utility
}

// ❌ Bad - Unnecessary scoping
module {
scope(named("email_validator")) { // Overkill for simple validator
scoped { EmailValidator() }
}
}

Memory Management

Avoid Activity/Fragment Leaks

// ❌ Bad - Activity leak
module {
single { SomeService(get<Activity>()) } // Activity reference in singleton!
}

// ✅ Good - Use Application context
module {
single { SomeService(androidContext()) } // Application context, safe
}

// ✅ Good - Use activity scope
module {
activityScope {
scoped { SomeService(/* activity-scoped dependencies */) }
}
}

Close Scopes Properly

// ✅ Good - Automatic scope management
class MyActivity : ScopeActivity() {
override val scope: Scope by activityScope()
// Scope automatically closed in onDestroy
}

// ❌ Bad - Manual scope without cleanup
class MyActivity : AppCompatActivity() {
private val myScope = createScope<MyActivity>()
// Scope never closed - memory leak!
}

// ✅ Good - Manual scope with cleanup
class MyActivity : AppCompatActivity() {
private lateinit var myScope: Scope

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myScope = createScope<MyActivity>()
}

override fun onDestroy() {
myScope.close()
super.onDestroy()
}
}

Clear References in Long-Lived Objects

// ❌ Bad - Holding references to UI
class UserRepository {
private val listeners = mutableListOf<UserUpdateListener>() // Might hold Activity refs

fun addListener(listener: UserUpdateListener) {
listeners.add(listener)
}
}

// ✅ Good - Weak references or manual cleanup
class UserRepository {
private val listeners = mutableListOf<WeakReference<UserUpdateListener>>()

fun addListener(listener: UserUpdateListener) {
listeners.add(WeakReference(listener))
}

fun removeListener(listener: UserUpdateListener) {
listeners.removeAll { it.get() == listener || it.get() == null }
}
}

Lifecycle Management

Match Scope to Lifecycle

// Application lifecycle
module {
single { Database() }
single { NetworkClient() }
}

// Activity lifecycle (survives rotation)
module {
activityRetainedScope {
scoped { CheckoutFlow() }
scoped { FormState() }
}
}

// Activity lifecycle (doesn't survive rotation)
module {
activityScope {
scoped { ScreenPresenter() }
}
}

// Fragment lifecycle
module {
fragmentScope {
scoped { ProductListState() }
}
}

ViewModel Scope Best Practices

// ✅ Good - ViewModel survives configuration changes
module {
viewModelOf(::HomeViewModel)
}

class HomeViewModel(
private val repository: UserRepository // From application scope
) : ViewModel()

// ❌ Bad - ViewModel in activity scope
module {
activityScope {
scoped { HomeViewModel(get()) } // Lost on rotation!
}
}

Service Lifecycle

module {
scope<MusicService> {
scoped { PlaybackEngine() }
}
}

class MusicService : Service(), AndroidScopeComponent {
override val scope: Scope by lazy { createScope<MusicService>() }

override fun onDestroy() {
super.onDestroy()
scope.close() // Clean up
}
}

Dependency Definitions

Prefer Constructor Injection

// ✅ Good - Constructor injection
class UserRepository(
private val api: ApiService,
private val database: UserDatabase
) {
suspend fun getUser(id: String): User {
return database.getUser(id) ?: api.fetchUser(id)
}
}

module {
singleOf(::UserRepository) // Autowire DSL
}

// ❌ Bad - Field injection with KoinComponent
class UserRepository : KoinComponent {
private val api: ApiService by inject()
private val database: UserDatabase by inject()
}

Use Autowire DSL

// ✅ Good - Clean and concise
module {
singleOf(::Database)
singleOf(::ApiClient)
singleOf(::UserRepository)
viewModelOf(::HomeViewModel)
}

// ❌ Verbose - Manual dependency resolution
module {
single { Database() }
single { ApiClient(get()) }
single { UserRepository(get(), get()) }
viewModel { HomeViewModel(get()) }
}

Explicit Types for Interfaces

// ✅ Good - Clear interface binding
module {
single<UserRepository> { UserRepositoryImpl(get()) }
single<ApiService> { ApiServiceImpl() }
}

// ❌ Bad - Unclear what's exposed
module {
single { UserRepositoryImpl(get()) } // Is this the interface or implementation?
}

Testing Strategies

Verify Modules at Compile Time

class ModuleVerificationTest {
@Test
fun `verify all modules`() {
koinApplication {
modules(
networkModule,
databaseModule,
dataModule,
homeModule
)
checkModules() // Fails if any dependency is missing
}
}
}

Use Fakes Over Mocks

// ✅ Good - Fake implementation
class FakeUserRepository : UserRepository {
private val users = mutableListOf<User>()

override suspend fun getUser(id: String): User {
return users.find { it.id == id } ?: throw UserNotFoundException()
}

override suspend fun saveUser(user: User) {
users.add(user)
}

fun addTestUser(user: User) {
users.add(user)
}
}

// ❌ Acceptable but less ideal - Mock
val mockRepo = mockk<UserRepository> {
coEvery { getUser(any()) } returns User("1", "Test")
}

Test Module Independence

@Test
fun `login module works independently`() {
koinApplication {
modules(
loginModule,
// Mock dependencies
module {
single<AuthService> { FakeAuthService() }
}
)
checkModules()
}
}

Code Organization

Keep Business Logic Out of DI

// ❌ Bad - Business logic in module
module {
single {
val user = get<UserRepository>().getUser("123")
if (user.isPremium) {
PremiumService(user)
} else {
BasicService(user)
}
}
}

// ✅ Good - Business logic in dedicated class
class ServiceFactory(private val userRepo: UserRepository) {
suspend fun createService(userId: String): Service {
val user = userRepo.getUser(userId)
return if (user.isPremium) {
PremiumService(user)
} else {
BasicService(user)
}
}
}

module {
singleOf(::ServiceFactory)
}

Separate Interface from Implementation

// api module
interface UserRepository {
suspend fun getUser(id: String): User
}

// impl module
internal class UserRepositoryImpl(
private val api: ApiService,
private val db: UserDatabase
) : UserRepository {
override suspend fun getUser(id: String): User {
return db.getUser(id) ?: api.fetchUser(id).also { db.insert(it) }
}
}

module {
single<UserRepository> { UserRepositoryImpl(get(), get()) }
}

Document Complex Modules

/**
* Network module providing HTTP clients and API services.
*
* Provides:
* - OkHttpClient: Configured with 30s timeout, auth interceptor
* - Retrofit: Base URL from BuildConfig
* - ApiService: Main API interface
*
* Dependencies:
* - AndroidContext for cache directory
*/
val networkModule = module {
single {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.cache(Cache(androidContext().cacheDir, 10 * 1024 * 1024))
.addInterceptor(AuthInterceptor())
.build()
}

single {
Retrofit.Builder()
.baseUrl(BuildConfig.API_URL)
.client(get())
.addConverterFactory(GsonConverterFactory.create())
.build()
}

single { get<Retrofit>().create(ApiService::class.java) }
}

Production Readiness

Use Proper Logging

startKoin {
// Development
if (BuildConfig.DEBUG) {
androidLogger(Level.ERROR) // Or Level.DEBUG for verbose
} else {
androidLogger(Level.NONE) // No logging in production
}

androidContext(this@MyApplication)
modules(appModules)
}

Handle Missing Dependencies Gracefully

class FeatureManager : KoinComponent {
fun initializeFeature(featureName: String) {
val feature = getOrNull<FeatureModule>(named(featureName))

if (feature != null) {
feature.initialize()
} else {
Log.w("FeatureManager", "Feature $featureName not available")
}
}
}

Module Verification in CI

// Run in CI pipeline
class CIModuleTest {
@Test
fun `verify production modules configuration`() {
koinApplication {
modules(ProductionModules.all)
checkModules() // Fail build if misconfigured
}
}
}

Environment-Specific Configuration

val configModule = module {
single<AppConfig> {
when {
BuildConfig.DEBUG -> DebugConfig()
BuildConfig.BUILD_TYPE == "staging" -> StagingConfig()
else -> ProductionConfig()
}
}
}

interface AppConfig {
val apiUrl: String
val enableAnalytics: Boolean
}

class ProductionConfig : AppConfig {
override val apiUrl = "https://api.production.com"
override val enableAnalytics = true
}

class DebugConfig : AppConfig {
override val apiUrl = "http://localhost:8080"
override val enableAnalytics = false
}

Common Anti-Patterns to Avoid

1. Service Locator Overuse

// ❌ Bad - Service locator pattern
class UserViewModel : ViewModel(), KoinComponent {
fun loadUser() {
val repository = get<UserRepository>() // Manual resolution
val analytics = get<AnalyticsService>()

// Use dependencies...
}
}

// ✅ Good - Proper dependency injection
class UserViewModel(
private val repository: UserRepository,
private val analytics: AnalyticsService
) : ViewModel() {
fun loadUser() {
// Dependencies already injected
}
}

2. God Modules

// ❌ Bad - Everything in one module
val appModule = module {
// 100+ definitions here
single { Database() }
single { NetworkClient() }
viewModel { HomeViewModel(get()) }
viewModel { ProfileViewModel(get()) }
// ... 96 more definitions
}

// ✅ Good - Organized modules
val databaseModule = module {
single { Database() }
single { UserDao() }
}

val networkModule = module {
single { NetworkClient() }
single { ApiService() }
}

val homeModule = module {
viewModel { HomeViewModel(get()) }
}

3. Circular Dependencies

// ❌ Bad - Circular dependency
class ServiceA(val serviceB: ServiceB)
class ServiceB(val serviceA: ServiceA)

module {
single { ServiceA(get()) }
single { ServiceB(get()) } // Circular!
}

// ✅ Good - Refactor to remove cycle
class SharedService
class ServiceA(val shared: SharedService)
class ServiceB(val shared: SharedService)

module {
single { SharedService() }
single { ServiceA(get()) }
single { ServiceB(get()) }
}

4. Excessive Qualifiers

// ❌ Bad - Qualifiers for different types
module {
single(named("user_repository")) { UserRepository() }
single(named("order_repository")) { OrderRepository() }
single(named("product_repository")) { ProductRepository() }
}

// ✅ Good - Types distinguish themselves
module {
singleOf(::UserRepository)
singleOf(::OrderRepository)
singleOf(::ProductRepository)
}

5. Mixing Concerns

// ❌ Bad - UI logic in module
module {
single {
val context = androidContext()
Toast.makeText(context, "Module loaded", Toast.LENGTH_SHORT).show() // NO!
Database(context)
}
}

// ✅ Good - Pure dependency creation
module {
single { Database(androidContext()) }
}

Debugging and Troubleshooting

Enable Debug Logging

startKoin {
androidLogger(Level.DEBUG) // See all Koin operations
androidContext(this@MyApplication)
modules(appModules)
}

Use checkModules() Early

class MyApplication : Application() {
override fun onCreate() {
super.onCreate()

startKoin {
androidContext(this@MyApplication)
modules(allModules)
}

// Verify in debug builds
if (BuildConfig.DEBUG) {
getKoin().checkModules()
}
}
}

Add Scope Callbacks for Debugging

class DebugActivity : ScopeActivity() {
override val scope: Scope by activityScope()

init {
scope.registerCallback(object : ScopeCallback {
override fun onScopeClose(scope: Scope) {
Log.d("Koin", "Scope ${scope.id} closing")
}
})
}
}

Validate Definitions

@Test
fun `all ViewModels can be created`() {
koinApplication {
modules(allModules)

// Try to create each ViewModel
val homeVM = getKoin().get<HomeViewModel>()
val profileVM = getKoin().get<ProfileViewModel>()

assertNotNull(homeVM)
assertNotNull(profileVM)
}
}

Migration Strategies

Gradual Migration from Manual DI

// Step 1: Start with new features
val newFeatureModule = module {
viewModelOf(::NewFeatureViewModel)
singleOf(::NewFeatureRepository)
}

startKoin {
modules(newFeatureModule)
}

// Step 2: Migrate existing features one by one
val legacyModule = module {
// Wrap existing singletons
single { ExistingSingleton.getInstance() }
}

// Step 3: Refactor to proper DI
val refactoredModule = module {
singleOf(::RefactoredService) // No more static getInstance()
}

Migrating from Dagger/Hilt

// Hilt Component → Koin Scope
// @InstallIn(ActivityComponent::class) → activityScope { }

// Before (Hilt)
@InstallIn(ActivityComponent::class)
@Module
object ActivityModule {
@Provides
fun providePresenter(): Presenter = Presenter()
}

// After (Koin)
val activityModule = module {
activityScope {
scopedOf(::Presenter)
}
}

Performance Checklist

Use single for:

  • Database instances
  • Network clients
  • Repositories
  • Manager classes

Use factory for:

  • Stateless utilities
  • Short-lived objects
  • Objects that need fresh instances

Use scoped for:

  • Activity-specific state
  • Fragment-specific state
  • ViewModel dependencies

Use lazy loading for:

  • Optional features
  • Premium features
  • Debug tools

Profile your app:

  • Use Android Profiler
  • Monitor allocation during startKoin
  • Check for scope leaks

Security Best Practices

Don't Store Secrets in Modules

// ❌ Bad - Hardcoded secrets
module {
single {
Retrofit.Builder()
.baseUrl("https://api.example.com")
.addInterceptor { chain ->
chain.proceed(
chain.request().newBuilder()
.header("API-Key", "super-secret-key") // NO!
.build()
)
}
.build()
}
}

// ✅ Good - Secrets from secure storage
module {
single {
val securePrefs = get<SecurePreferences>()
Retrofit.Builder()
.baseUrl("https://api.example.com")
.addInterceptor(AuthInterceptor(securePrefs))
.build()
}
}

Validate External Input

module {
factory { params ->
val userId = params.get<String>()
require(userId.isNotBlank()) { "User ID cannot be blank" }
UserService(userId, get())
}
}

Summary

Key best practices for Koin:

Organization:

  • One module per Gradle module
  • Structure by layer
  • Group related modules

Performance:

  • Use single for expensive objects
  • Lazy load optional features
  • Avoid unnecessary scopes

Memory:

  • Match scope to lifecycle
  • Avoid activity leaks
  • Close scopes properly

Code Quality:

  • Prefer constructor injection
  • Use autowire DSL
  • Separate interface from implementation

Testing:

  • Verify modules with checkModules()
  • Use fakes over mocks
  • Test module independence

Production:

  • Proper logging configuration
  • Environment-specific config
  • Module verification in CI

Next Steps