Android Kotlin: Room Database Tutorial

Mastering Local Data Persistence

Introduction to Room Database

Room Persistence Library is a robust abstraction layer over SQLite that provides seamless access to the database on your Android device. It's part of the Android Jetpack suite of libraries and is designed to reduce the boilerplate code you write when working with SQLite databases.

Before Room, interacting with SQLite involved a lot of manual cursor management, SQL statement writing, and type conversion, which was error-prone and time-consuming. Room aims to simplify this by allowing you to define your database schema using annotations.

Why Use Room?

Core Components of Room

Room consists of three main components:

  1. Entity: Represents a table in your database.
  2. DAO (Data Access Object): Contains the methods for accessing the database.
  3. Database: The main access point to your database and serves as a container for your entities and DAOs.

1. Entity

An Entity is a class that represents a table in your database. You annotate your class with @Entity. Each property in the class corresponds to a column in the table.

By default, the table name is the same as the class name. You can specify a custom name using the tableName parameter in the @Entity annotation.


import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val name: String,
    val email: String
)
            

In this example:

2. DAO (Data Access Object)

A DAO is an interface or abstract class that defines the methods for performing database operations like inserting, deleting, updating, and querying data. You annotate your DAO with @Dao.


import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(user: User)

    @Update
    suspend fun update(user: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUserById(userId: Int): User?

    @Query("SELECT * FROM users ORDER BY name ASC")
    fun getAllUsers(): Flow>
}
            

Key annotations used here:

3. Database

The Database class is an abstract class that extends RoomDatabase. It serves as the central point for managing your database. You annotate the class with @Database, specifying your entities and their version.


import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}
            

In this class:

Setting up Room in your Project

First, add the necessary Room dependencies to your app's build.gradle (app) file:


dependencies {
    // Room components
    implementation("androidx.room:room-runtime:2.6.1") // Use the latest version
    kapt("androidx.room:room-compiler:2.6.1")      // Use the latest version
    implementation("androidx.room:room-ktx:2.6.1")  // Kotlin Extensions and Coroutines support

    // Optional: If you want to use Guava Listenable futures.
    // implementation("androidx.room:room-guava:2.6.1")
}
            

Note: Make sure you have the Kotlin Kapt plugin enabled in your project-level build.gradle file:


plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.kapt") // Add this line
}
            

Initializing and Accessing the Database

You typically initialize your Room database using a singleton pattern to ensure only one instance exists. This is often done in your Application class or a dedicated dependency injection module.

Room Database Architecture
Conceptual flow of Room Database operations.

Here’s how you might get an instance of your database:


import android.content.Context
import androidx.room.Room

object DatabaseProvider {
    @Volatile
    private var INSTANCE: AppDatabase? = null

    fun getDatabase(context: Context): AppDatabase {
        return INSTANCE ?: synchronized(this) {
            val instance = Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                "app_database" // Database file name
            )
            // .addMigrations(MIGRATION_1_2) // Add migrations here if needed
            .build()
            INSTANCE = instance
            instance
        }
    }
}
            

To use the DAO, you would get the database instance and then call the method to get the DAO:


// In your ViewModel or Repository
val database = DatabaseProvider.getDatabase(applicationContext)
val userDao = database.userDao()

// Example of inserting a user using Coroutines
lifecycleScope.launch {
    userDao.insert(User(name = "Alice", email = "alice@example.com"))
}

// Example of observing users
userDao.getAllUsers().collect { users ->
    // Update your UI with the list of users
}
            

Database Migrations

When you change your database schema (e.g., add a new table, add a column, change a column type), you need to handle database migrations. Room requires you to define migration paths when the database version changes.

For example, if you change the version from 1 to 2:


// In AppDatabase class or a separate file
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        // Example: Add a new column 'age' to the 'users' table
        database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
    }
}

// In AppDatabase initialization:
// .addMigrations(MIGRATION_1_2)
            

Conclusion

Room provides a powerful and efficient way to manage local data persistence in your Android applications. By leveraging its annotations and components, you can significantly reduce boilerplate code, improve code quality, and ensure data integrity.

This tutorial covered the fundamental concepts: Entities, DAOs, and the Database. For more advanced topics like foreign keys, indexes, complex queries, and testing, refer to the official Android documentation.

Key Takeaway: Room simplifies SQLite database operations by providing an abstract layer with compile-time checks and less boilerplate code, making Android data persistence more manageable.