Database & Dependency Injection
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
singlewhen it should be afactory.
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.