Room Persistence Library Introduction – part 1

Michał Konkel
October 16, 2018 | Software development

This article is the first part of the three-part series that will smoothly introduce Room Persistence Library to you.
The first part will be focused on configuring the project and explaining the basic structures.

Part 2 can be found here – it’s focused on @Embedded entities and @TypeConverters.
Part 3 can be found here – it’s focused on how to add relations between tables/entities and how to query them properly.

All sources can be found in related GitHub project.

What is Room?

The Room is a persistence library that provides an object-mapping abstraction layer over SQLite to more robust database access while harnessing the full power of SQLite. It comes along with the Architecture components and was presented at Google I/O in 2017. Now it has reached version 2.0 and it is a part of Android Jetpack (on 10.10.2018 the 2.1 alpha 1 version was released). In this article, we will focus on version 1.1.1.

Let’s start!

At the beginning we need to create an Android Project with the Kotlin support, API 27 (or greater) with “No Activity” option.
To use Room we need to add some dependencies to our gradle file

dependencies {
    def room_version = "1.1.1"

    implementation "android.arch.persistence.room:runtime:$room_version"
    kapt "android.arch.persistence.room:compiler:$room_version"

   ...
}

Basic elements

Room consists of a few basic elements that you should know before starting any work.

@Entity

A class annotated with @Entity will represent our column in the database. Basically, it is just a POJO set of related fields.

By default, Room creates a column for each field that’s defined in the entity. If an entity has fields that you don’t want to persist, you can annotate them using @Ignore annotation.
To persist a field, Room must have access to it. You have to make a field public, or at least provide a getter and setter for it.

The tricky part with @Ignore and Kotlin _behind the scenes_ generation of constructors for nullable fields. In that particular case, Kotlin will generate every possible constructor for class with nullable values – this will cause an error – because Room needs an empty constructor. One way is to override constructors as described in the kotlins documentation. The Second one (the easiest one) is to set default values for the fields

@PrimaryKey

Each entity must define at least one field as a primary key. Even when there is only one field, you still need to annotate the field with the @PrimaryKey annotation. In addition, if you want Room to assign automatic IDs to entities, you should set the @PrimaryKey autoGenerate property to true. You can also provide a composite primary key, just use the primaryKeys property of the @Entity.

By default, Room uses the class name as the database table name. If you want the table to have a different name, set the tableName property of the @Entity annotation.
Similarly to the tableName property, Room uses the field names as the column names in the database. If you want a column to have a different name, add the @ColumnInfo annotation to a field.

Let’s write some code!

@Entity(tableName = "users")
data class User(
        @PrimaryKey(autoGenerate = true)
        var id: Long,

        var firstName: String,

        var lastName: String,

        var fullName: String,

        @ColumnInfo(name = "email")
        var emailAddress: String,

        @ColumnInfo(name = "phone")
        var phoneNumber: String,

        var picture: String
)

What we’ve got here is a simple user entity whose name will be users, primary key will be autogenerated long. The code above will generate a corresponding table in the database.

@Dao

To access your app’s data using the Room persistence library, you should work with data access objects or DAOs.
This set of DAO objects forms the main component of Room, as each DAO includes methods that offer abstract access to your app’s database.
Basically, this is the point where you will be communicating with the database – here you will be defining your data interactions, mapping SQL queries to functions and more.

It is recommended to have multiple Dao classes in your codebase depending on the tables they touch.
Room creates each DAO implementation at compile time.

DAO consists of 4 major methods @Insert, @Update, @Delete and @Query.

@Insert

The implementation of the method will insert its parameters into the database.
If the @Insert method receives only one parameter, it can return a long, which is the new row_id for the inserted item. If the parameter is an array or a collection, it should return long[] or List<Long> instead.
@Insert contains the onConflict property which determines the SQLite conflict resolving strategy when inserting data.

@Update

The implementation of the method will update its parameters in the database if they already exist (checked by primary keys). If they don’t already exist, this option will not change the database.

@Delete

The implementation of the method will delete its parameters from the database. It uses the primary keys to find the entities to delete.

@Query

The main annotation used in DAO classes allows you to perform read/write operations on a database. The query is verified at compile time by Room to ensure that it compiles fine against the database.

Let’s write some code!

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUser(user: User)

    @Insert
    fun insertUsers(users: List&lt;User&gt;)

    @Update
    fun updateUser(user: User)

    @Update
    fun updateUsers(vararg users: User)

    @Delete
    fun deleteUser(user: User)

    @Delete
    fun deleteUsers(users: List&lt;User&gt;)

    @Query("SELECT * FROM users")
    fun users(): List&lt;User&gt;
}

As you can see we have some basic methods that allow us to insert, update or delete a single user or a list of users. The methods mentioned above use the primary key to determine which row should be affected.

You can also pass parameters into queries to perform filtering operations, such as only displaying users with a certain name.

Room only supports named bind parameter to avoid any confusion between the method parameters and the query bind parameters.
Room will automatically bind the parameters of the method into the bind arguments. This is done by matching the name of the parameters to the name of the bind arguments.

