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.