One of the most exciting announcements for Android developers to come from Google I/O 2017 was the introduction of Android Architecture Components. Architecture Components are a collection of Android libraries from Google intended to help developers better handle Android lifecycle events and to persist data across those events. In the case of data persistence, part of this new framework is Room, a SQLite database helper library.

The Android framework already has components for working with SQLite database files created by your app, but they are very tedious to implement and modify due to how little abstraction there is from working with raw SQLite. Room is the newest player in the ORM field, and its simplicity and ease-of-use make it an attractive option. Many libraries have come forth in the past few years to make working with Android SQLite databases much cleaner and friendlier. Object-relational Mapping (ORM) database libraries, such as OrmLite, GreenDAO, SugarORM, DBFlow, and ActiveAndroid all attempt to lessen the tedium of SQLite database table creation and creating SQL statements to query and manipulate the database. Realm and ObjectBox are ORMs that use their own database systems under the hood instead of SQLite.

We’ll be using Kotlin and RxJava in the following examples.

Gradle

Add the following dependencies to your build.gradle to get the Room, RxJava 2, and the RxAndroid and RxKotlin bindings libraries. These are the latest versions of these libraries as of this writing, so check to make sure that you have the latest if you decide to give Room a try.

android {
    ...
}

dependencies {
    ...

    // Database
    def roomVersion = "1.0.0-alpha5"
    compile "android.arch.persistence.room:runtime:$roomVersion"
    kapt "android.arch.persistence.room:compiler:$roomVersion"
    compile "android.arch.persistence.room:rxjava2:$roomVersion"

    // RxJava
    compile 'io.reactivex.rxjava2:rxjava:2.1.0'
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    compile 'io.reactivex.rxjava2:rxkotlin:2.0.3'
}

Entity classes

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    var id: Int = -1,
    var username: String? = null,
    var email: String? = null,
    var isAdmin: Boolean = false
)

Specify your database entity with a class annotated with @Entity, give it an optional table name, annotate one of the fields as a @PrimaryKey, and you’re all set.

Data Access Object (DAO)

Now we need a DAO interface to specify how we will query/create/update/delete our User objects from the "users" database table.

@Dao
interface UserDao {
    // Retrieve a single User by its id
    @Query("SELECT * FROM user WHERE id = :id")
    fun getById(id: Int): User

    // Retrieve all User objects
    @Query("SELECT * FROM user")
    fun getAllUsers(): List<User>

    // Retrieve all User objects via an Rx Flowable object
    // with the Users returned to the subscriber after the query completes,
    // and the subscriber will be be called again each time the data changes.
    @Query("SELECT * FROM user")
    fun queryAllUsers(): Flowable<List<User>>

    // Retrieve a single User by its id, returned via an Rx Single.
    // If a user is found, onSuccess() is called.
    // Otherwise, it will call onError() with
    // android.arch.persistence.room.EmptyResultSetException
    @Query("SELECT * FROM user WHERE id = :id")
    fun getByIdSingle(id: Int): Single<User>

    // Retrieve a single User by its id, returned via an Rx Maybe.
    // If a User is found, onSuccess() is called.
    // Otherwise, onComplete() is called.
    @Query("SELECT * FROM user WHERE id = :id")
    fun getByIdMaybe(id: Int): Maybe<User>

    // insert a User
    @Insert
    fun insert(user: User)

    // delete a User
    @Delete
    fun delete(user: User)
}

With this DAO interface, we’ve specified a few different operations that we can use on the User table. Room allows us to use raw SQL statements for querying our objects and we can provide different arguments to the SQL (:id) with matching method arguments (id: Int). At compile time, Room will verify that each SQL argument has a matching method argument.

Setting up the Database

@Database(
    version = 1,
    entities = arrayOf(
        User::class
    )
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Google recommends using the Singleton pattern when instantiating a RoomDatabase. We can either do this by putting an instance of it in our custom Application object:

class App : Application() {
    companion object {
        lateinit var database: AppDatabase
    }

    override fun onCreate() {
        super.onCreate()
        App.database = Room.databaseBuilder(this, AppDatabase::class.java, "app.db").build()
    }
}

or if you are using Dagger dependency injection:

@Provides
@Singleton
fun provideAppDatabase(applicationContext: Context): AppDatabase {
    return Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app.db").build()
}

Type Converters

In the definition of the AppDatabase class, you may have noticed the @TypeConverters(Converters::class). We can define custom conversion methods to convert an object into a type recognized and able to be persisted by SQLite. For example, if you wanted to persist a java.util.Date as a Long in the database,

class Converters {
  @TypeConverter
  fun fromTimestamp(value: Long?): Date? {
    return if (value == null) null else Date(value)
  }

  @TypeConverter
  fun dateToTimestamp(date: Date?): Long? {
    return date?.time
  }
}

Where the @TypeConverters annotation is placed also affects its scope, from applying to all fields down to individual fields.

Writing and reading to the database

Now that our database code is all set up, persisting data is as simple as

val user = User(
    username = "johndoe",
    email = "john@doe.com",
    isAdmin = true
)
App.database.userDao().insert(user)

Well, almost that simple. If you run this, you’ll get this exception:

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

Room enforces database access to be off the main thread, since database operations can be potentially long-running. Using Rx and RxKotlin, we can easily move the database call to a background I/O thread.

Single.fromCallable {
    val user = User(
        username = "johndoe",
        email = "john@doe.com",
        isAdmin = true
    )
    App.database.userDao().insert(user)
}
    .subscribeOn(Schedulers.io())
    .subscribeBy(
        onError = { error ->
            Log.e(TAG, "Couldn't write User to database", error)
        }
    )

Using the same Rx approach, we can also read from the database.

Single.fromCallable {
    // need to return a non-null object, since Rx 2 doesn't allow nulls
    App.database.userDao().getById(123) ?: User()
}
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribeBy(
        onSuccess = { user ->
            // do something with 'user'
        },
        onError = { error ->
            Log.e(TAG, "Couldn't read User from database", error)
        }
    )

Using the dao method that returns a Flowable, we can also set up a subscriber to get updates to the database as they come in.

App.database.userDao().queryAllUsers()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { users ->
        // do something with 'users'
    }

Conclusion

There are many solutions for persisting mass amounts of data for an Android app, with most making use of Android’s built-in SQLite framework. My favorite features of Room were being able to create methods annotated with SQLite statements for querying the database and its integration with Rx. I found Room to be relatively quick and easy to set up and using it with Rx to be just as quick and easy.

If you’d like to learn more about Room, check out the following resources: