Export and import data functionality for your app

It's been a while since mobile phones have become not an optional but a necessary tool for everyday life. People are using apps for all sorts of crucial things and if access is lost to one of those apps, people will be upset, to say the least.

Being an app developer of such apps bears some responsibility. Users should be able to back up their data and transfer them to new devices on demand if needed. Android has an auto-backup feature that restores app data when a user gets a new device. But sometimes this mechanism fails and for essential apps having an export/import functionality on-demand for important data gives your users some piece of mind.

In this post, I will describe the simplest possible way of implementing such a mechanism. It might not be the most efficient or advanced way, but if something basic is needed it will be more than enough.  

Room database bulk queries

First of all, there needs to be a way to bulk extract and bulk import data into your database. Assuming that you are using Jetpack Room, which I consider to be the de-facto DB layer in the Android world these days, extend your DAO object to include these:

@Dao
interface ItemsDao {
	
    [...]

    @Query("SELECT * FROM item")
    fun listAll(): List<Item>
    
    @Insert
    fun insertAll(tasks: List<Item>): List<Long>
}

Export data

private val exportDataLauncher =
    registerForActivityResult( // 1.
        ActivityResultContracts.CreateDocument()) { // 2.
    if (it == null) return@registerForActivityResult // 3.
    val jsonString = Gson().toJson(itemsDao.listAll()) // 4.
    requireContext().contentResolver.openOutputStream(it) // 5.
         ?.bufferedWriter()?.apply {
             write(jsonString)
             flush()
         }
}
        
fun onExportDataClicked() {
    exportDataLauncher.launch("backup${Date().time}.json") // 6.
}
  1. This is the newer way of launching Activities in Android that will return some data. A contract (see 2.) is defining the details of the launched intent, and your callback will be called with the data returned from the launched Activity for you to handle. Note that you must call registerForActivityResult() before the Fragment or Activity is created.
  2. In this case, we are using a "built-in" ActivityResultContract called CreateDocument. As the name implies, it will open a file selector for the user to decide where the exported file will be stored.
  3. If the user cancels the file selector, your callback will still be called but with null.
  4. For the exported data to be stored in a file, a serialization mechanism must be chosen. For simplicity purposes, we are using Gson (use your favorite JSON library) to serialize the result of the bulk query itemsDao.listAll() to a simple string.
  5. The result of the file selector was a Uri. We are using the ContentResolver to convert that Uri to an OutputStream and write the JSON string to the file system.
  6. Finally, we need to launch this. The input of the ActivityResultContract is the default filename. Note that you cannot overwrite a file using this method, so if you choose a file that already exists a numbered postfix will be added (e.g. "file (1)", "file (2)", etc).

Import data

private val importDataLauncher = registerForActivityResult(
    ActivityResultContracts.OpenDocument()) { // 1.
        if (it == null) return@registerForActivityResult
        val jsonString = // 2.
            requireContext().contentResolver.openInputStream(it)
                        ?.bufferedReader()?.readText() ?: "[]"
            val listType: Type = object : TypeToken<List<Item>>() {}.type
            val items: List<Item> = 
                Gson().fromJson(jsonString, listType) // 3.
            itemsDao.insertAll(items) // 4.
    }
    
fun onImportDataClicked() {
    importDataLauncher.launch(arrayOf("*/*")) // 5.
}
  1. This time we are using a contract that opens a file selector dialog for selecting an existing file.
  2. For converting the returned Uri into an InputStream this time.
  3. Deserialize the file content into your model. In our case, Gson is used to convert a JSON list into a list of items.
  4. Bulk import into the database.
  5. Launch the file selector dialog.  The input of the ActivityResultContract here is the MIME type of allowed files.

Hopefully, you now have a quick mechanism in your hands to create a quick import/export data functionality. Please note that the above code snippets do not have proper error handling and you would need to add your own before using this in a real app.

Happy coding!