The Room Library notes

The library provides an abstraction layer over Database. I think some features are similar to Flask-SQLAlchemy.

Entity

Entity is used to represent the objects stored in Database. Each entity corresponds to a table in the associated Room database, and each instance of an entity represents a row of data in the corresponding table.

using annotations:

1
2
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
  • Every property that’s stored in the database needs to have public visibility, which is the Kotlin default.

autogenerate unique keys:

1
>@PrimaryKey(autoGenerate = true) val id: Int,

DAO

DAO(data access object)wrap SQL queries with methods calls. The compiler checks the SQL and generates queries from convenience annotations for common queries.

  • The DAO must be an interface or abstract class.
  • By default, all queries must be executed on a separate thread.
  • Room has Kotlin coroutines support. This allows your queries to be annotated with the suspend modifier and then called from a coroutine or from another suspension function.

Implement the DAO

1
2
3
4
5
6
7
8
9
10
11
12
@Dao
interface WordDao {

@Query("SELECT * FROM word_table ORDER BY word ASC")
fun getAlphabetizedWords(): List<Word>

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)

@Query("DELETE FROM word_table")
suspend fun deleteAll()
}

Observing database changes

Use a return value of type Flow and Room generates all necessary code to update Flow when the database is updated.

A Flow is an async sequence of values.

RoomDatabase

  1. create a class

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // Annotates class to be a Room Database with a table (entity) of the Word class
    @Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
    public abstract class WordRoomDatabase : RoomDatabase() {

    abstract fun wordDao(): WordDao

    companion object {
    // Singleton prevents multiple instances of database opening at the
    // same time.
    @Volatile
    private var INSTANCE: WordRoomDatabase? = null

    fun getDatabase(context: Context): WordRoomDatabase {
    // if the INSTANCE is not null, then return it,
    // if it is, then create the database
    return INSTANCE ?: synchronized(this) {
    val instance = Room.databaseBuilder(
    context.applicationContext,
    WordRoomDatabase::class.java,
    "word_database"
    ).build()
    INSTANCE = instance
    // return instance
    instance
    }
    }
    }
    }

  • The database exposes DAOs through an abstract “getter” method for each @Dao.
  • The annotation parameters to declare the entities that belong in the database and set the version number.

Repository

A repository class abstracts access to multiple data sources.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {

// Room executes all queries on a separate thread.
// Observed Flow will notify the observer when the data has changed.
val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()

// By default Room runs suspend queries off the main thread, therefore, we don't need to
// implement anything else to ensure we're not doing long running database work
// off the main thread.
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}

ViewModel

A view model is a communication center between the Repository and the UI and it can also share data between fragments.

ViewModel take care of holding and processing all the data needed for the UI.

LiveData

LiveData is an observable data holder - you can get notified every time the data changes. Unlike Flow, LiveData is lifecycle aware, meaning that it will respect the lifecycle of other components like Activity or Fragment.

Implement ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class WordViewModel(private val repository: WordRepository) : ViewModel() {

// Using LiveData and caching what allWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()

/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}

class WordViewModelFactory(private val repository: WordRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(WordViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return WordViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

Concepts

Coroutine

A coroutine is an instance of suspendable computation.(similar to a thread) However, a coroutine is not bound to any particular thread. It may suspend its execution in one thread and resume in another one.

Example:

1
2
3
4
5
6
7
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
  • launch is a coroutine builder. It launches a new coroutine concurrently with the rest of the code, which continues to work independently.

  • delay is a special suspending function. Suspending a coroutine does not block the underlying thread, but allows other coroutines to run and use the underlying thread for their code.

  • runBlocking is also a coroutine builder that bridges the non-coroutine world of a regular main() and the code with coroutines inside of runBlocking{}

    It means the thread run runBlocking will get blocked for the duration of the call, until all the coroutines inside the curly brackets finish their execution.

Structured concurrency

An outer scope cannot complete until all its children coroutines complete.

suspending function

Suspending functions can be used inside coroutines just like regular functions, but their additional feature is that they can, in turn, use other suspending functions (like delay in this example) to suspend execution of a coroutine.

1
2
3
4
5
6
7
8
9
10
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}

// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}

Scope builder

It is possible to declare your own scope using the coroutineScope builder. It creates a coroutine scope and does not complete until all launched children complete.

runBlocking and coroutineScope builders may look similar because they both wait for their body and all its children to complete.

The main difference:

  • the runBlocking method blocks the current thread for waiting.
  • coroutineScope just suspends, releasing the underlying thread for other usages.

Because of that difference, runBlocking is a regular function and coroutineScope is a suspending function.

1
2
3
4
5
6
7
8
9
10
11
fun main() = runBlocking {
doWorld()
}

suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(1000L)
println("World!")
}
println("Hello")
}

Scope builder and concurrency

perform multiple concurrent operation in coroutineScope:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
doWorld()
println("Done")
}

// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}

an explicit job

1
2
3
4
5
6
7
val job = launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done")

launch returns a Job object that is a handle to to the launched coroutine and can be used