Modern Android applications rarely rely only on network APIs. Whether you’re building a note-taking app, e-commerce platform, messaging application, or offline-first solution, you need a reliable local database.
Google recommends Room, part of the Android Jetpack libraries, as the preferred way to work with SQLite databases. Room simplifies database operations, reduces boilerplate code, provides compile-time query validation, and integrates seamlessly with Kotlin Coroutines, Flow, ViewModel, and Jetpack Compose.
In this comprehensive guide, you’ll learn everything you need to know about Room, from basic concepts to advanced architecture patterns.
What is Room?
Room is an ORM (Object Relational Mapping) library built on top of SQLite.
Instead of writing hundreds of lines of SQLite boilerplate code, Room allows developers to interact with databases using Kotlin objects and annotations.
Think of Room as a bridge between your Kotlin classes and SQLite tables.
Without Room:
- Manual SQL management
- Cursor handling
- Boilerplate code
- Runtime SQL errors
With Room:
- Annotation-based development
- Automatic object mapping
- Compile-time query validation
- Easy migrations
- Coroutine support
- Flow support
Google officially recommends Room over using SQLite APIs directly for most Android applications.
Why Use Room?
Room solves many problems developers face with SQLite.
Advantages
- Less boilerplate code
- Type-safe SQL
- Compile-time query verification
- Easy database migrations
- Coroutine support
- Kotlin Flow support
- LiveData support
- Paging integration
- Works perfectly with MVVM
- Supports relationships between tables
Room Architecture
Room consists of three primary components.
UI
│
▼
Repository
│
▼
DAO
│
▼
Room Database
│
▼
SQLite
The application interacts with the DAO instead of directly communicating with SQLite.
Core Components of Room
1. Entity
An Entity represents a database table.
Example:
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val email: String
)
Each object corresponds to one row in the database.
2. DAO (Data Access Object)
DAO contains all database operations.
Example:
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("SELECT * FROM users")
fun getUsers(): Flow<List<User>>
}
Room automatically generates the implementation during compilation.
3. Database Class
The database class connects everything together.
@Database(
entities = [User::class],
version = 1
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Setting Up Room
Add dependencies.
implementation("androidx.room:room-runtime:<latest-version>")
ksp("androidx.room:room-compiler:<latest-version>")
implementation("androidx.room:room-ktx:<latest-version>")
Google recommends using KSP instead of the older annotation processor for Kotlin projects.
Creating the Database
val database = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"app_database"
).build()
Usually this is provided using Dependency Injection such as Hilt.
CRUD Operations
Insert
userDao.insert(User(name = "John", email = "john@email.com"))
Read
val users = userDao.getUsers()
Update
userDao.update(user)
Delete
userDao.delete(user)
SQL Queries in Room
Room supports full SQLite queries.
Example:
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUser(id: Int): User
Example with sorting:
@Query("SELECT * FROM users ORDER BY name ASC")
Filtering:
@Query("SELECT * FROM users WHERE age > :age")
Searching:
@Query("SELECT * FROM users WHERE name LIKE '%' || :keyword || '%'")
Database operations should never run on the Main Thread.
Room works naturally with Coroutines.
viewModelScope.launch {
repository.insert(user)
}
Using Flow
Flow automatically emits updates whenever the database changes.
@Query("SELECT * FROM users")
fun getUsers(): Flow<List<User>>
Advantages:
- Automatic updates
- Reactive UI
- Lifecycle-friendly
- Perfect for Compose
Room with MVVM
A typical architecture looks like this.
Compose UI
↓
ViewModel
↓
Repository
↓
DAO
↓
Room Database
Each layer has a single responsibility, making the application easier to maintain and test.
Room Relationships
Real applications usually have multiple tables.
Example:
User
↓
Orders
↓
Products
Room supports:
- One-to-One
- One-to-Many
- Many-to-Many
using annotations like:
- @Relation
- @Embedded
- @Junction
Transactions
Sometimes multiple operations must succeed together.
@Transaction
suspend fun transferMoney() {
}
Transactions ensure database consistency.
Indexes
Indexes improve query performance.
@Entity(
indices = [
Index("email")
]
)
Use indexes on frequently searched columns.
Foreign Keys
Room supports referential integrity.
@Entity(
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["userId"]
)
]
)
Migrations
When your schema changes, increase the database version.
Without migrations:
App crashes
With migrations:
Existing user data remains safe.
Example:
val migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE users ADD COLUMN phone TEXT"
)
}
}
Then:
Room.databaseBuilder(...)
.addMigrations(migration)
Room provides structured migration support, helping preserve user data across app updates.
Room + Jetpack Compose
Room integrates naturally with Compose.
Example:
val users by viewModel.users.collectAsState()
Whenever the database changes:
- Flow emits
- ViewModel updates
- Compose recomposes automatically
No manual refresh is needed.
Testing Room
Google recommends using an in-memory database for tests.
Room.inMemoryDatabaseBuilder(...)
Benefits:
- Fast
- No disk writes
- Easy cleanup
Performance Tips
Use Flow
Avoid repeatedly querying the database manually.
Add Indexes
Speed up searches on large tables.
Avoid Main Thread Queries
Always use Coroutines.
Return Only Needed Columns
Instead of:
SELECT *
Use:
SELECT name,email
Common Room Annotations
| Annotation | Purpose |
|---|---|
| @Entity | Creates table |
| @Dao | Data access |
| @Insert | Insert row |
| @Update | Update row |
| @Delete | Delete row |
| @Query | Custom SQL |
| @Database | Database class |
| @PrimaryKey | Primary key |
| @Relation | Table relationship |
| @Embedded | Nested object |
| @Transaction | Atomic operation |
Common Mistakes
Running queries on Main Thread
This causes ANR issues.
Forgetting Migrations
Changing schemas without migrations can crash the app or force destructive recreation.
Using SELECT *
Retrieve only the data you need.
Missing Indexes
Large datasets become slow without proper indexing.
Ignoring Repository Pattern
Accessing DAOs directly from the UI makes applications harder to maintain.
Best Practices
- Follow MVVM architecture.
- Use Repository as the single source of truth.
- Prefer Kotlin Flow over LiveData for new projects.
- Use Coroutines for background operations.
- Create migration scripts for every schema change.
- Add indexes to frequently queried columns.
- Keep entities focused and normalized.
- Write unit tests for DAOs and migrations.
- Inject the database with Hilt or another DI framework.
- Use Room as the local source of truth in offline-first apps.
Room vs SQLite
| Feature | Room | SQLite |
|---|---|---|
| Boilerplate | Minimal | High |
| Compile-time SQL validation | ✅ | ❌ |
| Coroutines | ✅ | Manual |
| Flow support | ✅ | ❌ |
| Type safety | ✅ | Limited |
| Migrations | Built-in | Manual |
| MVVM integration | Excellent | Manual |
| Learning curve | Easy | Moderate |
When Should You Use Room?
Room is an excellent choice for:
- Offline-first applications
- Notes apps
- To-do applications
- Expense trackers
- Chat applications
- E-commerce carts
- Inventory systems
- Local caching of REST API responses
- Health and fitness apps
- Any app requiring persistent structured data
Conclusion
Room has become the standard solution for local data persistence in modern Android development. By abstracting SQLite behind a clean, annotation-driven API, it reduces boilerplate, catches SQL issues at compile time, and integrates seamlessly with Jetpack components such as ViewModel, Kotlin Coroutines, Flow, Paging, and Jetpack Compose.
Whether you’re building a small utility app or a large offline-first application, mastering Room is an essential skill. By following the best practices outlined in this guide—using repositories, handling migrations carefully, leveraging Flow for reactive updates, and structuring your database effectively—you can build scalable, maintainable, and high-performance Android applications.
Frequently Asked Questions (FAQ)
Is Room better than SQLite?
Yes. Room is built on top of SQLite and offers compile-time query validation, less boilerplate, safer APIs, and better integration with modern Android architecture components.
Does Room support Kotlin Coroutines?
Yes. Room has first-class support for Kotlin Coroutines and works seamlessly with suspend functions.
Can Room work with Jetpack Compose?
Absolutely. Room integrates naturally with Kotlin Flow, making it ideal for reactive UI updates in Jetpack Compose.
Is Room suitable for large applications?
Yes. Many production-grade Android applications use Room for local persistence, especially when combined with MVVM, Repository, and Dependency Injection.
Does Room replace SQLite?
No. Room is an abstraction layer built on top of SQLite. It still stores data in an SQLite database while providing a more developer-friendly API.
Android Framework To be a Senior Android Developer