Room Database: Robust Persistence in Android
Table of Contents
🏛️ Theory: Why Room and not pure SQLite?
SQLite is powerful but raw. Writing SQL by hand in Strings is error-prone, and mapping Cursor to Objects is tedious and repetitive.
Room is an abstraction layer (ORM - Object Relational Mapper) over SQLite that offers:
- Compile-time validation: If you miswrite your SQL query, the app won’t build.
- Coroutines/Flow integration: Simple asynchronous operations.
- Automatic mapping: From Columns to Properties.
- Managed migrations: Helps evolve the schema without losing data.
🏗️ The 3 Major Components
1. Entity (The Table)
Defines the table structure.
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: String,
@ColumnInfo(name = "full_name") val name: String,
val age: Int // Default column name is "age"
)
2. DAO (Data Access Object)
Defines operations. It’s an interface; Room generates the code.
@Dao
interface UserDao {
// 1. Reactive Read (Flow)
// Emits a new value every time the table changes.
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<UserEntity>>
// 2. Suspend Write (One-shot)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: UserEntity)
@Delete
suspend fun delete(user: UserEntity)
}
3. Database (The Access Point)
The main container. Must be a Singleton.
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
🔄 Relationships
Room doesn’t support object lists directly (because SQL doesn’t). You have two options.
Option A: TypeConverters (For simple data)
Convert a List<String> to a JSON String to save it, and vice versa when reading.
class Converters {
@TypeConverter
fun fromString(value: String): List<String> {
return Json.decodeFromString(value)
}
@TypeConverter
fun fromList(list: List<String>): String {
return Json.encodeToString(list)
}
}
Option B: @Relation (For real relational data)
If a User has many Posts.
data class UserWithPosts(
@Embedded val user: UserEntity,
@Relation(
parentColumn = "id",
entityColumn = "user_id"
)
val posts: List<PostEntity>
)
// In DAO
@Transaction // Important for consistency
@Query("SELECT * FROM users")
fun getUsersWithPosts(): Flow<List<UserWithPosts>>
⚠️ Migrations: The Production Terror
If you change your Entity (add a field) and bump the DB version without providing a migration, the app will crash for existing users (or wipe data if you use fallbackToDestructiveMigration).
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
// When building DB
Room.databaseBuilder(...)
.addMigrations(MIGRATION_1_2)
.build()
Pro Tip: Use Room’s automated migration tests to verify your migration works before releasing.
🎯 Conclusion
Room is the centerpiece of any “Offline-First” strategy. Its ability to expose Flow makes UI-Database synchronization trivial. Although it requires initial setup, the type safety and robustness it offers are worth every line of code.
You might also be interested in
ChatGPT 5.3 Codex: The New Standard for Mobile Development?
A deep dive into ChatGPT 5.3 Codex, its new dedicated app, and what it means for Android developers. Includes comparison with Gemini 3.0 Pro.
Offline-First Synchronization Patterns Powered by AI
Revolutionizing data synchronization and conflict resolution using local AI models in 2026.
Advanced KMP: UI Sharing Strategies with Compose Multiplatform 1.8
Exploring complex navigation patterns and state management across Android and iOS using Kotlin Multiplatform in 2026.