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.