Import and Export Mechanics
The ImportExport
module of the book’s primary sample project demonstrates copying databases for backup/restore or import/export purposes.
Its UI consists mostly of three big buttons:
- Add some random data to the database
- Export the database to a location that the user chooses via
ActivityResultContracts.CreateDocument
- Import the database from a location that the user chooses via
ActivityResultContracts.OpenDocument
Also, at the bottom, there is a TextView
showing the number of rows in our one database table and how old the oldest row is.
RandomRepository
is the repository that not only mediates conventional database operations but also handles import and export operations:
package com.commonsware.room.importexport
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
import kotlin.random.Random
class RandomRepository(
private val context: Context,
private val appScope: CoroutineScope
) {
private val mutex = Mutex()
private val dispatcher =
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private var db: RandomDatabase? = null
suspend fun summarize() =
withContext(dispatcher) {
mutex.withLock {
if (RandomDatabase.exists(context)) {
db().randomStore().summarize()
} else {
Summary(count = 0)
}
}
}
suspend fun populate() {
withContext(dispatcher + appScope.coroutineContext) {
mutex.withLock {
val count = Random.nextInt(100) + 1
db().randomStore().insert((1..count).map { RandomEntity(0) })
}
}
}
suspend fun export(uri: Uri) {
withContext(dispatcher + appScope.coroutineContext) {
mutex.withLock {
db?.close() // ensure no more access and single database file
db = null
context.contentResolver.openOutputStream(uri)?.use {
RandomDatabase.copyTo(context, it)
}
}
}
}
suspend fun import(uri: Uri) {
withContext(dispatcher + appScope.coroutineContext) {
mutex.withLock {
db?.close() // ensure no more access and single database file
db = null
context.contentResolver.openInputStream(uri)?.use {
RandomDatabase.copyFrom(context, it)
}
}
}
}
private fun db(): RandomDatabase {
if (db == null) {
db = RandomDatabase.newInstance(context)
}
return db!!
}
}
As with some of the other examples, this sample uses Kotlin coroutines. RandomRepository
get a Context
and a CoroutineScope
injected via Koin — the Context
is to open a RandomDatabase
, while the latter is for ensuring that data modification operations do not get canceled based on our MainActivity
getting destroyed.
Functions like summarize()
(used to get the data for the TextView
) and populate()
(used to insert some random rows into the database table) are mostly normal. They have two differences compared to past samples:
- Both wrap the database I/O in a
Mutex
.Mutex
is the coroutines approach for mutual exclusion. Only one bit of code can be operating inside of theMutex
(and itswithLock()
function) at a time. We use thisMutex
in ourimport()
andexport()
functions as well, and we use this to ensure that nothing tries working with our database while the import or export operations are ongoing. This is a crude approach, offering limited concurrency, and a more sophisticated app might want to do something… well, more sophisticated. - Both call a
db()
function to get theRandomDatabase
instance to use.db()
, in turn, lazy-creates theRandomDatabase
, if it presently isnull
.
import()
and export()
also use the Mutex
. However, inside of withLock()
, they:
-
close()
ourRandomDatabase
, so our WAL files get cleared and we have a single database file - Set the
db
property tonull
, sodb()
knows to re-open the database on the next regular bit of database I/O - Copies the database to or from a location specified by a
Uri
, usingcopyFrom()
andcopyTo()
functions onRandomDatabase
to handle the actual copy operations:
package com.commonsware.room.importexport
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import java.io.InputStream
import java.io.OutputStream
private const val DB_NAME = "random.db"
@Database(entities = [RandomEntity::class], version = 1)
@TypeConverters(TypeTransmogrifier::class)
abstract class RandomDatabase : RoomDatabase() {
abstract fun randomStore(): RandomStore
companion object {
fun newInstance(context: Context) =
Room.databaseBuilder(context, RandomDatabase::class.java, DB_NAME).build()
fun exists(context: Context) = context.getDatabasePath(DB_NAME).exists()
fun copyTo(context: Context, stream: OutputStream) {
context.getDatabasePath(DB_NAME).inputStream().copyTo(stream)
}
fun copyFrom(context: Context, stream: InputStream) {
val dbFile = context.getDatabasePath(DB_NAME)
dbFile.delete()
stream.copyTo(dbFile.outputStream())
}
}
}
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.