Skip to main content
Android Development with Kotlin
CHAPTER 21 Beginner

Local Databases with Room (SQLite)

Updated: May 16, 2026
35 min read

# CHAPTER 21

Local Databases with Room (SQLite)

1. Introduction

In Chapter 20, we downloaded user data from the internet. However, mobile devices frequently lose connection—when users step into an elevator, ride the subway, or activate airplane mode. If your app relies *only* on the internet, it becomes completely useless offline. Professional applications download data once and cache it locally on the phone's hard drive. Historically, Android developers wrote raw, error-prone SQLite queries to achieve this. Today, Google provides the Room Persistence Library. Room provides an abstraction layer over SQLite, turning complex database tables into simple Kotlin Objects and validating your SQL queries at compile time. In this chapter, we will master Local Databases with Room.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Explain the architectural importance of offline-first local data caching.
  • Configure Room Database dependencies within Gradle.
  • Define a database table utilizing the @Entity annotation.
  • Execute SQL queries (CRUD operations) safely utilizing a Data Access Object (@Dao).
  • Construct the central @Database class to manage connections.
  • Execute database transactions securely utilizing Coroutines.

3. Setting Up Room in Gradle

Room requires several components, including the Kotlin Symbol Processing (KSP) plugin to generate the complex background code for you. First, add the KSP plugin to your project-level build.gradle.kts: *(Note: Setting up KSP can vary slightly depending on your Android Studio version. Follow the official documentation if you encounter sync errors!)*

Add these to your build.gradle.kts (Module :app):

kotlin
1234
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
implementation("androidx.room:room-ktx:$room_version") // For Coroutines support!
ksp("androidx.room:room-compiler:$room_version") // The code generator

4. Step 1: The @Entity (The Table)

A database needs a Table. In Room, a Table is simply a Kotlin Data Class annotated with @Entity.
kotlin
12345678910111213141516
import androidx.room.Entity
import androidx.room.PrimaryKey

// 1. Tell Room this class represents a SQLite Table
@Entity(tableName = "users_table")
data class UserEntity(
    
    // 2. Every table needs a unique Primary Key! 'autoGenerate = true' counts up 1, 2, 3...
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    
    // 3. The columns of the table
    val name: String,
    val email: String,
    val age: Int
)

5. Step 2: The @Dao (The Queries)

The DAO (Data Access Object) is an Interface where you define *how* you want to interact with the table. You don't write the complex connection code; Room reads your annotations and generates the code for you! Notice the suspend keyword! Database operations are slow and must be executed on Coroutine background threads.
kotlin
1234567891011121314151617181920212223
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface UserDao {

    // 1. INSERT DATA
    // If we try to insert a User that already exists, REPLACE it!
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: UserEntity)

    // 2. READ DATA
    // Room verifies your SQL syntax at compile time!
    @Query("SELECT * FROM users_table ORDER BY id ASC")
    fun getAllUsers(): LiveData<List<UserEntity>> // Returns LiveData! Automatically updates UI when DB changes!

    // 3. DELETE DATA
    @Query("DELETE FROM users_table")
    suspend fun deleteAllUsers()
}

*Magic Feature:* By returning LiveData from a DAO query, Room automatically observes the database. If you insert a new user, Room instantly pushes the updated list to the Activity without you writing a second query!

6. Step 3: The @Database (The Core Engine)

Finally, we must construct the actual database that holds our Entities and DAOs. We build this as an abstract class. We also use a Singleton pattern to ensure we don't accidentally open 5 separate connections to the database, which causes severe memory leaks.
kotlin
1234567891011121314151617181920212223242526272829
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

// Tell Room which entities belong in this database, and set a version number!
@Database(entities = [UserEntity::class], version = 1, exportSchema = false)
abstract class UserDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {
        @Volatile // Ensures the instance is immediately visible to all threads
        private var INSTANCE: UserDatabase? = null

        // The Singleton Pattern
        fun getDatabase(context: Context): UserDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    UserDatabase::class.java,
                    "user_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

7. Step 4: Using the Database in the ViewModel

Just like Retrofit, we NEVER run database queries on the Main Thread. We inject our DAO into the ViewModel and execute the inserts inside viewModelScope.launch(Dispatchers.IO).
kotlin
123456789101112131415161718192021222324252627
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

// We use AndroidViewModel because Room needs a Context to build the database!
class UserViewModel(application: Application) : AndroidViewModel(application) {

