Many apps make data
persistent to improve User
Experience.
Slide 3
Slide 3 text
Data Persistence for UX
• Enabling Offline mode
• Preventing redundant API calls for
unchangeable data
• Loading data from local storage is much
faster than loading data from network =>
Enabling faster data loading
Slide 4
Slide 4 text
What are some options for
persisting data in Android?
Persistent Storage in Android
• Content Providers: Publicly sharing data
• Shared Preferences: key-value data
• SQLite: Structured data
Slide 7
Slide 7 text
Persistent Storage in Android
• Content Providers: Publicly sharing data
• Shared Preferences: key-value data
• SQLite: Structured data
Slide 8
Slide 8 text
Persistent Storage in Android
• Content Providers: Publicly sharing data
• Shared Preferences: key-value data
• SQLite: Structured data
→ Android provides Core Framework API
for SQLite
Slide 9
Slide 9 text
Persistent Storage in Android
• Content Providers: Publicly sharing data
• Shared Preferences: key-value data
• SQLite: Structured data
→ Android provides Core Framework API
for SQLite; works for API level 1~
Let’s read the
documentation!
https://developer.android.com/training/basics/data-storage/databases.html
Finally, move cursor and get
information…
Slide 23
Slide 23 text
“This is horrible!”
Slide 24
Slide 24 text
• No compile-time analysis for SQLite
statements, so hard to debug, prone to bug.
• A lot of boilerplate codes required for
converting SQL queries into Application-
specific data objects.
Slide 25
Slide 25 text
No content
Slide 26
Slide 26 text
Room
Slide 27
Slide 27 text
What is Room? - 1
• Introduced in Google I/O 2017
• One of the Architecture Components
(a.k.a. arch)
• Object mapping library for persisting data
with SQLite
Slide 28
Slide 28 text
What is Room? - 2
• Fully supports SQLite; focuses on what
SQLite can do.
• Provide compile-time analysis for SQL
queries for better debugging experience
and less bugs
• Provide Observability and testability
Slide 29
Slide 29 text
What is Room? - 3
• Generate boilerplate codes and
implementation by declaring interface with
annotations (Similar to Retrofit or Dagger)
• No direct “R”elationship between entities; No
object references between @Entities
• Use @Embedded or @ForeignKey to save or
access to minimal data
Slide 30
Slide 30 text
Rest of the Content
• Basics of Room
• Reading Some of the generated source codes
• TypeConverter and observable queries
• Conclusion
• Extra
Slide 31
Slide 31 text
Not to cover in this talk
• About Migration
• How to write tests using Room
• Other in-depth content
User.kt
class User(
val uid: Int,
val firstName: String,
val lastName: String)
Slide 42
Slide 42 text
User.kt
@Entity class User(
val uid: Int,
val firstName: String,
val lastName: String)
Slide 43
Slide 43 text
@Entity class User(
@PrimaryKey val uid: Int,
val firstName: String,
val lastName: String)
User.kt
Slide 44
Slide 44 text
@Entity class User(
@PrimaryKey val uid: Int,
val firstName: String,
val lastName: String)
User.kt
Slide 45
Slide 45 text
@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
AppDatabase.kt
Slide 46
Slide 46 text
@Entity class User(
@PrimaryKey val uid: Int,
val firstName: String,
val lastName: String)
User.kt
Slide 47
Slide 47 text
@Entity(tableName = "follower") class User(
@PrimaryKey val uid: Int,
val firstName: String,
val lastName: String)
User.kt
Slide 48
Slide 48 text
@Entity(tableName = "follower") class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String,
@ColumnInfo(name = "last_name") val lastName: String)
User.kt
Slide 49
Slide 49 text
@Annotations for @Entity
Slide 50
Slide 50 text
@PrimaryKey
@Annotations for @Entity
Slide 51
Slide 51 text
User.kt
@Entity class User(
@PrimaryKey(autoGenerate = true) val uid: Int,
Slide 52
Slide 52 text
User.kt
@Entity class User(
@PrimaryKey(autoGenerate = true) val uid: Int,
At least one field annotated
with @PrimaryKey is
required.
Slide 53
Slide 53 text
User.kt
@Entity class User(
@PrimaryKey(autoGenerate = true) val uid: Int,
Room will assign unique ID
automatically.
Slide 54
Slide 54 text
@Ignore
@Annotations for @Entity
Slide 55
Slide 55 text
User.kt
@Entity class User(
@PrimaryKey val uid: Int,
…
@Ignore val profileImage: Bitmap)
Slide 56
Slide 56 text
User.kt
@Entity class User(
@PrimaryKey val uid: Int,
…
@Ignore val profileImage: Bitmap)
Slide 57
Slide 57 text
@Embedded
@Annotations for @Entity
Slide 58
Slide 58 text
@Entity class User(
@PrimaryKey(autoGenerate = true) val uid: Long,
….
val address: Address
)
class Address(
@ColumnInfo(name = "post_code") val postCode: Int,
val street: String,
val province: String,
val city: String
)
Slide 59
Slide 59 text
@Entity class User(
@PrimaryKey(autoGenerate = true) val uid: Long,
….
@Embedded val address: Address
)
class Address(
@ColumnInfo(name = "post_code") val postCode: Int,
val street: String,
val province: String,
val city: String
)
@Entity class User(
@PrimaryKey(autoGenerate = true) val uid: Long,
….
@Embedded val address: Address
)
class Address(
@ColumnInfo(name = "post_code") val postCode: Int,
val street: String,
val state: String,
val city: String
)
Slide 64
Slide 64 text
@Entity class User(
@PrimaryKey(autoGenerate = true) val uid: Long,
….
@Embedded val primaryAddress: Address
@Embedded val secondaryAddress: Address
)
class Address(
@ColumnInfo(name = "post_code") val postCode: Int,
val street: String,
val state: String,
val city: String
)
Slide 65
Slide 65 text
@Entity class User(
@PrimaryKey(autoGenerate = true) val uid: Long,
….
@Embedded val primaryAddress: Address
@Embedded (prefix = “secondary_”)
val secondaryAddress: Address
) class Address(
@ColumnInfo(name = "post_code") val postCode: Int,
val street: String,
val state: String,
val city: String
)
UserDao.kt
@Dao interface UserDao {
@Insert fun insertAll(users: List)
@Update fun update(users: List)
@Delete fun delete(user: User)
@Query("SELECT * FROM user") fun getAll(): List
Slide 87
Slide 87 text
UserDao.kt
@Dao interface UserDao {
@Insert fun insertAll(users: List)
@Update fun update(users: List)
@Delete fun delete(user: User)
@Query("SELECT * FROM user") fun getAll(): List
Slide 88
Slide 88 text
UserDao.kt
@Dao interface UserDao {
@Insert fun insertAll(users: List)
@Update fun update(users: List)
@Delete fun delete(user: User)
@Query("SELECT * FROM user") fun getAll(): List
Slide 89
Slide 89 text
UserDao.kt
@Dao interface UserDao {
@Insert fun insertAll(users: List)
@Update fun update(users: List)
@Delete fun delete(user: User)
@Query("SELECT * FROM user") fun getAll(): List
Slide 90
Slide 90 text
UserDao.kt
@Dao interface UserDao {
@Insert fun insertAll(users: List)
@Update fun update(users: List)
@Delete fun delete(user: User)
@Query("SELECT * FROM user") fun getAll(): List
Slide 91
Slide 91 text
UserDao.kt
@Dao interface UserDao {
@Insert fun insertAll(users: List)
@Update fun update(users: List)
@Delete fun delete(user: User)
@Query("SELECT * FROM user") fun getAll(): List
Slide 92
Slide 92 text
UserDao.kt
@Dao interface UserDao {
@Insert fun insertAll(users: List)
@Update fun update(users: List)
@Delete fun delete(user: User)
@Query("SELECT * FROM user") fun getAll(): List
Slide 93
Slide 93 text
UserDao.kt
@Dao interface UserDao {
@Insert fun insertAll(users: List)
@Update fun update(users: List)
@Delete fun delete(user: User)
@Query("SELECT * FROM user") fun getAll(): List
Very similar to Retrofit’s service interfaces
Slide 94
Slide 94 text
UserDao.kt
@Dao interface UserDao {
@Insert fun insertAll(users: List)
@Update fun update(users: List)
@Delete fun delete(user: User)
@Query("SELECT * FROM user") fun getAll(): List
Slide 95
Slide 95 text
@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
AppDatabase.kt
Slide 96
Slide 96 text
UserDao.kt
@Dao interface UserDao {
@Insert fun insertAll(users: List)
@Update fun update(users: List)
@Delete fun delete(user: User)
@Query("SELECT * FROM USER") fun getAll(): List
Slide 97
Slide 97 text
UserDao_Impl.java
@Override
public List getAll() {
final String _sql = "SELECT * FROM USER";
final RoomSQLiteQuery _statement =
RoomSQLiteQuery.acquire(_sql, 0);
final Cursor _cursor = __db.query(_statement);
try {
final int _cursorIndexOfUid =
_cursor.getColumnIndexOrThrow("uid");
…
Slide 98
Slide 98 text
UserDao_Impl.java
@Override
public List getAll() {
final String _sql = "SELECT * FROM USER";
final RoomSQLiteQuery _statement =
RoomSQLiteQuery.acquire(_sql, 0);
final Cursor _cursor = __db.query(_statement);
try {
final int _cursorIndexOfUid =
_cursor.getColumnIndexOrThrow("uid");
…
Slide 99
Slide 99 text
UserDao_Impl.java
final List _result = new
ArrayList(_cursor.getCount());
while(_cursor.moveToNext()) {
final User _item;
final int _tmpUid;
_tmpUid = _cursor.getInt(_cursorIndexOfUid);
…
_item = new User(…);
_result.add(_item);
}
Slide 100
Slide 100 text
UserDao_Impl.java
final List _result = new
ArrayList(_cursor.getCount());
while(_cursor.moveToNext()) {
final User _item;
final int _tmpUid;
_tmpUid = _cursor.getInt(_cursorIndexOfUid);
…
_item = new User(…);
_result.add(_item);
}
Slide 101
Slide 101 text
UserDao_Impl.java
final List _result = new
ArrayList(_cursor.getCount());
while(_cursor.moveToNext()) {
final User _item;
final int _tmpUid;
_tmpUid = _cursor.getInt(_cursorIndexOfUid);
…
_item = new User(…);
_result.add(_item);
}
UserDao.kt
@Dao interface UserDao {
@Query("SELECT * FROM user") fun getAll(): List
Slide 106
Slide 106 text
UserDao.kt
@Dao interface UserDao {
@Query("SELECT * FROM user") fun getAll(): List
Slide 107
Slide 107 text
Bind Parameters
Methods using @Query
Slide 108
Slide 108 text
UserDao.kt
@Dao interface UserDao {
@Query("SELECT * FROM user") fun getAll(): List
@Query("SELECT * FROM user WHERE age > :minAge"
) fun loadAllUsersOlderThan(minAge: Int): List
Slide 109
Slide 109 text
@Query("SELECT * FROM user WHERE age
BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int
maxAge);
UserDao.kt
@Dao interface UserDao {
@Query("SELECT * FROM user") fun getAll(): List
@Query("SELECT * FROM user WHERE age > :minAge"
) fun loadAllUsersOlderThan(minAge: Int): List
Slide 110
Slide 110 text
@Query("SELECT * FROM user WHERE first_name
LIKE :search" + "OR last_name LIKE :search")
fun findUserWithName(search: String): List
UserDao.kt
@Dao interface UserDao {
@Query("SELECT * FROM user") fun getAll(): List
@Query("SELECT * FROM user WHERE age > :minAge"
) fun loadAllUsersOlderThan(minAge: Int): List
Slide 111
Slide 111 text
Returning Subsets of
Columns
Methods using @Query
Slide 112
Slide 112 text
NameTaple.kt
@Entity class NameTuple(
@ColumnInfo(name = "first_name") val firstName: String,
@ColumnInfo(name = "last_name") val lastName: String
)
Slide 113
Slide 113 text
NameTaple.kt
@Entity class NameTuple(
@ColumnInfo(name = "first_name") val firstName: String,
@ColumnInfo(name = "last_name") val lastName: String
)
Slide 114
Slide 114 text
UserDao.kt
@Dao interface UserDao {
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List
Slide 115
Slide 115 text
UserDao.kt
@Dao interface UserDao {
@Query("SELECT first_name, last_name, middleName
FROM user")
fun loadFullName(): List
Slide 116
Slide 116 text
UserDao.kt
@Dao interface UserDao {
@Query("SELECT first_name, last_name, FROM user")
fun loadFullName(): List
Slide 117
Slide 117 text
OnConflictStrategy for
@Insert, @Update
Slide 118
Slide 118 text
UserDao.kt
@Dao interface UserDao {
@Insert(onConflict = OnConflictStrategy.ROLLBACK) fun
insertAll(users: List)
@Update(onConflict = OnConflictStrategy.REPLACE) fun
updateUsers(users: List)
Slide 119
Slide 119 text
@Retention(SOURCE)
…
public @interface OnConflictStrategy {
int REPLACE = 1;
int ROLLBACK = 2;
int ABORT = 3;
int FAIL = 4;
int IGNORE = 5;
}
OnConflictStrategy.java
Slide 120
Slide 120 text
@Retention(SOURCE)
…
public @interface OnConflictStrategy {
int REPLACE = 1;
int ROLLBACK = 2;
int ABORT = 3;
int FAIL = 4;
int IGNORE = 5;
}
OnConflictStrategy.java
https://sqlite.org/lang_conflict.html
Based on
SQLite Conflict Algorithm
@Entity class User(
@PrimaryKey(autoGenerate = true) val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String,
@ColumnInfo(name = "last_name") val lastName: String,
@JvmField val birthday: LocalDateTime
)
User.kt
Slide 132
Slide 132 text
class LocalDateTimeConverter {
@TypeConverter fun fromTimestamp(value: Long?):
LocalDateTime? {
return if (value == null) null else
LocalDateTime.ofEpochSecond(value, 0, ZoneOffset.UTC)
}
@TypeConverter fun dateToTimestamp(date:
LocalDateTime?): Long? {
return date?.toEpochSecond(ZoneOffset.UTC)
….
LocalDateTimeConverter.kt
Slide 133
Slide 133 text
class LocalDateTimeConverter {
@TypeConverter fun fromTimestamp(value: Long?):
LocalDateTime? {
return if (value == null) null else
LocalDateTime.ofEpochSecond(value, 0, ZoneOffset.UTC)
}
@TypeConverter fun dateToTimestamp(date:
LocalDateTime?): Long? {
return date?.toEpochSecond(ZoneOffset.UTC)
….
LocalDateTimeConverter.kt
Slide 134
Slide 134 text
@Database(entities = arrayOf(User::class, Address::class),
version = 1)
@TypeConverters(LocalDateTimeConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
AppDatabase.kt
Slide 135
Slide 135 text
private final LocalDateTimeConverter __localDateTimeConverter
= new LocalDateTimeConverter();
...
final LocalDateTime _tmpBirthday;
final Long _tmp;
...
_tmpBirthday =
__localDateTimeConverter.fromTimestamp(_tmp);
UserDao_Impl.java
Slide 136
Slide 136 text
Observable Queries
Slide 137
Slide 137 text
@Query("SELECT * FROM user WHERE first_name
LIKE :search" + "OR last_name LIKE :search")
fun findUserWithName(search: String): List
UserDao.kt
@Dao interface UserDao {
@Query("SELECT * FROM user") fun getAll(): List
Slide 138
Slide 138 text
@Query("SELECT * FROM user WHERE first_name
LIKE :search" + "OR last_name LIKE :search")
fun findUserWithName(search: String):
LiveData>
UserDao.kt
@Dao interface UserDao {
@Query("SELECT * FROM user") fun getAll():
Flowable>
Slide 139
Slide 139 text
@Query("SELECT * FROM user WHERE first_name
LIKE :search" + "OR last_name LIKE :search")
fun findUserWithName(search: String):
LiveData>
UserDao.kt
@Dao interface UserDao {
@Query("SELECT * FROM user") fun getAll():
Flowable>
Slide 140
Slide 140 text
Conclusion
Slide 141
Slide 141 text
Conclusion
• Room provides simple and customizable APIs with
raw SQL queries to solve current problems on
Framework APIs
• Room forbids direct references between entities and
forces to access/fetch as minimal data as possible
• Room supports LiveData and Flowable integration
for observability
Slide 142
Slide 142 text
Extra
Slide 143
Slide 143 text
Extra
• You may not need Room if current DB or ORM
library works better for you
• Compared to other Architecture Components,
Room is more stable and ready to be used
• Room currently does not support query
builders - “That’s a Android Studio’s job and
we are working on it”