If you've ever built an Android app that needs to save data — like a to-do list, notes, or user settings — you need a local database. Room is the official Android library for this, and when you combine it with Flow, your UI updates automatically whenever data changes. No manual refreshing. No extra code. Just reactive, clean architecture.
In this guide, you'll go from zero to a working Room + Flow setup in Kotlin.
---
What is Room Database?
Room is a library built on top of SQLite. SQLite is powerful but writing raw SQL queries in Android is messy and error-prone. Room gives you:
A clean way to define your database tables using Kotlin classes
Auto-generated SQL under the hood
Compile-time checks so you catch errors before running the app
Think of it as a smart wrapper around SQLite.
---
What is Flow?
Flow is a Kotlin feature that lets you observe a stream of data over time. When combined with Room, it means: whenever your database changes, your UI gets notified automatically.
Without Flow, you'd have to manually query the database every time. With Flow, Room does that for you.
---
Setting Up Room in Your Project
Add these dependencies to your build.gradle (app):
dependencies {
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
implementation("androidx.room:room-ktx:$room_version")
kapt("androidx.room:room-compiler:$room_version")
}Also add the kapt plugin at the top of your build.gradle:
plugins {
id("kotlin-kapt")
}---
Step 1 — Create an Entity (Your Table)
An Entity is just a Kotlin data class that represents a table in your database. Each variable becomes a column.
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
)Here we're creating a notes table with three columns: id, title, and content. The id is auto-generated so you don't have to set it manually.
---
Step 2 — Create a DAO (Your Queries)
DAO stands for Data Access Object. This is where you write your database operations.
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertNote(note: Note)
@Delete
suspend fun deleteNote(note: Note)
@Query("SELECT * FROM notes ORDER BY id DESC")
fun getAllNotes(): Flow<List<Note>>
}---
Step 3 — Create the Database
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [Note::class], version = 1, exportSchema = false)
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
}
}
}
}The companion object ensures only one instance of the database exists throughout the app — this is called the Singleton pattern.
---
Step 4 — Observe Data in Your ViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class NoteViewModel(private val dao: NoteDao) : ViewModel() {
val notes = dao.getAllNotes()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun addNote(title: String, content: String) {
viewModelScope.launch {
dao.insertNote(Note(title = title, content = content))
}
}
fun deleteNote(note: Note) {
viewModelScope.launch {
dao.deleteNote(note)
}
}
}stateIn converts the Flow into a StateFlow which is safer to collect in the UI.
---
Step 5 — Collect in Your Activity or Fragment
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.notes.collect { noteList ->
adapter.submitList(noteList)
}
}
}Every time you add or delete a note, this block runs automatically with the updated list.
---
Common Mistakes Beginners Make
1. Running database operations on the main thread
2. Creating multiple database instances
Always use the Singleton pattern shown above. Multiple instances can cause data conflicts.
3. Forgetting room-ktx dependency
---
What's Next?
Add a Repository layer between DAO and ViewModel for cleaner architecture
Use @TypeConverter to store complex objects like lists or dates
Explore Room migrations for when you update your database schema
---
Summary
@Entity — defines your database table
@Dao — contains your queries
@Database — connects everything together
Flow — automatically updates UI on data change
ViewModel — holds and exposes data to the UI
Room + Flow is one of the most practical combinations in Android development. Once you understand this pattern, building offline-first apps becomes much easier.