15 minutes read

You have already seen how to work with SQLite in Android using the SQLiteOpenHelper. This is a universal approach that can be used in any situation, but it sometimes creates a lot of boilerplate code.

To address these kinds of issues, Google provides a set of special Jetpack libraries. They are designed to make life easier by helping developers follow best practices, avoid various bugs, and achieve the desired results with fewer lines of code. Room is part of this framework. It's a high-level interface for low-level bindings to SQLite.

In this topic, you will learn how to use Room to work with databases more quickly in Android.

Benefits

Room is an ORM (Object-relational mapping) library for Android. ORM is a programming technique that makes it possible to convert data from an OOP language into the format used by a relational database and vice versa. So, ORM is the link between the database and the code. It allows us to insert objects created in the program into the database, get them, perform operations with them, and so on.

As mentioned, Room reduces the number of lines we need to write. It creates an abstraction over SQLite to make accessing the database easier, and its annotations help simplify the code.

Room performs SQL query checks at compile-time, too. This can help to pick up errors before an application is launched. For example, without these checks, if you write the wrong column name in your query using the standard SQLite API, you will get an error at run-time instead of compile-time.

Adding the library

Before we can take full advantage of Room, we have to load it through the build.gradle file:

implementation "androidx.room:room-ktx:2.4.3"
kapt "androidx.room:room-compiler:2.4.3"

We must also enable the kotlin-kapt plugin at the top of build.gradle:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}

To learn more about Gradle scripts you can visit the build.gradle files topic.

Room components

Room, DAO, Entity relations diagram

To use Room, we must implement the Database, Dao, and Entity components.

As with the SQLite topic, we will use a database that stores people's details as our example. Each person will have an id, name, and age.

It's important to know that Room does not allow you to perform operations in the main thread! Attempting to do this will result in the following error:

java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

Entity

The first step towards achieving our goal is implementing Entity. This is simply a class that reflects a database table:

@Entity(tableName = "people")
data class Person(
        @ColumnInfo(name = "name") var name: String,
        @ColumnInfo(name = "age") var age: Int,
        @PrimaryKey(autoGenerate = true) var id: Int = 0
)

We tag the Person class with the Room library's @Entity annotation. In the annotation's parameters, we specify the name of the table "people". We want to make the id property the table's primary key and use the @PrimaryKey annotation with autoGenerate = true in the parameters to make it autoincrement. Otherwise, we won't have an automatically incremented PRIMARY KEY property in the database, and we know that it must be unique. The @ColumnInfo(name = "name") annotation is used to specify the column name if we want it to be different from the property name. In our case, the names are the same, meaning that we could remove this line. The @Ignore annotation can be utilized to indicate a constructor or property that should not be used when working with the database.

Database

Next, we create an abstract class based on RoomDatabase. In the annotation parameters, we specify the version of our database and entity classes:

