← All courses

Database & Dependency Injection

🗓 May 31, 2026 ⏱ 2 min read

A shared database with SQLDelight

To store data on-device in shared code, the standard tool is SQLDelight. You write your SQL in .sq files, and SQLDelight generates type-safe Kotlin APIs that work on Android (SQLite) and iOS (native SQLite) alike.

-- User.sq (in commonMain)
CREATE TABLE User (
  id INTEGER NOT NULL PRIMARY KEY,
  name TEXT NOT NULL
);

selectAll:
SELECT * FROM User;

insert:
INSERT INTO User(id, name) VALUES (?, ?);
// generated, type-safe API
db.userQueries.insert(1, "Anand")
val users: List<User> = db.userQueries.selectAll().executeAsList()

The database driver is platform-specific (provided via expect/actual or injection), but every query you write is shared.

Why dependency injection helps

Your shared code has pieces that depend on each other: the API needs an HTTP client, the repository needs the API and the database, the ViewModel needs the repository. Wiring these by hand gets messy. Dependency injection (DI) assembles them for you and makes testing easy.

Koin: multiplatform DI

Koin is a lightweight DI library that works across KMP targets. You declare how to build each object once in common code.

// commonMain
val sharedModule = module {
    single { HttpClient { install(ContentNegotiation) { json() } } }
    single { UserApi(get()) }                    // get() supplies the HttpClient
    single { UserRepository(get(), get()) }      // api + database
    factory { UsersViewModel(get()) }            // a new one per screen
}

fun initKoin() = startKoin { modules(sharedModule) }

get() tells Koin to supply a dependency it already knows how to build — so the whole graph is assembled automatically.

Platform-specific pieces in DI

Platform-only objects (like the database driver) are provided by a per-platform Koin module, then combined with the shared module — a clean alternative to scattering expect/actual everywhere.

// androidMain
val androidModule = module {
    single<SqlDriver> { AndroidSqliteDriver(Schema, get(), "app.db") }
}
// iosMain provides a NativeSqliteDriver similarly

Using it from each app

  • Android — call initKoin() in your Application class and inject as usual.
  • iOS — call the shared initKoin() once at app start; Swift then asks the shared module for what it needs.

Common mistakes

  • Forgetting to provide the platform database driver (queries crash at runtime).
  • Building the dependency graph by hand instead of using DI.
  • Registering a screen-scoped object as a single when it should be a factory.
Summary: Use SQLDelight for a shared, type-safe database (with a per-platform driver) and Koin to assemble your API, repositories and ViewModels across platforms. DI keeps the shared layer clean and testable.