Gone are the days of SQLiteOpenHelper. We live in a new era of Android Development dominated by Android Architecture Components.

One of which is the Room Persistence Library: a library that provides an abstraction layer of SQLite for more robust access database access with sexy code that looks like:

@Dao
interface TodoDao {

    @Query("SELECT * FROM todo_lists")
    fun getAll(): LiveData<List<Todo>>

    @Query("SELECT * FROM todo_lists WHERE id=:id")
    fun loadSingleById(id: Int): LiveData<Todo>

    @Insert
    suspend fun insert(todo: Todo)

    @Update
    suspend fun update(todo: Todo)

    @Delete
    suspend fun delete(todo: Todo)

}

The 3 Major Room Components

  1. Database: A class that contains the database holder and serves as the main access point for the underlying connection to your app’s persisted, relational data. This class also holds the other 2 components.
  2. Entity: A data class that also represents a table in your database
  3. Dao (Database Access Object): An interface that contains all your queries, inserts, deletes, and other database access methods.

Once you have your 3 components, accessing your database becomes as simple as calling the methods in your Dao which ideally you have in your repository class (assuming you’ve opted for MVVM).

Import the Dependencies

We’ll be developing with Room using Kotlin and Coroutines. We’re in 2020 fam! Add these to your app/build.gradle file.

def room_version = "2.2.4"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"

Since we’re using kapt here, don’t forget to apply plugin: 'kotlin-kapt' at the top of your file.

If you haven’t already guessed it, we’re going to make one of the coolest and definitely not overused kinds of apps you can ever make when learning something: a Todo List! But not just any todo list, a Room Todo List.

Create the Entity

@Entity(tableName = "todo_lists")
data class Todo(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "text") val text: String
)

Every one of these components is defined by their annotations, the first of which is the @Entity.

You can either define the table name yourself, or if you don’t, it will generate one based on the name of the class.

The entity represents a table. As per basic database rules, it must have a primary key. You can also define a composite key as such:

@Entity(primaryKeys = {"column1","column2","column3"})

As for the rest of the fields, the @ColumnInfo annotation is an optional one here. You’d use it if you want the name of the column to be different from the name of the variable so it’s completely useless in the above example other than for display purposes.

Entity fields have a number of different annotations. If you want to see more of them, check out the official docs on Room.

Create the Dao

@Dao
interface TodoDao {

    @Query("SELECT * FROM todo_lists")
    fun getAll(): LiveData<List<Todo>>

    @Query("SELECT * FROM todo_lists WHERE id=:id")
    fun loadSingleById(id: Int): LiveData<Todo>

    @Insert
    suspend fun insert(todo: Todo)

    @Update
    suspend fun update(todo: Todo)

    @Delete
    suspend fun delete(todo: Todo)

}

The Dao must be an interface or abstract class. It contains all the methods you use for accessing your database.

What’s great about this is that the update, and delete functions here work automatically based on the primary key in the entity you pass in to them.

Another amazing thing here is that Room works naturally with LiveData and Coroutines. Notice the return types of the queries, and the fact that the other functions are suspend functions. Performing an insert, update, or delete will reflect almost instantaneously in any observers observing the LiveData returned by the queries.

Create the Database

@Database(entities = [Todo::class], version = 1, exportSchema = false)
abstract class TodoDatabase : RoomDatabase() {

    abstract fun todoDao(): TodoDao

    companion object {

        @Volatile
        private var INSTANCE: TodoDatabase? = null

        fun getDatabase(context: Context): TodoDatabase {

            val tempInstance = INSTANCE
            if (tempInstance != null)
                return tempInstance

            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    TodoDatabase::class.java,
                    "todo_lists"
                ).build()

                INSTANCE = instance
                return instance
            }
        }

    }

}

Ok, I know this looks complicated, but it’s really not. Let me break it down.

The Database class has to satisfy 3 conditions:

  1. Be an abstract class that extends RoomDatabase
  2. Include a list entities (at least one) within the @Database annotation
  3. Contain at least one abstract class with no parameters that returns a Dao

The rest of this class is simply implementing the Singleton pattern. Database objects are expensive so we don’t want to have more than one instance of it running in our app at the same time.

Accessing the Database within the MVVM

My repository takes in a Dao as a parameter, and contains only LiveData and suspend functions.

class TodoRepository(private val todoDao: TodoDao) {

    val allTodos: LiveData<List<Todo>> = todoDao.getAll()

    suspend fun insert(todo: Todo) {
        todoDao.insert(todo)
    }

    suspend fun update(todo: Todo, newText: String) {
        todoDao.update(todo.withNewText(newText))
    }

    suspend fun delete(todo: Todo) {
        todoDao.delete(todo)
    }

}

For the ViewModel, since we need the Application context to access the Singleton this way, I’m extending the AndroidViewModel here.

class MainViewModel(application: Application): AndroidViewModel(application)

Then you can instantiate your Dao and Repository like so.

private val repository: TodoRepository

val allTodos: LiveData<List<Todo>>

init {
    val todoDao = TodoDatabase.getDatabase(application).todoDao()
    repository = TodoRepository(todoDao)
    allTodos = repository.allTodos
}

This way, our ViewModel is already observing the LiveData returned by the queries. Now all that’s left is for us to call the suspend functions to make changes to our database.

fun insert(todo: Todo) = viewModelScope.launch {
    repository.insert(todo)
}

Notice that I’m using viewModelScope here. If you get an error that Android Studio can’t resolve it, you need the dependency for it.

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha01'

And that about wraps it up.

Get the Source Code

Room is easily the definitive way for using SQLite in Android. It’s clean, it’s robust, it’s beautiful.

Get the complete source code of RoomTodoList if you want to see how I handle updates, deletes, and just the whole MVVM flow in general.

Newsletter

Subscribe to the Newsletter