    val allUsers: LiveData<List<UserEntity>>
    private val dao: UserDao

    init {
        val database = UserDatabase.getDatabase(application)
        dao = database.userDao()
        // Room automatically runs LiveData queries on a background thread!
        allUsers = dao.getAllUsers()
    }

    fun insert(user: UserEntity) {
        // We MUST run inserts/deletes on the IO thread!
        viewModelScope.launch(Dispatchers.IO) {
            dao.insertUser(user)
        }
    }
}

8. Database Migrations

If you release your app with version = 1, and a month later you update UserEntity to add a val profilePicture: String column, the app will instantly crash for all existing users! SQLite databases cannot magically change their structure without instructions. You must update the @Database(version = 2) and provide a Migration Object detailing the SQL ALTER TABLE command. For beginners, if you change your Entity during development, simply uninstall the app from the emulator to wipe the old database.

9. Common Mistakes

  • Main Thread Crashes: Attempting to call dao.insertUser() directly from a Button click on the Main Thread. Room is designed to be bulletproof; if it detects you executing a synchronous query on the UI thread, it will purposefully crash the application with an IllegalStateException to prevent ANRs.
  • Forgetting suspend: If you forget to add the suspend keyword to your @Insert or @Delete functions in the DAO, Kotlin will allow you to call them on the Main Thread, triggering the crash mentioned above.

10. Best Practices

  • Single Source of Truth: A robust architecture utilizes Room as the Single Source of Truth. When the app opens, it fetches the API (Retrofit). It does NOT display the API data directly. Instead, it saves the API data into Room. The UI *only* observes the Room Database. This guarantees seamless offline functionality.

11. Exercises

  1. 1. Create a NoteEntity data class with an auto-generating id (Int), title (String), and content (String).
  1. 2. Create a NoteDao interface. Write an @Insert function, and an @Query function that selects all notes ordered by title alphabetically.

12. Coding Challenges

Challenge: Implement a Search feature. Inside your DAO, write a custom @Query that utilizes the SQL LIKE operator to search the database for a specific string. *(Hint: @Query("SELECT * FROM users_table WHERE name LIKE :searchQuery") and pass searchQuery: String into the function parameters).*

13. MCQ Quiz with Answers

Question 1

What is the structural purpose of the @Entity annotation within the Android Room Persistence Library?

Question 2

Why does the Room architecture aggressively throw an IllegalStateException if a developer attempts to execute a DAO @Insert operation without utilizing Dispatchers.IO or an asynchronous background thread?

14. Interview Questions

  • Q: Explain the structural mechanics of the Room DAO returning LiveData. How does this specific implementation facilitate reactive UI synchronization without manual database polling?
  • Q: Describe the catastrophic consequences of incrementing a Room Database version without providing a corresponding Migration object. How does the concept of Schema integrity apply here?
  • Q: Contrast the persistence capabilities of SharedPreferences/DataStore versus a Room Database. When is it architecturally inappropriate to utilize Room?

15. FAQs

Q: Can Room store complex objects, like a List<String> inside my Entity? A: No! SQLite can only store primitives (Int, String, Boolean). If you try to store a custom object or a List, Room will throw an error. You must utilize a Room TypeConverter to convert your List into a JSON String before saving, and back into a List when reading.

16. Summary

In Chapter 21, we secured our application's offline capabilities. We transitioned from volatile network reliance to permanent local persistence utilizing the Room Database. We constructed SQLite Tables via Kotlin @Entity Data Classes, automated complex query execution using DAOs, and engineered a robust Database Singleton Engine. By executing operations asynchronously and observing the database reactively via LiveData, we laid the foundation for enterprise-grade, offline-first architectures.

17. Next Chapter Recommendation

We now have ViewModels, Coroutines, Retrofit, and Room. However, our codebase is becoming chaotic. ViewModels are doing too much work deciding whether to fetch from the API or the Database. We need an architectural governor to orchestrate data flow. Proceed to Chapter 22: Clean Architecture and Repositories.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·