Binding Patterns
This guide covers different ways to define and bind dependencies in Koin modules, from simple singleton definitions to complex external library integrations.
Overview
Koin provides flexible patterns for binding dependencies to suit different scenarios. Whether you're defining your own classes, binding interfaces to implementations, or integrating external libraries, Koin's DSL makes it straightforward.
Quick Reference
| Pattern | Koin Syntax | Use Case |
|---|---|---|
| Singleton | single { MyClass() } | Shared instance across app |
| Factory | factory { MyClass() } | New instance each time |
| Interface Binding | single<Interface> { Impl() } | Bind interface to implementation |
| Autowire Binding | singleOf(::MyClass) | Constructor injection autowiring |
| External Library | single { Retrofit.Builder()...build() } | Third-party library instances |
| With Parameters | factory { params -> MyClass(params.get()) } | Runtime parameters |
Single vs Factory
The two fundamental definition types in Koin are single and factory.
Single - Shared Instance
Creates one instance that's shared across your entire application:
class Database {
init {
println("Database created")
}
}
val appModule = module {
single { Database() }
}
// Usage
val db1: Database = get() // Prints: Database created
val db2: Database = get() // No print - reuses same instance
// db1 === db2 (same instance)
When to use:
- Expensive resources (database, network clients)
- Stateful objects that need to be shared
- Configuration objects
- Repositories, managers, services
Factory - New Instance Each Time
Creates a new instance on every injection:
class UserPresenter(private val userId: String) {
init {
println("Presenter created for $userId")
}
}
val appModule = module {
factory { params -> UserPresenter(params.get()) }
}
// Usage
val presenter1 = get<UserPresenter> { parametersOf("user1") } // Prints: Presenter created for user1
val presenter2 = get<UserPresenter> { parametersOf("user2") } // Prints: Presenter created for user2
// presenter1 !== presenter2 (different instances)
When to use:
- Lightweight, stateless objects
- View presenters/controllers (new for each screen)
- Objects that need unique state
- Temporary/disposable objects
Comparison
| Aspect | single | factory |
|---|---|---|
| Instances | One shared instance | New instance each time |
| Memory | Kept in memory until app closes | Garbage collected when unused |
| State | Shared state across app | Independent state per instance |
| Performance | Faster (no recreation) | Slight overhead creating instances |
| Best for | Services, Repositories, Databases | Presenters, temporary objects |
Interface Binding
Binding interfaces to implementations is a core dependency injection pattern. Koin provides multiple ways to do this.
Method 1: Type Specification
Specify the interface type directly:
interface AnalyticsService {
fun logEvent(event: String)
}
class FirebaseAnalytics : AnalyticsService {
override fun logEvent(event: String) {
println("Firebase: $event")
}
}
val appModule = module {
single<AnalyticsService> { FirebaseAnalytics() }
}
// Inject as interface
class UserRepository(private val analytics: AnalyticsService)
val repoModule = module {
singleOf(::UserRepository) // Koin resolves AnalyticsService automatically
}
Method 2: Using bind Keyword
Bind implementation to interface using bind:
interface UserRepository {
fun getUser(id: String): User
}
class UserRepositoryImpl(
private val database: Database
) : UserRepository {
override fun getUser(id: String): User {
return database.queryUser(id)
}
}
val appModule = module {
single { Database() }
single { UserRepositoryImpl(get()) } bind UserRepository::class
}
Method 3: Autowire with Binding
Use autowire DSL with type binding:
val appModule = module {
singleOf(::UserRepositoryImpl) bind UserRepository::class
}
Even cleaner! Koin automatically resolves constructor dependencies and binds to the interface.
Multiple Interface Binding
Bind one implementation to multiple interfaces:
interface Serializer {
fun serialize(data: Any): String
}
interface Deserializer {
fun deserialize(json: String): Any
}
class JsonConverter : Serializer, Deserializer {
override fun serialize(data: Any): String = "..."
override fun deserialize(json: String): Any = "..."
}
val appModule = module {
single { JsonConverter() } binds arrayOf(
Serializer::class,
Deserializer::class
)
}
// Now you can inject either interface
class DataStore(private val serializer: Serializer)
class DataLoader(private val deserializer: Deserializer)
val dataModule = module {
singleOf(::DataStore) // Gets JsonConverter as Serializer
singleOf(::DataLoader) // Gets JsonConverter as Deserializer
}
When binding to multiple interfaces, Koin registers the instance under each interface type, but still creates only one instance that's shared across all bindings.
External Library Integration
Integrating third-party libraries is a common use case. Here's how to provide instances of external library classes.
Retrofit Example
interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") String): User
}
val networkModule = module {
// Provide OkHttpClient
single {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
}
// Provide Retrofit
single {
Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(get()) // Inject OkHttpClient
.addConverterFactory(GsonConverterFactory.create())
.build()
}
// Provide API Service
single {
get<Retrofit>().create(ApiService::class.java)
}
}
Room Database Example
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
val databaseModule = module {
// Provide Room Database
single {
Room.databaseBuilder(
androidContext(),
AppDatabase::class.java,
"app-database"
).build()
}
// Provide DAOs
single { get<AppDatabase>().userDao() }
}
Gson Example
val serializationModule = module {
single {
GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
.setPrettyPrinting()
.create()
}
}
// Usage in other definitions
val networkModule = module {
single {
Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(get())) // Inject Gson
.build()
}
}
WorkManager Example
class SyncWorker(
context: Context,
params: WorkerParameters,
private val repository: SyncRepository
) : Worker(context, params) {
override fun doWork(): Result {
repository.sync()
return Result.success()
}
}
val workModule = module {
// For WorkManager with Koin support
workerOf(::SyncWorker)
}
For WorkManager, use the koin-androidx-workmanager dependency and see WorkManager Integration for complete setup.
Constructor Injection in Modules
When defining dependencies in modules, use get() to inject other dependencies.
Basic Constructor Injection
class ApiClient
class AuthService
class UserRepository(
private val apiClient: ApiClient,
private val authService: AuthService
)
val appModule = module {
single { ApiClient() }
single { AuthService() }
single { UserRepository(get(), get()) } // Koin resolves dependencies
}
Autowire Alternative
val appModule = module {
singleOf(::ApiClient)
singleOf(::AuthService)
singleOf(::UserRepository) // Dependencies resolved automatically!
}
Named Parameters for Clarity
For complex constructors, use named parameters:
class UserService(
private val userRepo: UserRepository,
private val authRepo: AuthRepository,
private val analytics: AnalyticsService,
private val logger: Logger
)
val appModule = module {
single {
UserService(
userRepo = get(),
authRepo = get(),
analytics = get(),
logger = get()
)
}
}
Dependency Resolution Order
Koin resolves dependencies in the order they appear in constructor parameters:
class MyService(
private val dep1: Dependency1,
private val dep2: Dependency2,
private val dep3: Dependency3
)
val appModule = module {
single { Dependency1() }
single { Dependency2() }
single { Dependency3() }
// Koin resolves: get<Dependency1>(), get<Dependency2>(), get<Dependency3>()
single { MyService(get(), get(), get()) }
}
Factory with Parameters
Sometimes you need to pass runtime parameters when creating instances.
Basic Parameters
class UserPresenter(
private val userId: String, // Runtime parameter
private val repository: UserRepository // Injected dependency
)
val appModule = module {
singleOf(::UserRepository)
// Option 1: Using params.get()
factory { params ->
UserPresenter(
userId = params.get(), // Get runtime parameter
repository = get() // Inject dependency
)
}
// Option 2: Using destructured declaration
factory { (userId) ->
UserPresenter(
userId = userId,
repository = get()
)
}
}
// Usage
class UserActivity : AppCompatActivity() {
private val userId = "user123"
private val presenter: UserPresenter by inject { parametersOf(userId) }
}
Multiple Parameters
class OrderPresenter(
private val orderId: String,
private val customerId: String,
private val repository: OrderRepository
)
val appModule = module {
// Option 1: Using params.get() with indices
factory { params ->
OrderPresenter(
orderId = params.get(0), // First parameter
customerId = params.get(1), // Second parameter
repository = get() // Injected
)
}
// Option 2: Using Kotlin destructured declarations (cleaner!)
factory { (orderId, customerId) ->
OrderPresenter(
orderId = orderId,
customerId = customerId,
repository = get()
)
}
}
// Usage
val presenter: OrderPresenter = get { parametersOf("order123", "customer456") }
Destructured declarations provide cleaner, more readable code for multiple parameters. Kotlin automatically unpacks the parameters in order.
Autowire with Parameters
Even with autowire, you can use parameters:
class UserPresenter(
private val userId: String,
private val repository: UserRepository
)
val appModule = module {
singleOf(::UserRepository)
factoryOf(::UserPresenter) // Autowire handles both parameter and dependency!
}
// Usage
val presenter: UserPresenter by inject { parametersOf("user123") }
Koin's autowire DSL automatically matches parametersOf() values to constructor parameters based on type, then resolves remaining dependencies from the container.
Type-Safe Parameters
data class UserConfig(val userId: String, val isAdmin: Boolean)
class UserService(
private val config: UserConfig,
private val repository: UserRepository
)
val appModule = module {
factory { params ->
UserService(
config = params.get(), // Type-safe: expects UserConfig
repository = get()
)
}
}
// Usage
val config = UserConfig("user123", isAdmin = true)
val service: UserService = get { parametersOf(config) }
Advanced Binding Patterns
Lazy Initialization
Create definitions that are initialized only when first accessed:
val appModule = module {
single {
// This expensive operation only runs when Database is first injected
Database().apply {
println("Database initialized")
loadSchema()
}
}
}
All Koin definitions are lazy by default - they're created on first access, not at module load time.
Early Initialization
Force creation at startup:
val appModule = module {
single(createdAtStart = true) {
Database() // Created when Koin starts
}
}
When to use:
- Resources that need to be ready before app starts
- Background services that should start immediately
- Expensive initialization that should happen during splash screen
Scoped Definitions
Create instances scoped to specific lifecycles:
val appModule = module {
scope<MainActivity> {
scoped { ActivityPresenter() } // One per MainActivity instance
}
}
// Usage in Activity
class MainActivity : AppCompatActivity() {
private val presenter: ActivityPresenter by activityScope()
}
For more on scoping, see Android Scopes.
Collections Injection
Inject all implementations of an interface:
interface Plugin {
fun execute()
}
class AnalyticsPlugin : Plugin {
override fun execute() { /* ... */ }
}
class LoggingPlugin : Plugin {
override fun execute() { /* ... */ }
}
val pluginModule = module {
single<Plugin> { AnalyticsPlugin() }
single<Plugin> { LoggingPlugin() }
}
class PluginManager(private val plugins: List<Plugin>)
val managerModule = module {
single { PluginManager(getAll()) } // Injects List<Plugin>
}
For more advanced patterns, see Advanced Patterns.
Best Practices
1. Prefer Autowire DSL
// Good - Clean and concise
val appModule = module {
singleOf(::Database)
singleOf(::ApiClient)
singleOf(::UserRepository)
}
// Verbose - Use only when needed
val appModule = module {
single { Database() }
single { ApiClient() }
single { UserRepository(get(), get()) }
}
2. Use Interface Binding
// Good - Depends on abstraction
interface UserRepository
class UserRepositoryImpl : UserRepository
val appModule = module {
singleOf(::UserRepositoryImpl) bind UserRepository::class
}
// Bad - Depends on concrete implementation
val appModule = module {
singleOf(::UserRepositoryImpl)
}
3. Keep Module Definitions Simple
// Good - Definition delegates to constructor
val networkModule = module {
single { createOkHttpClient() }
}
fun createOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.build()
}
// Avoid - Too much logic in module
val networkModule = module {
single {
val timeout = if (BuildConfig.DEBUG) 60L else 30L
val interceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) Level.BODY else Level.NONE
}
OkHttpClient.Builder()
.connectTimeout(timeout, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.build()
}
}
4. Group Related Definitions
// Good - Related dependencies grouped together
val networkModule = module {
single { OkHttpClient.Builder().build() }
single { Retrofit.Builder().client(get()).build() }
single { get<Retrofit>().create(ApiService::class.java) }
}
val databaseModule = module {
single { Room.databaseBuilder(...).build() }
single { get<AppDatabase>().userDao() }
}
5. Use Single for Shared State
// Good - Shared state as singleton
val appModule = module {
single { UserPreferences(androidContext()) } // Shared across app
factory { UserPresenter(get()) } // New for each screen
}
Common Patterns
Repository Pattern
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun saveUser(user: User)
}
class UserRepositoryImpl(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun getUser(id: String): User {
return localDataSource.getUser(id) ?: remoteDataSource.getUser(id).also {
localDataSource.saveUser(it)
}
}
override suspend fun saveUser(user: User) {
localDataSource.saveUser(user)
remoteDataSource.saveUser(user)
}
}
val dataModule = module {
singleOf(::UserRemoteDataSource)
singleOf(::UserLocalDataSource)
singleOf(::UserRepositoryImpl) bind UserRepository::class
}
Use Case Pattern
class GetUserUseCase(
private val userRepository: UserRepository
) {
suspend operator fun invoke(userId: String): Result<User> {
return try {
Result.success(userRepository.getUser(userId))
} catch (e: Exception) {
Result.failure(e)
}
}
}
val domainModule = module {
factoryOf(::GetUserUseCase) // New instance per usage
}
ViewModel with Dependencies
class UserViewModel(
private val getUserUseCase: GetUserUseCase,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val userId: String = savedStateHandle["user_id"] ?: ""
val user: LiveData<User> = liveData {
emit(getUserUseCase(userId).getOrNull())
}
}
val viewModelModule = module {
viewModelOf(::UserViewModel) // Koin + Android ViewModels
}
Summary
Koin provides flexible binding patterns for all scenarios:
- Single vs Factory - Choose based on instance sharing needs
- Interface Binding - Three ways: type specification,
bindkeyword, or autowire withbind - External Libraries - Easy integration with builder patterns
- Constructor Injection - Automatic with
get()or autowire DSL - Parameters - Runtime values with
parametersOf() - Best Practices - Prefer autowire, bind interfaces, keep definitions simple
Next Steps
- Qualifiers - Handle multiple bindings of the same type
- Android Scopes - Scope instances to lifecycles
- Advanced Patterns - Collections, lazy injection, assisted injection
- Koin Modules - Organize and structure your modules