@Query("SELECT * FROM users WHERE firstName = :userName")
fun usersWithName(userName: String): List&lt;User

We will focus on this a little bit more in the second part of the article series.

@Database

Contains the database holder and serves as the main access point for the underlying connection to your app’s persisted, relational data.
A class annotated with @Database should be an abstract class and extend RoomDatabase.
You can receive an implementation of the class via Room.databaseBuilder.

RoomDatabase provides direct access to the underlying database implementation but you should prefer using Dao classes.

Database class should consist at least of:

  • List of entities
  • DB version
  • Abstract methods returning DAO’s
  • Database builder method.

Let’s code to see this in action!

@Database(
        entities = [User::class],
        version = AppDatabase.DB_VERSION
)
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {
        const val DB_VERSION = 1
        const val DB_NAME = "application.db"

        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase =
                INSTANCE ?: synchronized(this) {
                    INSTANCE ?: buildDatabase(context)
                }

        private fun buildDatabase(context: Context) =
                Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DB_NAME)
                        .build()
    }
}

From the beginning:
Annotation @Database requires two properties:

  • List of entities – the tables in our DB (classes)
  • DB version

At the top of the class, we should declare all DAO’s as the abstract functions – it’s just a convention.
Then, we should always use the singleton pattern to obtain the database object, because the creation of the DB connection is quite expensive.
The code above will create room database when anything asks for its instance.

We are also using:

  • @Volatile – that has semantics for memory visibility. Basically, the value of a volatile field becomes visible to all readers (other threads in particular) after a write operation completes on it. Without volatile, readers could see some non-updated value.
  • synchronized(this) – in the simplest words, when you have two threads that are reading and writing to the same ‘resource’ you need to ensure that these threads access the variable in an atomic way. Without it, the thread 1 may not see the change thread 2 made to a variable.

Furthermore, it’s also quite handy to get some Injector class that will provide necessary objects that we will later use in our Activity.

object Injector {

    fun provideUserDao(context: Context): UserDao {
        return AppDatabase.getInstance(context).userDao()
    }
}

Another thing we should take care of is pre-populating our database in some data set. With the traditional approach the data will probably come from some web API – for this short tutorial, we will use prepared data.

For this task, we will add the onCreate callback to the databaseBuilder. We should keep in mind that any operation related with the DB cannot be performed on the mainThread – because Room will throw an exception – so kotlin coroutine sounds like a good plan for this task.

We will need to add some dependencies to our app gradle.

kotlin {
    experimental {
        coroutines "enable"
    }
}

…

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.26.1'

Then we also need to add some code to the database builder.

private fun buildDatabase(context: Context) =
        Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DB_NAME)
                .addCallback(dbCreateCallback(context))
                .build()

private fun dbCreateCallback(context: Context) = object : Callback() {
     override fun onCreate(db: SupportSQLiteDatabase) {
         super.onCreate(db)
         GlobalScope.launch {
            getInstance(context).userDao()
                    .insertUsers(PrepopulateData.users)
        }
     }
}

Our prefilled data can look like that.

object PrepopulateData {
    val users = listOf(
            User(
                    id = null,
                    firstName = "John",
                    lastName = "Doe",
                    fullName = "John Doe",
                    emailAddress = "jdoe@mail.com",
                    phoneNumber = "001333444555",
                    picture = "/pictures/jdoe/avatar/s34trag_732_jkdal.png"
            ),
            User(
                    id = null,
                    firstName = "Mark",
                    lastName = "Smith",
                    fullName = "Mark Smith",
                    emailAddress = "mastermike@mail.com",
                    phoneNumber = "001666999888",
                    picture = "/pictures/msmith/avatar/123454647_gfas.png"
            )
    )
}

The given changes will create two new users when DB is first created, this will allow us to pre-populate DB when it’s first created. Now there is nothing more left for us, let’s finally check if everything works.

We can add some simple activity to validate the code.

class TestActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)

        val userDao = Injector.provideUserDao(this)

        GlobalScope.launch {
            val users = userDao.users()

            users.forEach {
                Log.d("User", “$it")
            }
        }
    }
}

Now if you run the application and everything went OK and the is showing a blank screen you should see a log message similar to this

User(id=1, firstName=John, lastName=Doe, fullName=John Doe, emailAddress=jdoe@mail.com, phoneNumber=001333444555, picture=/pictures/jdoe/avatar/s34trag_732_jkdal.png)
User(id=2, firstName=Mark, lastName=Smith, fullName=Mark Smith, emailAddress=mastermike@mail.com, phoneNumber=001666999888, picture=/pictures/msmith/avatar/123454647_gfas.png

That’s All Folks! We’ve reached the end of the first part of the introduction to the Room Persistence Library. I hope you’ve enjoyed the post and you can’t wait for more.
The second part will cover some details of @Entities, we will learn how to use TypeConverters and Embedded Entities.

Cheers!

If you want to meet us in person, click here and we’ll get in touch!