Room and Coroutines
For Kotlin developers, the leading reactive solution is Kotlin’s own coroutines system. This is a direct extension of the programming language and offers the most power with the least syntactic complexity.
Room supports coroutines via the androidx.room:room-ktx
dependency. Adding that will pull in a compatible version of coroutines in addition to Room’s own code for supporting coroutines in @Dao
-annotated interfaces.
suspend
One advantage that Room’s coroutines support has over its LiveData
support is that you can use coroutines with @Insert
, @Update
, and @Delete
. Simply add the suspend
keyword to the @Dao
-annotated functions:
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(vararg entities: ToDoEntity)
@Delete
suspend fun delete(vararg entities: ToDoEntity)
These functions are from a ToDoEntity.Store
implementation in the Coroutines
module of the book’s primary sample project. This module is functionally the same as the LiveData
module, except that it uses coroutines with Room.
These suspend
functions can then be called from any suitable CoroutineScope
, such as the viewModelScope
offered by Jetpack’s ViewModel
system.
Just as the LiveData
@Dao
functions use a Room-supplied background thread, so too do the suspend
@Dao
functions.
Flow
For @Query
-annotated functions, you have two choices. You could use a suspend
function, just as you can with @Insert
, @Update
, and @Delete
. The closer equivalent of using LiveData
, though, is to use Flow
:
@Dao
interface Store {
@Query("SELECT * FROM todos")
fun all(): Flow<List<ToDoEntity>>
@Query("SELECT * FROM todos WHERE id = :modelId")
fun find(modelId: String): Flow<ToDoEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(vararg entities: ToDoEntity)
@Delete
suspend fun delete(vararg entities: ToDoEntity)
}
Flow
in the "Introducing Flows and Channels" chapter of Elements of Kotlin Coroutines!
Flow
is a bit like LiveData
, in that you can observe it (via functions like collect()
) to receive your results. Like the rest of coroutines, you can control the dispatcher that dictates what thread is used for receiving those results. Or, you can use the asLiveData()
extension function supplied by the androidx.lifecycle:lifecycle-livedata-ktx
artifact to convert a Flow
into a LiveData
for consumption by an activity or fragment:
package com.commonsware.todo.ui.roster
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.commonsware.todo.repo.ToDoModel
import com.commonsware.todo.repo.ToDoRepository
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class RosterViewState(
val items: List<ToDoModel> = listOf()
)
class RosterMotor(private val repo: ToDoRepository) : ViewModel() {
val states: LiveData<RosterViewState> =
repo.items().map { RosterViewState(it) }.asLiveData()
fun save(model: ToDoModel) {
viewModelScope.launch {
repo.save(model)
}
}
}
This ViewModel
(RosterMotor
) is not using the Flow
from our ToDoEntity.Store
directly. Rather, it is calling all()
on a ToDoRepository
, which in turn is using ToDoEntity.Store
. The all()
on ToDoRepository
takes advantage of the rich set of operators on Flow
to convert entities to model objects:
package com.commonsware.todo.repo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class ToDoRepository(
private val store: ToDoEntity.Store,
private val appScope: CoroutineScope
) {
fun items(): Flow<List<ToDoModel>> =
store.all().map { all -> all.map { it.toModel() } }
fun find(id: String): Flow<ToDoModel?> = store.find(id).map { it?.toModel() }
suspend fun save(model: ToDoModel) {
withContext(appScope.coroutineContext) {
store.save(ToDoEntity(model))
}
}
suspend fun delete(model: ToDoModel) {
withContext(appScope.coroutineContext) {
store.delete(ToDoEntity(model))
}
}
}
Benefits of Coroutines
Room’s coroutines support covers all @Dao
functions, whereas LiveData
only works for @Query
.
Coroutines provides flexibility for the thread that you use to receive the results. While often times you will use Dispatchers.Main
to get the results on the main application thread (for UI use), you have the option of using other dispatchers for other scenarios.
Coroutines overall are not tied to Android, the way that LiveData
is. It will be more common to see libraries expose a coroutines-based API than one based on LiveData
. As a result, for the overall Kotlin ecosystem, coroutines is likely to eclipse LiveData
in popularity, if it has not done so already.
Issues with Coroutines
Kotlin’s coroutines system is the youngest of the three main Room reactive frameworks. As such, it has not been “beaten up” as much as, say, RxJava has.
Coroutines are tied to Kotlin. While there are ways to get Java code to interoperate with Kotlin code that uses coroutines, it is rather clunky. If you expect to have a lot of Java code needing to work with your @Dao
, you may be better off with LiveData
or RxJava, until such time as you can move to coroutines.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.