Level Up Your KMP Project with Room Database: A Step-by-Step Guide
Hey Android devs! ?? Ready to supercharge your Kotlin Multiplatform (KMP) project with data persistence? Today, we're diving into the world of Room database, the go-to solution for storing data locally in your Android apps. We'll explore a straightforward and efficient way to integrate Room into your KMP project, step-by-step. Let's get started! ??
Why Room Rocks?
Room is an awesome persistence library that provides an abstraction layer over SQLite, making it easier to work with databases in Android. It offers:
Setting the Stage: Project Setup
Before we dive into Room, let's make sure our KMP project is ready.
Update Gradle Properties:
Open your gradle.properties file and add the following lines:
#Disable compiler daemon for Room compatibility
kotlin.native.disableCompilerDaemon = true
Why is this important? This setting ensures compatibility between Room and Kotlin/Native, preventing potential build issues.
Add Room Dependencies:
Open your libs.toml file and add the following dependencies:
[versions]
room = "2.7.0-alpha12"
ksp = "2.0.20-1.0.24"
sqlite = "2.5.0-SNAPSHOT"
[libraries]
room - runtime = {
module = "androidx.room:room-runtime",
version.ref = "room"
}
room - runtime - android = {
module = "androidx.room:room-runtime-android",
version.ref = "room"
}
room - compiler = {
module = "androidx.room:room-compiler",
version.ref = "room"
}
sqlite - bundled = {
module = "androidx.sqlite:sqlite-bundled",
version.ref = "sqlite"
}
[plugins]
ksp = {
id = "com.google.devtools.ksp",
version.ref = "ksp"
}
room = {
id = "androidx.room",
version.ref = "room"
}
Configure Gradle:
In your shared module's build.gradle.kts file, apply the KSP and Room plugins:
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.room)
}
And add the Room runtime dependencies to your commonMain source set:
kotlin {
sourceSets {
commonMain {
dependencies {
implementation(libs.room.runtime)
implementation(libs.sqlite.bundled)
}
}
}
}
Finally, configure the Room compiler and schema export:
room {
// Export Room database schemas to the specified directory
schemaDirectory("$projectDir/schemas")
}
dependencies {
// Use Kotlin Symbol Processing (KSP) for Room annotation processing
ksp(libs.room.compiler)
}
Building Blocks: Entities, DAO, and Repository
Entities: shared/src/commonMain/kotlin/.../database/entities/TestEntity.kt
@Entity(tableName = TableNames.TESTS)
data class TestEntity(
@PrimaryKey @ColumnInfo(name = ColumnNames.TEST_ID) val id: Int,
// ... other fields ...
)
Why separate entity classes? This ensures a clear separation between your domain models and database representations, allowing for flexibility and maintainability.
领英推荐
DAO (Data Access Object):
The DAO provides an interface for interacting with your database. Define functions for CRUD operations (Create, Read, Update, Delete) and custom queries.
shared/src/commonMain/kotlin/.../data/database/TestDao.kt
@Dao
interface TestDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTest(test: TestEntity)
// ... other DAO functions ...
}
Repository:
The repository abstracts data access logic and provides a clean interface for your application to interact with the data layer.
shared/src/commonMain/kotlin/.../data/TestRepository.kt
class TestRepository : KoinComponent {
private val testDao: TestDao by inject()
suspend fun insertTest(test: TestEntity) {
testDao.insertTest(test)
}
// ... other repository functions ...
}
Alternative Approach
Instead of directly injecting the DAO into the repository, you can inject the TestDatabase instance and then access the DAO through it. Example
class TestRepository(private val database: TestDatabase) {
private val testDao = database.testDao()
// ... repository functions using testDao ...
}
Why This Approach Can Be Beneficial
class TestRepository(private val database: TestDatabase) {
private val testDao = database.testDao()
suspend fun updateTestAndQuestions(test: Test, questions: List<Question>) {
database.withTransaction {
testDao.updateTest(test.toEntity())
questions.forEach { question ->
testDao.updateQuestion(question.toEntity())
}
}
}
}
Mapping Magic: Data Mappers
To bridge the gap between your domain models and database entities, create data mappers. These functions convert data between different representations.
// Convert TestEntity to domain model Test
fun TestEntity.toDomain(): Test {
return Test(id = id, topic = topic, numberOfQuestions = numberOfQuestions, age = age,
grade = grade, questions = questions.map {
it.toDomainQuestion()
} // Assuming you have a mapper for QuestionEntity
)
}
// Convert domain model Test to TestEntity
fun Test.toEntity(): TestEntity {
return TestEntity(id = id, topic = topic, numberOfQuestions = numberOfQuestions, age = age, grade = grade, questions = questions.map {
it.toEntityQuestion()
} // Assuming you have a mapper for QuestionEntity
)
}
Why mappers? Mappers ensure data consistency and maintainability by separating data transformation logic from your domain and data access code.
Wrapping Up
Congratulations! You've successfully integrated Room database into your KMP project. You've learned how to set up the project, create entities, DAO, repository, and data mappers. Now you're ready to build powerful and data-driven KMP applications.
Happy coding! ??
Note: This blog post provides a simplified overview. For more in-depth information and advanced use cases, refer to the official Android developer documentation and the linked resources.
#AndroidDev #Kotlin #KMP #RoomDatabase #DataPersistence #SQLite #AndroidArchitecture #DependencyInjection #DataLayer #Database #Entities #DAO #Repository #Mappers #BestPractices #Tutorial #KotlinCoroutines #Flow #TypeSafety #CompileTimeVerification #Performance #Maintainability #Testability