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.

You can learn more about coroutines in the "Introducing Coroutines" chapter of Elements of Kotlin Coroutines!

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)
  }
You can learn more about 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.