When you're building an Android app, you need to store data locally. Sure, you could use SharedPreferences for simple key-value pairs, but when your data gets complex — multiple objects, relationships, queries — you need a real database.
That's where Room comes in.
Room is Android's recommended SQLite abstraction layer. It's built on top of SQLite, which means you get a battle-tested, lightweight database that ships with every Android device. But Room adds a critical layer on top: compile-time SQL verification and Kotlin coroutines support.
In this article, I'll walk you through Room's three core components and show you the exact code patterns that AI tools like Claude Code generate when building production Android apps.
The Three Pillars of Room
Room architecture is built on three interconnected pieces:
- Entity — Your data model. Describes the shape of your data.
- DAO — Data Access Object. The methods you use to read/write data.
- Database — The Room database holder. Creates the DAOs and manages the database file.
Let's build a real example: a Note-Taking App.
1. Entity: Defining Your Data
An Entity is a Kotlin data class that represents a table in your SQLite database. Room reads the annotations and generates the SQL for you.
Here's an example Note entity:
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val content: String,
val createdAt: Long = System.currentTimeMillis(),
val updatedAt: Long = System.currentTimeMillis(),
val isPinned: Boolean = false
)
Let's break this down:
- @entity: Tells Room this class maps to a database table called "notes"
-
@PrimaryKey(autoGenerate = true): The
idfield is the primary key. SQLite auto-increments it when you insert a new note. - val id: Int = 0: The default value of 0 signals "this is a new record, assign the next ID"
-
Long timestamps: This is crucial. Store timestamps as
Long(Unix epoch milliseconds), not as Strings. It's faster in queries, avoids timezone bugs, and serializes cleanly.
Why not use Date or Instant? Because SQLite doesn't have a native date type. Storing as Long is the idiomatic approach in Android.
2. DAO: How You Access the Data
The DAO is where you define the methods to read, create, update, and delete your notes. Room generates the SQL at compile time.
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY updatedAt DESC")
fun getAllNotes(): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE isPinned = 1 ORDER BY updatedAt DESC")
fun getPinnedNotes(): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE id = :noteId")
suspend fun getNoteById(noteId: Int): Note?
@Insert
suspend fun insertNote(note: Note): Long
@Update
suspend fun updateNote(note: Note)
@Delete
suspend fun deleteNote(note: Note)
@Query("DELETE FROM notes WHERE id = :noteId")
suspend fun deleteNoteById(noteId: Int)
}
Notice three critical patterns here:
Pattern 1: Flow for Reactive Queries
fun getAllNotes(): Flow<List<Note>>
Flow is Kotlin's reactive stream. When the database changes, Flow automatically emits the new data. Your UI observes this Flow and updates automatically — no callbacks, no manual refresh. This is how modern Android apps work.
Pattern 2: Suspend Functions for Async
suspend fun getNoteById(noteId: Int): Note?
suspend fun insertNote(note: Note): Long
Suspend functions run on a background dispatcher. When you call insertNote() from a ViewModel, Room handles the threading automatically. You don't need to manually create coroutines or Thread objects.
Pattern 3: Separation of Concerns
Each DAO method does one thing:
-
getAllNotes()— get all notes, reactive -
getPinnedNotes()— get pinned notes, reactive -
getNoteById()— get a single note, one-time query -
insertNote()— create a new note -
updateNote()— modify an existing note -
deleteNote()— delete by object -
deleteNoteById()— delete by ID
This keeps your database layer clean and testable.
3. Database: The Room Holder
The Database class is where you register your Entities and DAOs. You create it once per app and keep a reference to it in your dependency injection container (or a singleton).
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context
@Database(entities = [Note::class], version = 1)
abstract class NoteDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile
private var INSTANCE: NoteDatabase? = null
fun getDatabase(context: Context): NoteDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
NoteDatabase::class.java,
"note_database"
).build()
INSTANCE = instance
instance
}
}
}
}
Key points:
-
@database: Declares this as a Room database with the
Noteentity, version 1 - abstract fun noteDao(): Room generates the implementation automatically
- @Volatile INSTANCE: Thread-safe singleton pattern. Only one database connection per app.
- synchronized(this): Prevents race conditions if multiple threads try to create the database simultaneously
How They Work Together
Here's the complete flow:
┌─────────────────────┐
│ Your UI (Screen) │ Observes Flow, calls ViewModel
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ ViewModel │ Uses Repository, launches coroutines
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Repository │ Calls DAO methods
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ NoteDao (DAO) │ Translates to SQL
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ NoteDatabase │ Manages SQLite connection
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ SQLite File │ Stored in app's private data directory
└─────────────────────┘
A ViewModel using this database looks like this:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class NoteViewModel(private val noteDao: NoteDao) : ViewModel() {
val notes = noteDao.getAllNotes()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
fun createNote(title: String, content: String) {
viewModelScope.launch {
noteDao.insertNote(Note(title = title, content = content))
}
}
fun deleteNote(note: Note) {
viewModelScope.launch {
noteDao.deleteNote(note)
}
}
}
The notes property is a StateFlow<List<Note>>. The UI observes it and automatically updates whenever the database changes. No manual refresh needed.
Why This Matters: Privacy & Control
Room stores data in a private SQLite database file. By default, this file is stored in the app's private data directory (/data/data/com.example.noteapp/databases/note_database).
This means:
- No INTERNET permission needed — Your app doesn't transmit data to a server
- No cloud dependency — Works offline, works without network
- User has control — The data stays on the device unless the user explicitly chooses to sync it
- GDPR-compliant — No third-party servers involved in data storage
Many apps add INTERNET permission "just in case" without thinking. But if you're using Room for local storage, you don't need it. That's a privacy win.
The Complete Picture
Room is the standard solution for Android data persistence because it:
- Integrates with Kotlin — Suspend functions, Flow, coroutines built in
- Compile-time safety — Invalid SQL is caught at build time, not runtime
- Minimal boilerplate — Annotations handle 90% of the work
- Production-ready — Used in billions of apps
- Privacy-first — Local storage, no network required
AI tools like Claude Code consistently generate Room databases using these exact patterns. The architecture is correct, the threading model is sound, and the code is maintainable from day one.
Next Steps
All 8 of my AI-generated Android app templates use Room with this exact pattern. They include:
- Simple utilities — Unit Converter, Countdown Timer (single entity, basic queries)
- Data-rich apps — Budget Manager, Task Manager with priorities (relationships, complex queries)
- Real-world patterns — Repository layer, ViewModel state management, Flow integration
Check them out on Gumroad. Each template is a complete, working project you can customize for your own app.
Have you built with Room before? What's your approach to database migrations or multi-entity relationships? Drop a comment — I'd love to see what you're working on.
Top comments (0)