Level Up Your KMP Project with Room Database: A Step-by-Step Guide

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:

  • Type safety: Room uses Kotlin's type system to ensure data integrity.
  • Compile-time verification: Room checks your database queries at compile time, catching errors early on.
  • Convenience: Room simplifies database operations with easy-to-use annotations and APIs.
  • Performance: Room is optimized for performance, ensuring smooth data access.

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

  • Centralized Database Access: The TestDatabase class acts as a central point of access for all your DAOs. This can be helpful for managing database transactions and ensuring consistency.
  • Testability: It's easier to mock the TestDatabase class in your unit tests compared to mocking individual DAOs. This allows you to isolate your repository logic for testing.
  • Dependency Management: If you have multiple DAOs, injecting the TestDatabase into the repository simplifies dependency management. You only need to inject one dependency instead of multiple DAOs.
  • Transaction Management: If you need to perform multiple database operations within a single transaction, you can use the TestDatabase instance to begin and end transactions. Example with Transaction

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

要查看或添加评论,请登录

Anastasiya Liverant的更多文章

社区洞察

其他会员也浏览了