How to return a single (non-LiveData) object from Room using Coroutines

Viewed 3233

Room executes queries that return LiveData on a background thread automatically. But I want to return a single value that is not wrapped into LiveData (because I don't want live updates). How do I implement this using coroutines?

How do I return the Task object from this function?

fun getTask(id: Int): Task {
    viewModelScope.launch {
        repository.getTask(id)
    }
}

This function is inside the ViewModel. It forwards the call down to the DAO:

@Query("SELECT * FROM task_table WHERE id = :id")
fun getTask(id: Int): Task
4 Answers

If you don't return a LiveData from Room you won't get updates from the DB. You can however return a LiveData from your viewModel.

val data = liveData {
    emit(repository.getTask(id))
}

The liveData extension function runs in a coroutine and then you can use the suspend version of your DAO to handle backgrounding properly.

@Query("SELECT * FROM task_table WHERE id = :id")
suspend fun getTask(id: Int): Task?

A big thing you need to do is make sure it is nullable if you aren't using an aggregate function in your query.

If you are really wanting to call the method in your viewModel to return the task you should run the launch from your activity/fragment (not recommended)

ViewModel
suspend fun getTask(id: Int): Task {
    repository.getTask(id)
}

Activity/Fragment
lifecycleScope.launch {
    val task = viewModel.getTask(id)
    // Do What you want with the task
}

Flow is the new LiveData!

I had a similar problem in two project before, each solved differently. But recently I learnt to use Flow and it appears that it is the cleanest way yet.

Alternative to LiveData

If you don't need to use LiveData you have two option:

  1. Retrieving a Cursor by a @query, suitable for refactoring old projects.
  2. Using Flow, suitable for new projects.

Retrieving only one object/value

  1. LiveDate: You can unsubscribe from the LiveData, remove the observer after the first fetch. Not clean way in my opinion.
  2. Flow: You can retrive just a single object/value if you want and then stop the flow collecting.

Dao:

getTask(): this method return a Flow<Task>:

@Query("SELECT * FROM task_table WHERE id = :id")
fun getTask(id: Int): Flow<Task>

ViewModel:

getTask(): return a Task object (not Flow<Task>), also it is suspend function.

first() The terminal operator that returns the first element emitted by the flow and then cancels flow’s collection. Throws NoSuchElementException if the flow was empty.

suspend fun getTask(id: Int): Task {
    return dao.getTask(id).first()
}

Fragment/Activity:

Properties:

private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

don't forget to cancel viewModelJob when fragment/activity not needed aka onClear/onDestory/... so that all coroutines tight to this is canceled.

Usage

Now whenever we want to retrieve our object Task from that suspended function we need to be inside a suspend or coroutine. Therefore using launch builder to create a coroutine is suitable here (since we don't want any return object from that builder we only want to run a suspend function, otherwise async to return a deferred).

    onCreate() / onCreateView()
    .
    ..
    ...
    uiScope.launch() {
        // Here are the Task & Main-UI
        val task = viewModel.getTask(1)
        binding.taskTitleTextView.text = task.title
    }

If we don't use first() then we need to collect the flow viewModel.getTasks().collect{ it } there are many useful function at kotlinx.coroutines.flow. Flow is the best thing that happen in Coroutine Package, and oh sorry that I pass the repository layer, it is just a duplicated for viewModel in most cases .

Suspend functions in Room are main-safe and run on a custom dispatcher. Same as LiveData as you mentioned in your question. Below is the example to achieve the same

Inside some function in viewmModel class

 viewModelScope.launch {
       // suspend and resume make this database request main-safe
       // so our ViewModel doesn't need to worry about threading
       someLiveData.value =
               repository.getSomething()
   }

In repository class

suspend fun getSomething(): List<Something> {
   return dao.getSomething()

}

In Dao class

 @Query("select * from tableName")
   suspend fun getSomething(): List<Something>

One of the workaround would be to return Deferred object immediately and then call .await() on the return deferred

fun getTaskAsync(id: Int): Deferred<Task> = viewModelScope.async {
        repository.getTask(id)
    }

//call-site
getTaskAsync(id).await() // <- this is suspension point
Related