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:

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:

  1. 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 the Mutex (and its withLock() function) at a time. We use this Mutex in our import() and export() 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.
  2. Both call a db() function to get the RandomDatabase instance to use. db(), in turn, lazy-creates the RandomDatabase, if it presently is null.

import() and export() also use the Mutex. However, inside of withLock(), they:

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.