Skip to main content
Version: 4.2

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

PatternKoin SyntaxUse Case
Singletonsingle { MyClass() }Shared instance across app
Factoryfactory { MyClass() }New instance each time
Interface Bindingsingle<Interface> { Impl() }Bind interface to implementation
Autowire BindingsingleOf(::MyClass)Constructor injection autowiring
External Librarysingle { Retrofit.Builder()...build() }Third-party library instances
With Parametersfactory { 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

Aspectsinglefactory
InstancesOne shared instanceNew instance each time
MemoryKept in memory until app closesGarbage collected when unused
StateShared state across appIndependent state per instance
PerformanceFaster (no recreation)Slight overhead creating instances
Best forServices, Repositories, DatabasesPresenters, 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
}
info

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)
}
note

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") }
info

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") }
info

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()
}
}
// 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, bind keyword, or autowire with bind
  • 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