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
Group Related Modules
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
singlefor 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
- Advanced Patterns - Complex DI scenarios
- Testing - Comprehensive testing guide
- Multi-Module Apps - Organizing large projects
- Scopes - Lifecycle management