Implementing Koin in a Compose Multiplatform App with DataStore and Room Database

The perfect setup.
May 05, 2025
This tutorial walks you through setting up Koin for dependency injection in a Kotlin Multiplatform (KMP) project using Compose, including DataStore preferences and Room database integration. We’ll cover shared code, platform-specific configurations, and best practices.
You may be interested in this article as well:

1. Add Dependencies

In your shared (commonMain) module’s build.gradle.kts, add the following dependencies:
commonMain.dependencies { // Koin core api("io.insert-koin:koin-core:3.5.0") // Koin for Compose api("io.insert-koin:koin-androidx-compose:3.5.0") api("io.insert-koin:koin-androidx-compose-viewmodel:3.5.0") // Room implementation(libs.androidx.room.runtime) implementation(libs.sqlite.bundled) // DataStore implementation("androidx.datastore:datastore:1.1.4") implementation("androidx.datastore:datastore-preferences:1.1.4") } androidMain.dependencies { // Android-specific Koin integration implementation("io.insert-koin:koin-android:3.5.0") }

2. Define Your Database and DataStore

Room Database Setup

First, create your Room database and DAO interfaces in the shared module:
// AppDatabase.kt @Database(entities = [YourEntity::class], version = 1) @ConstructedBy(AppDatabaseConstructor::class) abstract class AppDatabase : RoomDatabase() { abstract fun yourDao(): YourDao } // The Room compiler generates the `actual` implementations. @Suppress("NO_ACTUAL_FOR_EXPECT") expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> { override fun initialize(): AppDatabase } fun getRoomDatabase( builder: RoomDatabase.Builder<AppDatabase> ): AppDatabase { return builder .fallbackToDestructiveMigrationOnDowngrade(false) .setDriver(BundledSQLiteDriver()) .setQueryCoroutineContext(Dispatchers.IO) .build() } // YourDao.kt @Dao interface YourDao { @Query("SELECT * FROM your_table") suspend fun getAll(): List<YourEntity> // Add other queries... }

DataStore Setup

Create a preferences repository to handle DataStore operations:
// AppPreferencesRepository.kt class AppPreferencesRepository(private val dataStore: DataStore<Preferences>) { private val someKey = booleanPreferencesKey("some_key") val somePreference: Flow<Boolean> = dataStore.data .map { preferences -> preferences[someKey] ?: false } suspend fun updateSomePreference(value: Boolean) { dataStore.edit { preferences -> preferences[someKey] = value } } }

3. Platform-Specific Implementations

Common Interface

Create platform-specific implementations for database and DataStore:

Android Implementation

// androidMain fun createDatabaseBuilder(ctx: Context): RoomDatabase.Builder<AppDatabase> { val appContext = ctx.applicationContext val dbFile = appContext.getDatabasePath("app.db") return Room.databaseBuilder<AppDatabase>( context = appContext, name = dbFile.absolutePath ) } fun createDataStore(context: Context): DataStore<Preferences> { return PreferenceDataStoreFactory.create( produceFile = { context.preferencesDataStoreFile("app.preferences_pb") } ) }

iOS Implementation

// iosMain import platform.Foundation.NSApplicationSupportDirectory import platform.Foundation.NSFileManager import platform.Foundation.NSSearchPathForDirectoriesInDomains import platform.Foundation.NSUserDomainMask fun createDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> { val paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true) val basePath = paths.firstOrNull() ?: error("Cannot access application support directory") return Room.databaseBuilder<AppDatabase>( name = "$basePath/database.db" ) } fun createDataStore(): DataStore<Preferences> { return PreferenceDataStoreFactory.createWithPath( produceFile = { val root = PathUtils.getPrivateFileStorageDirectory(null) "${root}/app.preferences_pb".toPath() } ) }

4. Define Your Koin Modules

Create a file Koin.kt in your commonMain to define your modules:
expect val platformModule: Module fun initKoinModules( databaseBuilder: RoomDatabase.Builder<AppDatabase>, createDataStore: () -> DataStore<Preferences> ): Array<Module> { val dataModule = module { // Database single<AppDatabase> { getRoomDatabase(databaseBuilder) } single<YourDao> { get<AppDatabase>().yourDao() } // DataStore single<DataStore<Preferences>> { createDataStore() } single<AppPreferencesRepository> { AppPreferencesRepository(get()) } // Repositories single<YourRepository> { YourRepository(get(), get()) } // ViewModels viewModel { YourViewModel(get()) } } return arrayOf(dataModule, platformModule) }

5. Platform-Specific Koin Initialization

Android

In androidMain
// KoinHelper.kt actual val platformModule = module { // Android-specific singletons/factories } fun initKoin(context: Context) { val koinModules = initKoinModules( databaseBuilder = createDatabaseBuilder(context), createDataStore = { createDataStore(context) } ) startKoin { androidContext(context) modules(*koinModules) } } // YourApplication.kt class YourApplication : Application() { override fun onCreate() { super.onCreate() initKoin(this) } }

iOS

// KoinHelper.kt actual val platformModule = module { // iOS-specific singletons/factories } fun initKoin() { val koinModules = initKoinModules( databaseBuilder = createDatabaseBuilder(), createDataStore = { createDataStore() } ) startKoin { modules(*koinModules) } }
Call it in your iOSApp.swift:
@main struct iOSApp: App { init() { KoinHelperKt.doInitKoin() } var body: some Scene { WindowGroup { ContentView() } } }

6. Using Dependencies

In ViewModels

class YourViewModel( private val repository: YourRepository, private val preferences: AppPreferencesRepository ) : ViewModel() { // Use your dependencies here }

In Compose Screens

@Composable fun YourScreen(viewModel: YourViewModel = koinViewModel()) { // Use your viewModel here }

In Repositories

class YourRepository( private val dao: YourDao, private val preferences: AppPreferencesRepository ) { // Use your dependencies here }

7. Best Practices

  1. Use single for shared instances (database, DataStore, repositories)
  2. Use viewModel for Compose view models
  3. Use factory for new instances when needed
  4. Keep platform-specific implementations in their respective source sets
  5. Use constructor injection for better testability
  6. Initialize Koin as early as possible in your application lifecycle

8. Summary

  1. Add necessary dependencies for Koin, Room, and DataStore
  2. Set up your Room database and DataStore implementations
  3. Create platform-specific implementations for database and DataStore
  4. Define your Koin modules with proper dependency injection
  5. Initialize Koin in your platform entry points
  6. Use dependency injection in your view models, repositories, and Compose screens