Koin DSL
Thanks to the power of the Kotlin language, Koin provides a DSL to help you describe your application's dependency injection container. With its Kotlin DSL, Koin offers a smart functional API to prepare your dependency injection without annotation processing or code generation.
Overview
Koin's DSL has two main parts:
- Application DSL - Configure the Koin container itself (logging, modules, properties)
- Module DSL - Declare the components and their dependencies
Application DSL
A KoinApplication instance represents your configured Koin container. This lets you set up logging, load properties, and register modules.
Creating a KoinApplication
Choose between two approaches:
koinApplication { }- Creates a standaloneKoinApplicationinstancestartKoin { }- Creates aKoinApplicationand registers it in theGlobalContext
// Standalone instance (useful for testing or custom contexts)
val koinApp = koinApplication {
modules(myModule)
}
// Global instance (standard approach for applications)
startKoin {
logger()
modules(myModule)
}
Configuration Functions
Within koinApplication or startKoin, you can use:
logger()- Set the logging level and Logger implementation (default: EmptyLogger)modules()- Load modules into the container (accepts list or vararg)properties()- Load a HashMap of propertiesfileProperties()- Load properties from a fileenvironmentProperties()- Load properties from OS environment variablescreateEagerInstances()- Instantiate all definitions marked withcreatedAtStartallowOverride(Boolean)- Enable/disable definition overriding (default: true since 3.1.0)
Global vs Local Context
The key difference between koinApplication and startKoin:
startKoin- Registers the container inGlobalContext, making it accessible viaKoinComponent,by inject(), and other global APIskoinApplication- Creates an isolated instance you control directly
// Global context - standard usage
startKoin {
logger()
modules(appModule)
}
// Later, anywhere in your app:
class MyClass : KoinComponent {
val service: Service by inject() // Uses GlobalContext
}
// Local context - advanced usage (testing, multi-context apps)
val customKoin = koinApplication {
modules(testModule)
}.koin
val service = customKoin.get<Service>() // Use specific instance
Starting Koin
A complete Koin setup example:
startKoin {
// Configure logging
logger(Level.INFO)
// Load properties
environmentProperties()
// Declare modules
modules(
networkModule,
databaseModule,
repositoryModule,
viewModelModule
)
// Create eager singletons
createEagerInstances()
}
Module DSL
A Koin module is a logical grouping of definitions. It describes what to create and how to wire dependencies.
Creating a Module
Use the module function:
val myModule = module {
// Your definitions here
}
Definition Keywords
Koin provides several keywords for declaring components:
Lambda-Based Definitions
single { }- Singleton (one instance shared across the app)factory { }- Factory (new instance every time)scoped { }- Scoped (one instance per scope lifecycle)
module {
single { DatabaseHelper() } // Singleton
factory { NetworkRequest() } // Factory
scoped { UserSession() } // Scoped
}
Autowire Definitions (Constructor DSL)
singleOf()- Singleton with constructor autowiringfactoryOf()- Factory with constructor autowiringscopedOf()- Scoped with constructor autowiring
class MyService(val repository: MyRepository)
module {
singleOf(::MyRepository)
singleOf(::MyService) // Dependencies autowired automatically
}
Learn more about Autowire DSL in the Autowire DSL documentation
Resolution & Dependency Injection
Use get() to resolve dependencies within definitions:
class Controller(val service: Service)
class Service(val repository: Repository)
module {
single { Repository() }
single { Service(get()) } // Inject Repository
single { Controller(get()) } // Inject Service
}
Binding Types
Single Type Binding
By default, definitions are bound to their exact type:
interface UserRepository
class UserRepositoryImpl : UserRepository
module {
// Bound as UserRepository (preferred)
single<UserRepository> { UserRepositoryImpl() }
// Also valid using cast
single { UserRepositoryImpl() as UserRepository }
}
Additional Type Binding
Bind a definition to multiple types using bind:
interface Logger
interface DebugLogger
class ConsoleLogger : Logger, DebugLogger
module {
// Available as both ConsoleLogger and Logger
single { ConsoleLogger() } bind Logger::class
// Available as all three types
single { ConsoleLogger() } binds arrayOf(
Logger::class,
DebugLogger::class
)
}
Qualifiers (Named Definitions)
Use named() to distinguish between multiple definitions of the same type:
module {
single<Database>(named("local")) { LocalDatabase() }
single<Database>(named("remote")) { RemoteDatabase() }
}
// Retrieve by qualifier
val localDb: Database by inject(named("local"))
val remoteDb: Database by inject(named("remote"))
Qualifiers can be:
- Strings:
named("qualifier") - Types:
named<MyType>() - Enums:
named(MyEnum.VALUE)
Injection Parameters
Pass runtime parameters to definitions:
class UserPresenter(val userId: String, val view: UserView)
module {
factory { (userId: String, view: UserView) ->
UserPresenter(userId, view)
}
}
// Provide parameters when resolving
val presenter: UserPresenter = get { parametersOf("user123", myView) }
Learn more in the Injection Parameters documentation
Definition Options
Using withOptions
Apply multiple options to a definition:
module {
single { MyService(get()) } withOptions {
named("primary")
createdAtStart()
bind<ServiceInterface>()
}
}
Available options:
named()- Assign a qualifierbind<Type>()- Bind to additional typebinds()- Bind to multiple typescreatedAtStart()- Create eagerly at startupoverride()- Allow override even if global override is disabled (4.2.0+)onClose { }- Register cleanup callback
Eager Instantiation
Create instances immediately at startup:
module {
// Single definition
single(createdAtStart = true) { CacheManager() }
// Or via withOptions
single { CacheManager() } withOptions {
createdAtStart()
}
}
You can also mark an entire module:
module(createdAtStart = true) {
single { ServiceA() }
single { ServiceB() }
}
Override Control (4.2.0+)
Mark specific definitions as allowed to override:
startKoin {
allowOverride(false) // Strict mode
modules(productionModule, testModule)
}
val productionModule = module {
single<Service> { ProductionService() }
}
val testModule = module {
// Explicitly allowed to override
single<Service> { MockService() } withOptions {
override()
}
}
Scopes
Define logical grouping for scoped instances:
module {
scope<MyActivity> {
scoped { ActivityPresenter() }
}
}
Learn more in the Scopes documentation
Lifecycle Callbacks
Register cleanup logic with onClose:
module {
single {
DatabaseConnection()
} onClose { connection ->
connection?.close()
}
}
Complete Example
Putting it all together:
// Data layer
class ApiClient
class Database
class UserRepository(val api: ApiClient, val db: Database)
// Domain layer
class GetUserUseCase(val repository: UserRepository)
// Presentation layer
class UserViewModel(val useCase: GetUserUseCase)
// Module definitions
val dataModule = module {
single { ApiClient() }
single { Database() }
single { UserRepository(get(), get()) }
}
val domainModule = module {
factory { GetUserUseCase(get()) }
}
val presentationModule = module {
factory { UserViewModel(get()) }
}
// Start Koin
fun main() {
startKoin {
logger(Level.INFO)
modules(dataModule, domainModule, presentationModule)
}
}
Best Practices
- Organize by layers - Create separate modules for data, domain, and presentation layers
- Use autowire DSL - Prefer
singleOf(::MyClass)oversingle { MyClass(get()) }for cleaner code - Leverage qualifiers - Use
named()when you need multiple implementations of the same type - Explicit types - Use
single<Interface> { Implementation() }for clarity - Module composition - Use
includes()to compose larger modules from smaller ones
For more details on modules, see Modules documentation
For more details on definitions, see Definitions documentation