Android Development

Getting Started With Room for Android

September 14, 2017

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 does not 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:

Joseph Kreiser
Joseph Kreiser
Software Developer

Looking for more like this?

Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.

Kotlin Multiplatform
Android Development iOS

Kotlin Multiplatform

July 14, 2022

A brief look at Kotlin Multiplatform Mobile, a newer cross-platform mobile application framework.

Read more
How our Associates are using AI tools: Advice for early-career developers
Development

How our Associates are using AI tools: Advice for early-career developers

August 13, 2024

Our 2024 Associates at Michigan Labs share their experiences using AI tools like GitHub Copilot and ChatGPT in software development. They discuss how these tools have enhanced their productivity, the challenges they've faced, and provide advice for using AI effectively.

Read more
Making your Android project modular with convention plugins
Android Development

Making your Android project modular with convention plugins

May 22, 2024

Explore the journey of Gradle and build tools like it, particularly in the context of Android development. You'll learn the necessity of separating code into modules as projects grow and how Gradle convention plugins can streamline this process.

Read more
View more articles