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:
Looking for more like this?
Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.
Three principles for creating user-friendly products
January 25, 2023Grayson discusses three guiding principles he utilizes when designing user experiences for products.
Read more5 takeaways from the Do iOS conference that will benefit our clients
November 30, 2023 Read more3 tips for navigating tech anxiety as an executive
March 13, 2024C-suite leaders feel the pressure to increase the tempo of their digital transformations, but feel anxiety from cybersecurity, artificial intelligence, and challenging economic, global, and political conditions. Discover how to work through this.
Read more