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. 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 kotlinsdocumentation. 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 @PrimaryKeyautoGenerate
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<User>)
@Update
fun updateUser(user: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUser(user: User)
@Delete
fun deleteUsers(users: List<User>)
@Query("SELECT * FROM users")
fun users(): List<User>
}
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<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!
This post was originally published onSpeednet blog 16.10.2018