But First, A To-Do Reminder

Back in the chapter on reactive threading solutions, we looked at some code for tracking to-do items. This code was derived from the sample app built up in Exploring Android. That chapter explored variations of this code that used LiveData, RxJava, and coroutines.

The SQLCipher material in this book continues its riff on that example, so let’s quickly review the core Room elements of the to-do code, specifically the coroutines edition.

The Entity, the Model, and the Store

Our one Room entity is ToDoEntity implemented as a data class:

package com.commonsware.todo.repo

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(): 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)
  }
}

To simulate a more complex scenario, an entity knows how to convert itself to and from a ToDoModel:

package com.commonsware.todo.repo

import org.threeten.bp.Instant
import java.util.*

data class ToDoModel(
  val description: String,
  val id: String = UUID.randomUUID().toString(),
  val isCompleted: Boolean = false,
  val notes: String = "",
  val createdOn: Instant = Instant.now()
) {
}

In theory, that model might have a significantly different representation than does the entity, from data type conversions to having direct references to other models derived from other entities.

ToDoEntity contains a nested Store @Dao interface with functions for working with entities. Two (all() and find()) are queries and return Flow objects, while the save() and delete() functions are marked with suspend. Hence, our DAO uses coroutines for read and write operations.

The Database and the Transmogrifier

Our @Database class is ToDoDatabase:

package com.commonsware.todo.repo

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters

private const val DB_NAME = "stuff.db"

@Database(entities = [ToDoEntity::class], version = 1)
@TypeConverters(TypeTransmogrifier::class)
abstract class ToDoDatabase : RoomDatabase() {
  abstract fun todoStore(): ToDoEntity.Store

  companion object {
    fun newInstance(context: Context) =
      Room.databaseBuilder(context, ToDoDatabase::class.java, DB_NAME).build()

    fun newTestInstance(context: Context) =
      Room.inMemoryDatabaseBuilder(context, ToDoDatabase::class.java)
        .build()
  }
}

It ties in a TypeTransmogrifier class that offers type converters between Instant timestamps and Long objects for storage in Room:

package com.commonsware.todo.repo

import androidx.room.TypeConverter
import org.threeten.bp.Instant
import java.util.*

class TypeTransmogrifier {
  @TypeConverter
  fun instantToLong(timestamp: Instant?) = timestamp?.toEpochMilli()

  @TypeConverter
  fun longToInstant(timestamp: Long?) =
    timestamp?.let { Instant.ofEpochMilli(it) }
}

ToDoDatabase offers newInstance()-style factory functions both for normal use and for tests, with the latter being backed purely by memory instead of storing data on disk.

The Repository

ToDoRepository hides all of those details, exposing its own coroutine-based API that, in particular, uses a custom CoroutineScope to ensure that write operations are not canceled early due to user navigation and the resulting clearing of viewmodels:

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))
    }
  }
}

Prev Table of Contents Next

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