Room and LiveData

One “out of the box” option is to use LiveData.

You can learn more about LiveData in the "Thinking About Threads and LiveData" chapter of Elements of Android Jetpack!

No special dependencies are required, other than Room itself. You can simply wrap your DAO @Query return values in LiveData:

package com.commonsware.todo.repo

import androidx.lifecycle.LiveData
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.threeten.bp.Instant
import java.util.*

@Entity(tableName = "todos", indices = [Index(value = ["id"])])
data class ToDoEntity(
  val description: String,
  @PrimaryKey
  val id: String = UUID.randomUUID().toString(),
  val notes: String = "",
  val createdOn: Instant = Instant.now(),
  val isCompleted: Boolean = false
) {
  constructor(model: ToDoModel) : this(
    id = model.id,
    description = model.description,
    isCompleted = model.isCompleted,
    notes = model.notes,
    createdOn = model.createdOn
  )

  fun toModel(): ToDoModel {
    return ToDoModel(
      id = id,
      description = description,
      isCompleted = isCompleted,
      notes = notes,
      createdOn = createdOn
    )
  }

  @Dao
  interface Store {
    @Query("SELECT * FROM todos")
    fun all(): LiveData<List<ToDoEntity>>

    @Query("SELECT * FROM todos WHERE id = :modelId")
    fun find(modelId: String): LiveData<ToDoEntity?>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun save(vararg entities: ToDoEntity)

    @Delete
    fun delete(vararg entities: ToDoEntity)
  }
}

This ToDoEntity class is from the LiveData module of the book’s primary sample project.

You can learn more about the to-do application this module is based on in the "What We Are Building" chapter of Exploring Android!

We have a @Dao-annotated interface named ToDoEntity.Store. It has four functions, two of which have @Query annotations (all() and find()). Instead of returning entities directly, though, they return LiveData wrappers around those entities.

When we call all() or find(), we get a LiveData back immediately. The I/O will not be performed until we observe() that LiveData. At that point, the database I/O will be conducted on a Room-supplied background thread, but the results will be delivered to our Observer on the main application thread (as with all uses of LiveData).

Benefits of LiveData

As with all of our reactive options, the database I/O gets offloaded to a background thread, so we do not need to fork such a thread ourselves. Yet, we still get the results delivered to us on the main application thread, so we can easily apply those results to our UI.

Also, LiveData does not require any additional dependencies, which can help to keep the app a bit smaller.

Issues with LiveData

LiveData is not an option for @Insert, @Update, or @Delete functions — you would still need to manage your own background threads for those.

LiveData always delivers its results on the main application thread. This is great when we want those results on the main application thread. This is a problem when we do not want those results on the main application thread, such as performing some database I/O in preparation for making a Web service request.

LiveData needs to be observed, and the typical way of observing a LiveData requires that you pass a LifecycleOwner to observe(). This is annoying when you do not have a LifecycleOwner to use, such as when using LiveData in some types of services. You can use observeForever() to avoid the need for the LifecycleOwner, but then you need to remember to remove your Observer, lest you accidentally wind up with a memory leak.

And LiveData is lightweight by design. If you have complex background operations to perform, such as combining results from multiple sources and converting those results into specific object types, LiveData has limited facilities to help with that. And, what it does have can be somewhat arcane to use (e.g., MediatorLiveData).


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.