Room Database with Kotlin
Room is a persistence library that provides an abstraction layer over SQLite. Let’s explore how to implement Room Database in your Kotlin Android application.
Project Setup
Add Dependencies
// build.gradle.kts
dependencies {
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion")
}
Entity Definition
Basic Entity
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val email: String,
val age: Int
)
Entity with Relations
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String
)
@Entity(tableName = "posts")
data class Post(
@PrimaryKey val id: Int,
val userId: Int,
val title: String,
val content: String
)
data class UserWithPosts(
@Embedded val user: User,
@Relation(
parentColumn = "id",
entityColumn = "userId"
)
val posts: List<Post>
)
DAO (Data Access Object)
Basic DAO
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
}
Complex Queries
@Dao
interface UserDao {
@Query("""
SELECT * FROM users
WHERE age >= :minAge
AND name LIKE '%' || :searchQuery || '%'
ORDER BY name ASC
""")
fun searchUsers(minAge: Int, searchQuery: String): Flow<List<User>>
@Query("""
SELECT u.*, COUNT(p.id) as postCount
FROM users u
LEFT JOIN posts p ON u.id = p.userId
GROUP BY u.id
""")
fun getUsersWithPostCount(): Flow<List<UserWithPostCount>>
}
Database Setup
Database Class
@Database(
entities = [User::class, Post::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
Repository Pattern
Repository Implementation
class UserRepository @Inject constructor(
private val userDao: UserDao
) {
fun getAllUsers(): Flow<List<User>> = userDao.getAllUsers()
suspend fun getUserById(id: Int): User? = userDao.getUserById(id)
suspend fun insertUser(user: User) = userDao.insertUser(user)
suspend fun updateUser(user: User) = userDao.updateUser(user)
suspend fun deleteUser(user: User) = userDao.deleteUser(user)
}
Type Converters
Custom Type Converters
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
@TypeConverter
fun fromString(value: String): List<String> {
return value.split(",").map { it.trim() }
}
@TypeConverter
fun toString(list: List<String>): String {
return list.joinToString(",")
}
}
Migration
Database Migration
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
ALTER TABLE users
ADD COLUMN phoneNumber TEXT
""")
}
}
@Database(
entities = [User::class],
version = 2,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
companion object {
fun getInstance(context: Context): AppDatabase {
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_1_2)
.build()
}
}
}
Testing
Database Testing
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java
).build()
userDao = database.userDao()
}
@Test
fun insertAndGetUser() = runBlocking {
val user = User(name = "John", email = "john@example.com", age = 25)
userDao.insertUser(user)
val loadedUser = userDao.getUserById(user.id)
assertThat(loadedUser?.name, equalTo(user.name))
}
@After
fun closeDb() {
database.close()
}
}
Best Practices
Coroutines Support
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
@Transaction
suspend fun insertUserWithPosts(user: User, posts: List<Post>) {
insertUser(user)
posts.forEach { post ->
insertPost(post)
}
}
}
Error Handling
class UserRepository @Inject constructor(
private val userDao: UserDao
) {
suspend fun getUserById(id: Int): Result<User> {
return try {
val user = userDao.getUserById(id)
if (user != null) {
Result.success(user)
} else {
Result.failure(Exception("User not found"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
Conclusion
Room Database helps you:
- Store data locally
- Handle complex queries
- Manage database migrations
- Test database operations
Remember:
- Use coroutines for async operations
- Implement proper error handling
- Follow repository pattern
- Test your database operations
Stay tuned for our next post about Working with Retrofit in Kotlin!