Back to blog
October 30, 2025
4 min read

Room Database with Kotlin

Learn how to implement local storage in your Android app using Room Database

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!