1. Before you begin
Prerequisites
- Basic understanding of Kotlin Multiplatform.
- Experience with Kotlin.
- Basic understanding of Swift syntax.
- Xcode and iOS simulator installed.
What you need
- The latest stable version of Android Studio.
- A Mac machine with a macOS system.
- Xcode 16.1 and iPhone simulator with iOS 16.0 or higher.
What you learn
- How to share a Room Database between an Android app and an iOS app.
2. Get set up
To get started, follow these steps:
- Clone the GitHub repository with the following terminal command:
$ git clone https://github.com/android/codelab-android-kmp.git
Alternatively, you can download the repository as a zip file:
- In Android Studio, open the
migrate-roomproject, which contains the following branches:
main: Contains the starter code for this project, where you make changes to complete the codelab.end: Contains the solution code for this codelab.
We recommend that you begin with the main branch and follow the codelab step by step at your own pace.
- If you want to see the solution code, run this command:
$ git clone -b end https://github.com/android/codelab-android-kmp.git
Alternatively, you can download the solution code:
3. Understand the sample app
This tutorial consists of the Fruitties sample application built in native frameworks (Jetpack Compose on Android, SwiftUi on iOS).
The Fruitties app offers two main features:
- A list of Fruit items, each with a button to add the item to a Cart.
- A Cart displayed at the top, showing how many fruits have been added and their quantities.

Android app architecture
The Android app follows the official Android architecture guidelines to maintain a clear and modular structure.

iOS app architecture

