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.
Kotlin Multiplatform
July 14, 2022A brief look at Kotlin Multiplatform Mobile, a newer cross-platform mobile application framework.
Read moreHow our Associates are using AI tools: Advice for early-career developers
August 13, 2024Our 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 moreMaking your Android project modular with convention plugins
May 22, 2024Explore 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