@Database(entities = [Person::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun getPersonDao(): PersonDao
}

If we wanted to use multiple classes to store data, we would pass them separated by commas in square brackets.

RoomDatabase objects are very expensive, so we implement access to them via the Application class. This is known as a singleton design pattern.

To refer to the application object that is responsible for the application as a whole, we must specify this class in the manifest:

<application
        android:name=".SomeApplication"

In Room.databaseBuilder we pass the application context (this because our class inherits from the Application), our abstract class itself, and the name of the SQLite database file to be created:

class SomeApplication : Application() {
    val database: AppDatabase by lazy {
        Room.databaseBuilder(
            this,
            AppDatabase::class.java,
            "database.db"
        ).allowMainThreadQueries().build()
    }
}

Don't forget that you can't usually perform Room operations in the main thread. This is done intentionally because database access operations can take much longer than the UI thread is designed for, which can cause the UI to stop responding. However, in this example, we will use the method allowMainThreadQueries() when creating a database object for the sake of simplicity. This forcibly prevents Room from checking that database operations have been brought into a separate thread.

Room will generate the required code for us if we follow the necessary instructions. We have already created the getPersonDao function that will return PersonDao and, in the next section, you will learn about Dao.

DAO

A DAO (data access object) is responsible for defining methods that allow you to interact with the database. This is an interface, and we label it @Dao.

Inside, we define various functions. For example, the first is for inserting people and the second for deleting them. We mark them with the corresponding annotations @Insert and @Delete — Room does the rest.

Room doesn't have ready-made annotations for every occasion, though. When there isn't a specific annotation, we can use @Query and pass it an SQL query. For example, deleteAll() will delete all the data in the database, getAllPeople() will return every record, and getPeopleWithAge(age: Int) will return users of a certain age:

@Dao
interface PersonDao {
    @Insert
    fun insert(vararg people: Person)

    @Delete
    fun delete(person: Person)

    @Query("DELETE FROM people")
    fun deleteAll()

    @Query("SELECT * FROM people")
    fun getAllPeople(): List<Person>

    @Query("SELECT * FROM people WHERE age = :age")
    fun getPeopleWithAge(age: Int): List<Person>

    @Query("SELECT * FROM people WHERE name LIKE :prefix || '%'")
    fun getPeopleWhereNameStartsWith(prefix: String): List<Person>

    @Query("SELECT name FROM people")
    fun getNames(): List<String>

    @Query("SELECT name as first, age as second FROM people")
    fun getPairs(): List<StringIntPair>
}

The getPeopleWithAge() and getPeopleWhereNameStartsWith() functions are of particular interest. In the function parameters, we pass the age argument, but in the @Query description, we pass :age. It's important to remember this syntax. Room simply plugs the parameter into the SQL query. Convenient!

The getNames() function demonstrates how to get a list of values from a specific column in a table.

getPairs() shows how to return the values of the custom class StringIntPair, whose field names match the aliases of the column names we picked with the SELECT operation:

data class StringIntPair(val first: String, var second: Int)

With our interface defined, all we have to do now is initialize our database in MainActivity.

Displaying the database

Let's display our database in RecyclerView.

class MainActivity : AppCompatActivity() {
    private lateinit var appDatabase: AppDatabase

We'll add the below code in onCreate(). First, we get our database instance from application. Then, we fill the list in adapter from it:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    
    appDatabase = (application as SomeApplication).database

    peopleAdapter = PeopleAdapter(
        appDatabase
            .getPersonDao()
            .getAllPeople()
    )

To display our database elements in the UI, we have to create an Adapter. We covered this in the RecyclerView topic, so it's nothing new:

class PeopleAdapter(data: List<Person>) :
    RecyclerView.Adapter<PeopleAdapter.PersonHolder>() {

    var data: List<Person> = data
        set(value) {
            field = value
            notifyDataSetChanged()
        }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonHolder {
        return PersonHolder(
            PersonItemBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )
    }

    override fun onBindViewHolder(holder: PersonHolder, position: Int) {
        val person = data[position]
        holder.binding.apply {
            textName.text = person.name
            textAge.text = person.age.toString()
            textId.text = person.id.toString()
        }
    }

    override fun getItemCount() = data.size

    class PersonHolder(val binding: PersonItemBinding) : RecyclerView.ViewHolder(binding.root)
}

We simply display Person data in three different TextViews.

In MainActivity, we add some code for setting up the adapter to RecyclerView:

binding.rv.layoutManager = LinearLayoutManager(this)
binding.rv.adapter = peopleAdapter

Now we assign to Buttons and EditText listeners:

binding.addBtn.setOnClickListener {
    if (isInputValid) {
        val person = Person(
            binding.inputName.text.toString(),
            binding.inputAge.text.toString().toInt()
        )
        appDatabase.getPersonDao().insert(person)
        peopleAdapter.data =
            appDatabase.getPersonDao().getAllPeople()
    } else Toast.makeText(this, "Input is empty", Toast.LENGTH_SHORT).show()
}
binding.delBtn.setOnClickListener {
    appDatabase.getPersonDao().deleteAll()
    peopleAdapter.data = emptyList()
}
binding.filterEditText.doAfterTextChanged {
    peopleAdapter.data =
        appDatabase.getPersonDao().getPeopleWhereNameStartsWith(it.toString())
}

The purpose of the data array is just to store data in RAM so it can be displayed in RecyclerView. We clear it before each action and then update it again from the database.

With filterEditText, we have implemented a search for the currently written text. This is based on a query, where || is used for SQLite string concatenation, and the percent symbol ('%') in the LIKE pattern is used to match any sequence of zero or more characters in the string:

"SELECT * FROM people WHERE name LIKE :name || '%'"

This will find names that begin with name.

And here's some simple code that checks the input isn't empty:

private val isInputValid: Boolean
    get() = binding.inputName.text.toString().isNotEmpty() &&
            binding.inputAge.text.toString().isNotEmpty()

Partial queries

Room can also perform partial queries that enable us to get specific columns from a table.

We have three columns in our table — id, name, and age. But what if we only want to get the names? The following type of SQL query is used for this purpose:

SELECT name FROM people

As you will remember, to execute a query, we must make a function for it in the Dao class:

@Dao
interface PersonDao {

    //...

    @Query("SELECT name FROM people")
    fun getNames(): List<String>
}

Let's display the names when the user clicks the button in the Toast notification:

binding.showNames.setOnClickListener {
    Toast.makeText(
        this, appDatabase.getPersonDao()
            .getNames().toString(), Toast.LENGTH_SHORT
    ).show()
}

toString() will automatically convert the list to a string representation if one is defined for its elements.

Conclusion

You now know how to use Room to work with databases in Android more quickly. You have learned how to create a RoomDatabase class and why you should organize it through a singleton. You have also discovered what a DAO interface is, seen the way to write queries in it, and learned how to create Entity classes. It's time to test your knowledge!

27 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo