Room is Android's official SQLite abstraction library. It provides compile-time SQL verification, reactive queries via Flow, and a clean annotation-driven API. Understanding entities, DAOs, and type converters is the foundation of any Room-based data layer.
Entities: Mapping Tables
@Entity(
tableName = "articles",
indices = [
Index(value = ["author_id"]), // for fast author queries
Index(value = ["title"], unique = true) // no duplicate titles
],
foreignKeys = [
ForeignKey(
entity = AuthorEntity::class,
parentColumns = ["id"],
childColumns = ["author_id"],
onDelete = ForeignKey.CASCADE // delete articles when author deleted
)
]
)
data class ArticleEntity(
@PrimaryKey val id: String,
val title: String,
val body: String,
@ColumnInfo(name = "author_id") val authorId: String,
@ColumnInfo(name = "published_at") val publishedAt: Long,
@ColumnInfo(name = "cached_at") val cachedAt: Long = System.currentTimeMillis()
)
DAOs: Typed Query Methods
@Dao
interface ArticleDao {
// Suspend functions for one-shot operations
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(article: ArticleEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(articles: List<ArticleEntity>)
@Update
suspend fun update(article: ArticleEntity)
@Delete
suspend fun delete(article: ArticleEntity)
@Query("DELETE FROM articles WHERE id = :id")
suspend fun deleteById(id: String)
@Query("DELETE FROM articles")
suspend fun deleteAll()
// Flow for reactive queries — emits on every table change
@Query("SELECT * FROM articles ORDER BY published_at DESC")
fun getAllAsFlow(): Flow<List<ArticleEntity>>
@Query("SELECT * FROM articles WHERE id = :id")
fun getByIdAsFlow(id: String): Flow<ArticleEntity?>
// Suspend for one-shot reads
@Query("SELECT * FROM articles WHERE id = :id")
suspend fun getById(id: String): ArticleEntity?
// JOIN query — Room maps result to a POJO
@Query("""
SELECT a.*, u.name as author_name, u.avatar_url as author_avatar
FROM articles a
INNER JOIN authors u ON a.author_id = u.id
WHERE a.id = :id
""")
suspend fun getArticleWithAuthor(id: String): ArticleWithAuthor?
// Pagination with Paging 3
@Query("SELECT * FROM articles ORDER BY published_at DESC")
fun pagingSource(): PagingSource<Int, ArticleEntity>
}
data class ArticleWithAuthor(
@Embedded val article: ArticleEntity,
@ColumnInfo(name = "author_name") val authorName: String,
@ColumnInfo(name = "author_avatar") val authorAvatar: String
)
Type Converters
Room stores only primitives and Strings natively. For custom types, provide converters:
class Converters {
// List<String> ↔ JSON String
@TypeConverter
fun fromStringList(value: List<String>): String = Json.encodeToString(value)
@TypeConverter
fun toStringList(value: String): List<String> = Json.decodeFromString(value)
// Enum ↔ String
@TypeConverter
fun fromStatus(status: ArticleStatus): String = status.name
@TypeConverter
fun toStatus(value: String): ArticleStatus = ArticleStatus.valueOf(value)
// Custom data class ↔ JSON
@TypeConverter
fun fromMetadata(meta: ArticleMetadata?): String? =
meta?.let { Json.encodeToString(it) }
@TypeConverter
fun toMetadata(value: String?): ArticleMetadata? =
value?.let { Json.decodeFromString(it) }
}
enum class ArticleStatus { DRAFT, PUBLISHED, ARCHIVED }
@Serializable
data class ArticleMetadata(val wordCount: Int, val readTimeMinutes: Int)
Register converters on the database:
@Database(entities = [ArticleEntity::class, AuthorEntity::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun articleDao(): ArticleDao
abstract fun authorDao(): AuthorDao
companion object {
@Volatile private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase = INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app.db"
).build().also { INSTANCE = it }
}
}
}
Relationships: @Relation
data class AuthorWithArticles(
@Embedded val author: AuthorEntity,
@Relation(
parentColumn = "id",
entityColumn = "author_id"
)
val articles: List<ArticleEntity>
)
// In AuthorDao:
@Transaction
@Query("SELECT * FROM authors WHERE id = :authorId")
suspend fun getAuthorWithArticles(authorId: String): AuthorWithArticles?
Always use @Transaction with @Relation — Room makes two separate queries and needs a transaction to ensure consistency.
Key Takeaways
| Concept | Rule |
|---|---|
@Insert(onConflict = REPLACE) | Use for upserts in cache scenarios |
Flow returns | Use for reactive UI; Room emits on any change |
| Suspend functions | Use for one-shot operations (insert, delete) |
@TypeConverter | Required for enums, lists, custom objects |
@Relation + @Transaction | Always pair these — @Transaction is mandatory |
@ColumnInfo(name = ...) | Keep DB column names stable even when you rename Kotlin properties |