KMP Shared Module
This project has already been set up with a shared module for KMP, although it is currently empty. If your project hasn't been set up with a shared module yet, start with the Getting Started with Kotlin Multiplatform codelab.
4. Prepare the Room database for KMP integration
Before you move the Room database code from the Fruitties Android app to the shared module, you need to ensure that the app is compatible with Kotlin Multiplatform (KMP) Room APIs. This section walks you through that process.
One key update is using a SQLite driver compatible with both Android and iOS. To support Room database functionality across multiple platforms, you can use the BundledSQLiteDriver. This driver bundles SQLite directly into the application, making it suitable for multiplatform use in Kotlin. For detailed guidance, refer to the Room KMP migration guide.
Update dependencies
First, add the room-runtime and sqlite-bundled dependencies to the libs.versions.toml file:
# Add libraries
[libraries]
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" }
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "androidx-sqlite" }
Next, update the build.gradle.kts of the :androidApp module to use these dependencies, and remove use of libs.androidx.room.ktx:
// Add
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
// Remove
implementation(libs.androidx.room.ktx)
Now, sync the project in Android Studio.
Modify the database module for BundledSQLiteDriver
Next, modify the database creation logic in the Android app to use the BundledSQLiteDriver, making it compatible with KMP while maintaining functionality on Android.
- Open the
DatabaseModule.ktfile located atandroidApp/src/main/kotlin/com/example/fruitties/kmptutorial/android/di/DatabaseModule.kt - Update the
providesAppDatabasemethod as in the following snippet:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
@Module
@InstallIn(SingletonComponent::class)
internal object DatabaseModule {
...
@Provides
@Singleton
fun providesAppDatabase(@ApplicationContext context: Context): AppDatabase {
val dbFile = context.getDatabasePath("sharedfruits.db")
return Room.databaseBuilder<AppDatabase>(context, dbFile.absolutePath)
.setDriver(BundledSQLiteDriver())
.build()
}
Build and run the Android app
Now that you switched the Native SQLite driver to the bundled one, let's verify the app builds and everything works correctly before you migrate the database to the :shared module.
5. Move database code to the :shared module
In this step, we'll transfer the Room database setup from the Android app to the :shared module, allowing the database to be accessible by both Android and iOS.
Update the build.gradle.kts configuration of the :shared module
Start by updating the build.gradle.kts of the :shared module to use Room multiplatform dependencies.
- Add the KSP and Room plugins:
plugins {
...
// TODO add KSP + ROOM plugins
alias(libs.plugins.ksp)
alias(libs.plugins.room)
}
- Add the
room-runtimeandsqlite-bundleddependencies to thecommonMainblock:
sourceSets {
commonMain {
// TODO Add KMP dependencies here
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
}
}
- Add KSP configuration for each platform target by adding a new top level
dependenciesblock. For ease, you can just add it to the bottom of the file:
// Should be its own top level block. For convenience, add at the bottom of the file
dependencies {
add("kspAndroid", libs.androidx.room.compiler)
add("kspIosSimulatorArm64", libs.androidx.room.compiler)
add("kspIosX64", libs.androidx.room.compiler)
add("kspIosArm64", libs.androidx.room.compiler)
}
- Also at the top level, add a new block to set the
Roomschema location:
// Should be its own top level block. For convenience, add at the bottom of the file
room {
schemaDirectory("$projectDir/schemas")
}
- Gradle sync the project
Move Room schema to :shared module
Move the androidApp/schemas directory to the :shared module root folder next to the src/ folder:
From: 
To: 
Move DAOs and entities
Now that you added the necessary Gradle dependencies to the KMP shared module, you need to move the DAOs and entities from the :androidApp module to the :shared module.
This will involve moving files to their respective locations in the commonMain source set in the :shared module.
Move Fruittie model
You can leverage the Refactor → Move functionality to switch modules without breaking the imports:
- Locate the
androidApp/src/main/kotlin/.../model/Fruittie.ktfile, right-click the file, and select Refactor → Move (or key F6):
- In the Move dialog, select the
...icon next to the Destination directory field.
- Select the commonMain source set in the Choose Destination Directory dialog and click OK. You may have to disable the Show only existing source roots checkbox.

- Click the Refactor button to move the file.
Move CartItem and CartItemWithFruittie models
For the file androidApp/.../model/CartItem.kt you need to follow these steps:
- Open the file, right-click the
CartItemclass, and select Refactor > Move. - This opens the same Move dialog, but in this case you also check the box for the
CartItemWithFruittiemember.
Continue by selecting the ...icon and selecting thecommonMainsource set just like you did for theFruittie.ktfile.
Move DAOs and AppDatabase
Do the same steps for the following files (you can select all three at the same time):
androidApp/.../database/FruittieDao.ktandroidApp/.../database/CartDao.ktandroidApp/.../database/AppDatabase.kt
Update the shared AppDatabase to work across platforms
Now that you have the database classes moved to the :shared module, you need to adjust them to generate the required implementations on both platforms.
- Open the
/shared/src/commonMain/kotlin/com/example/fruitties/kmptutorial/android/database/AppDatabase.ktfile. - Add the following implementation of
RoomDatabaseConstructor:
import androidx.room.RoomDatabaseConstructor
// The Room compiler generates the `actual` implementations.
@Suppress("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
override fun initialize(): AppDatabase
}
- Annotate the
AppDatabaseclass with@ConstructedBy(AppDatabaseConstructor::class):
import androidx.room.ConstructedBy
@Database(
entities = [Fruittie::class, CartItem::class],
version = 1,
)
// TODO Add this line
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
...

Move database creation to :shared module
Next, you move the Android-specific Room setup from the :androidApp module to the :shared module. This is necessary because in the next step you will remove the Room dependency from the :androidApp module.
- Locate the
androidApp/.../di/DatabaseModule.ktfile. - Select the content of the
providesAppDatabasefunction, right-click, and select Refactor > Extract Function to Scope:
- Select
DatabaseModule.ktfrom the menu.
This moves the content to a global appDatabasefunction. Press Enter to confirm the function name.
- Make the function public by removing the
privatevisibility modifier. - Move the function into the
:sharedmodule by right-clicking Refactor > Move. - In the Move dialog, select the ... icon next to the Destination directory field.

- In the Choose Destination Directory dialog, select the shared >androidMain source set and select the /shared/src/androidMain/ folder, then click OK.

- Change the suffix in the To package field from
.dito.database
- Click Refactor.
Clean up unneeded code from :androidApp
At this point, you moved the Room database to the multiplatform module and none of the Room dependencies are needed in the :androidApp module, so you can remove them.
- Open the
build.gradle.ktsfile in the:androidAppmodule. - Remove the dependencies and configuration as in the following snippet:
plugins {
// TODO Remove
alias(libs.plugins.room)
}
android {
// TODO Remove
ksp {
arg("room.generateKotlin", "true")
}
dependencies {
// TODO Keep room-runtime
implementation(libs.androidx.room.runtime)
// TODO Remove
implementation(libs.androidx.sqlite.bundled)
ksp(libs.androidx.room.compiler)
}
// TODO Remove
room {
schemaDirectory("$projectDir/schemas")
}
- Gradle sync the project.
Build and run the Android app
Run the Fruitties Android app to ensure that the app runs properly and now uses the database from the :shared module. If you had cart items added before, then you should see the same items at this point as well even though the Room database is now located in the :shared module.
6. Prepare Room for use on iOS
To further prepare the Room database for the iOS platform, you need to set up some supporting code in the :shared module for use in the next step.
Enable database creation for iOS app
The first thing to do is to add an iOS-specific database builder.
- Add a new file in the
:sharedmodule in theiosMainsource set namedAppDatabase.ios.kt:
- Add the following helper functions. These functions will be used by the iOS app to get an instance of the Room database.
package com.example.fruitties.kmptutorial.shared
import androidx.room.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import com.example.fruitties.kmptutorial.android.database.AppDatabase
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.ObjCObjectVar
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.value
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSError
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask
fun getPersistentDatabase(): AppDatabase {
val dbFilePath = documentDirectory() + "/" + "fruits.db"
return Room.databaseBuilder<AppDatabase>(name = dbFilePath)
.setDriver(BundledSQLiteDriver())
.build()
}
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
private fun documentDirectory(): String {
memScoped {
val errorPtr = alloc<ObjCObjectVar<NSError?>>()
val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = errorPtr.ptr,
)
if (documentDirectory != null) {
return requireNotNull(documentDirectory.path) {
"""Couldn't determine the document directory.
URL $documentDirectory does not conform to RFC 1808.
""".trimIndent()
}
} else {
val error = errorPtr.value
val localizedDescription = error?.localizedDescription ?: "Unknown error occurred"
error("Couldn't determine document directory. Error: $localizedDescription")
}
}
}
Add "Entity" suffix to Room entities
Because you're adding wrappers for the Room entities in Swift, it's best to have Room entities' names differ from the names of the wrappers. We'll make sure that's the case by adding the Entity suffix to the Room entities using the @ObjCName annotation.
Open the Fruittie.kt file in the :shared module and add the @ObjCName annotation to the Fruittie entity. Since this annotation is experimental, you may need to add the @OptIn(ExperimentalObjC::class) annotation to the file.
Fruittie.kt
import kotlin.experimental.ExperimentalObjCName
import kotlin.native.ObjCName
@OptIn(ExperimentalObjCName::class)
@Entity(indices = [Index(value = ["id"], unique = true)])
@ObjCName("FruittieEntity")
data class Fruittie(
...
)
Then, do the same for the CartItem entity in the CartItem.kt file.
CartItem.kt
import kotlin.experimental.ExperimentalObjCName
import kotlin.native.ObjCName
@OptIn(ExperimentalObjCName::class)
@ObjCName("CartItemEntity")
@Entity(
foreignKeys = [
ForeignKey(
entity = Fruittie::class,
parentColumns = ["id"],
childColumns = ["id"],
onDelete = ForeignKey.CASCADE,
),
],
)
data class CartItem(@PrimaryKey val id: Long, val count: Int = 1)
7. Use Room in the iOS app
The iOS app is a pre-existing app that uses Core Data. In this codelab you won't worry about migrating any existing data in the database as this app is just a prototype. If you are migrating a production app to KMP, you will have to write functions to read the current Core Data database and insert those items into the Room database on first launch post-migration.
Open the Xcode project
Open the iOS project in Xcode by navigating to the /iosApp/ folder and opening Fruitties.xcodeproj in the associated app.

Remove Core Data entity classes
First, you need to remove the Core Data entity classes to make room for the entity wrappers you'll create later. Depending on the kind of data your application stores in Core Data, you may remove the Core Data entities entirely, or keep them for data migration purposes. For this tutorial you can just remove them, since you won't need to migrate any existing data.
In Xcode:
- Go to the Project Navigator.
- Go to the Resources folder.
- Open the
Fruittiesfile. - Click on and delete each entity.

To make these changes available in the code, clean and rebuild the project.
This will fail the build with the following errors, which is expected.

Creating entity wrappers
Next, we'll create entity wrappers for the Fruittie and CartItem entities to smoothly handle the API differences between Room and Core Data entities.
These wrappers will help us transition from Core Data to Room by minimizing the amount of code that needs to be updated right away. You should aim to replace these wrappers with direct access to the Room entities in the future.
For now, we'll instead create a wrapper for the FruittieEntity class, giving it optional properties.
Create FruittieEntity wrapper
- Create a new Swift file in the
Sources/Repositorydirectory by right-clicking the directory name and selecting New File from Template...

- Name it
Fruittieand ensure that only the Fruitties target is selected, not the test target.
- Add the following code to the new Fruittie file:
import sharedKit
struct Fruittie: Hashable {
let entity: FruittieEntity
var id: Int64 {
entity.id
}
var name: String? {
entity.name
}
var fullName: String? {
entity.fullName
}
}
The Fruittie struct wraps the FruittieEntity class, making the properties optional and passing through the entity's properties. Additionally we make the Fruittie struct conform to the Hashable protocol, so it can be used in SwiftUI's ForEach view.
Create CartItemEntity wrapper
Next, create a similar wrapper for the CartItemEntity class.
Create a new Swift file called CartItem.swift in the Sources/Repository directory.
import sharedKit
struct CartItem: Hashable {
let entity: CartItemWithFruittie
let fruittie: Fruittie?
var id: Int64 {
entity.cartItem.id
}
var count: Int64 {
Int64(entity.cartItem.count)
}
init(entity: CartItemWithFruittie) {
self.entity = entity
self.fruittie = Fruittie(entity: entity.fruittie)
}
}
Since the original Core Data CartItem class had a Fruittie property, we have included a Fruittie property in the CartItem struct as well. While the CartItem class doesn't have any properties that are optional, the count property has a different type in the Room entity.
Update repositories
Now that the entity wrappers are in place, you need to update the DefaultCartRepository and DefaultFruittieRepository to use Room instead of Core Data.
Update DefaultCartRepository
Let's begin with the DefaultCartRepository class as it's the simpler of the two.
Open the CartRepository.swift file in the Sources/Repository directory.
- First, replace the
CoreDataimport withsharedKit:
import sharedKit
- Then remove the
NSManagedObjectContextproperty and replace it with aCartDaoproperty:
// Remove
private let managedObjectContext: NSManagedObjectContext
// Replace with
private let cartDao: any CartDao
- Update the
initconstructor to initialise the newcartDaoproperty:
init(cartDao: any CartDao) {
self.cartDao = cartDao
}
- Next, update the
addToCartmethod. This method needed to pull existing cart items from Core Data, but our Room implementation doesn't require this. Instead it will insert a new item or increment the count of an existing cart item.
func addToCart(fruittie: Fruittie) async throws {
try await cartDao.insertOrIncreaseCount(fruittie: fruittie.entity)
}
- Lastly, update the
getCartItems()method. This method will call thegetAll()method on theCartDaoand map theCartItemWithFruittieentities to ourCartItemwrappers.
func getCartItems() -> AsyncStream<[CartItem]> {
return cartDao.getAll().map { entities in
entities.map(CartItem.init(entity:))
}.eraseToStream()
}
Update DefaultFruittieRepository
To migrate the DefaultFruittieRepository class, we apply similar changes as we did for the DefaultCartRepository.
Update the FruittieRepository file to the following:
import ConcurrencyExtras
import sharedKit
protocol FruittieRepository {
func getData() -> AsyncStream<[Fruittie]>
}
class DefaultFruittieRepository: FruittieRepository {
private let fruittieDao: any FruittieDao
private let api: FruittieApi
init(fruittieDao: any FruittieDao, api: FruittieApi) {
self.fruittieDao = fruittieDao
self.api = api
}
func getData() -> AsyncStream<[Fruittie]> {
let dao = fruittieDao
Task {
let isEmpty = try await dao.count() == 0
if isEmpty {
let response = try await api.getData(pageNumber: 0)
let fruitties = response.feed.map {
FruittieEntity(
id: 0,
name: $0.name,
fullName: $0.fullName,
calories: ""
)
}
_ = try await dao.insert(fruitties: fruitties)
}
}
return dao.getAll().map { entities in
entities.map(Fruittie.init(entity:))
}.eraseToStream()
}
}
Replace @FetchRequest property wrappers
We also need to replace the @FetchRequest property wrappers in the SwiftUI views. The @FetchRequest property wrapper is used to fetch data from Core Data and observe changes, so we can't use it with Room entities. Instead, let's use the UIModel to access the data from the repositories.
- Open the
CartViewin theSources/UI/CartView.swiftfile. - Replace the implementation with the following:
import SwiftUI
struct CartView : View {
@State
private var expanded = false
@ObservedObject
private(set) var uiModel: ContentViewModel
var body: some View {
if (uiModel.cartItems.isEmpty) {
Text("Cart is empty, add some items").padding()
} else {
HStack {
Text("Cart has \(uiModel.cartItems.count) items (\(uiModel.cartItems.reduce(0) { $0 + $1.count }))")
.padding()
Spacer()
Button {
expanded.toggle()
} label: {
if (expanded) {
Text("collapse")
} else {
Text("expand")
}
}
.padding()
}
if (expanded) {
VStack {
ForEach(uiModel.cartItems, id: \.self) { item in
Text("\(item.fruittie!.name!): \(item.count)")
}
}
}
}
}
}
Update the ContentView
Update the ContentView in the Sources/View/ContentView.swift file to pass the FruittieUIModel to the CartView.
CartView(uiModel: uiModel)
Updating DataController
The DataController class in the iOS application is responsible for setting up the Core Data stack. Since we're moving away from Core Data, we need to update the DataController to initialize the Room database instead.
- Open the
DataController.swiftfile inSources/Database. - Add the
sharedKitimport. - Remove the
CoreDataimport. - Instantiate the Room database in the
DataControllerclass. - Lastly, remove the
loadPersistentStoresmethod call from theDataControllerinitializer.
The final class should look like this:
import Combine
import sharedKit
class DataController: ObservableObject {
let database = getPersistentDatabase()
init() {}
}
Updating dependency injection
The AppContainer class in the iOS application is responsible for initializing the dependency graph. Since we updated the repositories to use Room instead of Core Data, we need to update the AppContainer to pass Room DAOs to the repositories.
- Open the
AppContainer.swiftin theSources/DIfolder. - Add the
sharedKitimport. - Remove the
managedObjectContextproperty from theAppContainerclass. - Change the
DefaultFruittieRepositoryandDefaultCartRepositoryinitializations by passing in Room DAOs from theAppDatabaseinstance provided by theDataController.
When you're done, the class will look like this:
import Combine
import Foundation
import sharedKit
class AppContainer: ObservableObject {
let dataController: DataController
let api: FruittieApi
let fruittieRepository: FruittieRepository
let cartRepository: CartRepository
init() {
dataController = DataController()
api = FruittieNetworkApi(
apiUrl: URL(
string:
"https://android.github.io/kotlin-multiplatform-samples/fruitties-api"
)!)
fruittieRepository = DefaultFruittieRepository(
fruittieDao: dataController.database.fruittieDao(),
api: api
)
cartRepository = DefaultCartRepository(
cartDao: dataController.database.cartDao()
)
}
}
Finally, update FruittiesApp in main.swift to remove the managedObjectContext:
struct FruittiesApp: App {
@StateObject
private var appContainer = AppContainer()
var body: some Scene {
WindowGroup {
ContentView(appContainer: appContainer)
}
}
}
Build and run the iOS app
Finally, once you build and run the app by pressing ⌘R, the app should start with the database migrated from Core Data to Room.

8. Congratulations
Congratulations! You have successfully migrated standalone Android and iOS apps to a shared data layer using Room KMP.
For reference, here is the comparison of app architectures to see what was achieved:
Before
Android | iOS |
|
|
Post Migration Architecture

Learn more
- Learn which other Jetpack libraries support KMP.
- Read the Room KMP documentation.
- Read the SQLite KMP documentation.
- Check the official Kotlin Multiplatform documentation.