Room Database
DataStore is great for a handful of loose settings, but TrailLog's real data — the growing list of logged trails — needs something built for structured, queryable storage. Room is Jetpack's ORM (object-relational mapper) sitting on top of SQLite. Instead of writing raw SQL strings by hand and hoping they're correct, Room lets you declare your schema and queries as Kotlin code that's checked at compile time — a typo in a column name fails the build instead of crashing at runtime.
Piece One: The @Entity
An @Entity is a plain data class annotated to describe one database table. Each
property becomes a column, and one property is marked as the primary key:
Trail.kt — TrailLog's Room entity
@Entity
data class Trail(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String,
val notes: String,
val photoUri: String?,
val latitude: Double,
val longitude: Double,
val distanceKm: Double,
val date: Long
)
@Entity tells Room this class maps to a table (named Trail by default,
matching the class name). @PrimaryKey(autoGenerate = true) means Room assigns each
new row its own unique id automatically, so callers can pass 0 and never
worry about picking an id themselves.
Piece Two: The @Dao
A @Dao (Data Access Object) is an interface that declares the queries you're allowed
to run against the table — Room generates the actual implementation for you at compile time:
TrailDao.kt
@Dao
interface TrailDao {
@Insert
suspend fun insertTrail(trail: Trail)
@Query("SELECT * FROM Trail ORDER BY date DESC")
fun getAllTrails(): Flow<List<Trail>>
@Delete
suspend fun deleteTrail(trail: Trail)
}
The genuinely powerful detail here is getAllTrails() returning Flow<List
<Trail>> directly. Room watches the underlying table and automatically emits a
fresh list through that Flow the instant any row changes — insert a trail anywhere in
the app, and every screen collecting this Flow updates on its own, with no manual "refresh" call
needed anywhere.
Piece Three: The @Database
The @Database abstract class ties the entities and DAOs together into one database
definition, and is where you get an actual RoomDatabase instance from:
TrailDatabase.kt
@Database(entities = [Trail::class], version = 1)
abstract class TrailDatabase : RoomDatabase() {
abstract fun trailDao(): TrailDao
companion object {
@Volatile
private var INSTANCE: TrailDatabase? = null
fun getInstance(context: Context): TrailDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
TrailDatabase::class.java,
"trail_database"
).build().also { INSTANCE = it }
}
}
}
}
The companion object singleton pattern guarantees only one TrailDatabase
(and one underlying SQLite connection) ever exists for the app's lifetime — building a second
instance elsewhere would waste resources and risk two connections stepping on each other.
Why Room Insists on suspend and Flow
Notice every write in TrailDao is a suspend fun, and the read is a
Flow — Room deliberately does not offer a plain blocking call for either. Database
I/O reads from and writes to disk, which takes measurable time; running it directly on the main
thread would freeze TrailLog's UI while it waits, and Android will actually crash the app with an
exception if you try. Requiring suspend or Flow forces every call
through a coroutine, which Coroutines Basics covered as
the whole reason structured concurrency exists — moving work off the main thread without manual
thread juggling.
Evolving the Schema: A Brief Word on Migrations
Real apps change their schema over time — TrailLog might later add an elevationGainM
column to Trail. Bumping the version number in @Database
without a plan simply destroys the existing table by default, wiping out every trail a user has
already logged. Room's migration system lets you supply the exact SQL needed to
carry old data forward into the new schema instead. This is conceptual awareness for now — a full
migration walkthrough is beyond this lesson's scope, but knowing it exists (and that skipping it
deletes user data) is essential before shipping any schema change.