Export Room Database to CSV πŸ“‘

androidbites Feb 21, 2021

7 Easy steps πŸšΆβ€β™‚ ️to export your Room Database into CSV File.



onCreate Digest - Issue #45
onCreate Digest is a weekly newsletter with links to Android Development related content.
OnCreate Digest #45

It's fun being a part of TheCodeMonks, we are a FOSS organisation which solve problems by building better software solutions.

About The Code Monks
Tech bytes for Monks by Monks


Recently SpikeySanju, has released another one of his android project called Expenso which is an expense tracker application. Β 

Spikeysanju/Expenso
πŸ“Š A Minimal Expense Tracker App built to demonstrate the use of modern android architecture component with MVVM Architecture - Spikeysanju/Expenso

Recently a feature request was made by coolnikhilmaurya, regarding the ability to export the transactions into a CSV file , you can track that issue here, Β for which I have raised a PR, which you can track here .

Added Export to CSV by ch8n Β· Pull Request #48 Β· Spikeysanju/Expenso
Feature Preview WhatsApp.Video.2021-02-10.at.8.49.18.PM.mp4 Current functionality : Export your transaction details to CSV file This file is located at Documents/ex...

Preview

In this Article I will be breaking down how I have implemented this feature and how you can do the same for your own app in 7 simple steps.


I assume you have setup your Room Database correctly cause I will be sharing how to export it to CSV file in this article.

Step 1 : Setup and Dependencies

We will be using opencsv library in android to export our Room database, do override common-logging from the dependency as it will cause conflicts in gradle assemble.


Add these dependencies to your app-level gradle file

configurations {
    all {
    	// resolves conflicts of openCSV with platform common-logging
        exclude module: 'commons-logging'
    }
}

dependencies {
  ...
  // OpenCsv
  implementation ("com.opencsv:opencsv:5.3")
}
open csv gradle dependencies

Step 2 : Selecting Entity to Export

In Expenso app, we are exporting all the transaction details that has been logged by the user, which is stored using Transaction entity in Room database.


@Entity(tableName = "all_transactions")
data class Transaction(
    @ColumnInfo(name = "title")
    var title: String,
    @ColumnInfo(name = "amount")
    var amount: Double,
    @ColumnInfo(name = "transactionType")
    var transactionType: String,
    @ColumnInfo(name = "tag")
    var tag: String,
    @ColumnInfo(name = "date")
    var date: String,
    @ColumnInfo(name = "note")
    var note: String,
    @ColumnInfo(name = "createdAt")
    var createdAt: Long =
        System.currentTimeMillis(),
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id: Int = 0,
) : Serializable {
    val createdAtDateFormat: String
        get() = DateFormat.getDateTimeInstance()
            .format(createdAt) // Date Format: Jan 11, 2021, 11:30 AM
}
transaction entitiy from Expenso app

Step 3 : Setting up Service

Why Service? why not Repository? … IMO repositories are the data layer in your application , they are getters and setters for your data, while a service manipulates the data or performs some business logic.

Dirctory Strucrure
- packageName
  - services / csv / adapters / TransctionsCSV.kt 
  - services / csv / ExportService.kt
  - services / csv / ExportConfig.kt
Expenso directory structure


Step 4 : Setup Configuration for export

I have designed these classes to make export service easy to extend when more ways to export the transactions comes in.


For that I have created an exportable interface which will be the marker to every exportable entity of our application.

// Marker interface for exportable classes
interface Exportable

Currently app only supports CSV export, hence there is an Export sealed class, which has only CSV as hierarchy for now. Β 


CSV is a data class which depends upon object of CSVConfig.

// List of supported export functionality
sealed class Exports {
    data class CSV(val csvConfig: CsvConfig) : Exports()
}

CSVConfig class is responsible to get csv file name and host path, as of now Android 11 uses scoped storage hence getExternalStorageDirectory shows deprecations.


You can replace it with your scoped storage implementation if you like. Currently our app will be supporting this feature till Android API 29. (I will be adding support for scoped storage later, then would be updating the article)

data class CsvConfig(
    private val prefix: String = "expenso",
    private val suffix: String = DateFormat
        .getDateTimeInstance()
        .format(System.currentTimeMillis())
        .toString()
        .replace(",","")
        .replace(" ", "_"),
    
    val fileName: String = "$prefix-$suffix.csv",
    @Suppress("DEPRECATION")
    val hostPath: String = Environment
        .getExternalStorageDirectory()?.absolutePath?.plus("/Documents/Expenso") ?: ""
)

By default the file name would be expenso-Jan_11_2021_11:30_AM Β and host path where file would be created will be /Documents/Expenso


Step 5 : Creating adapter/mapper classes

They are responsible to map/adapt your entities to model that would be exported out into CSV.

// We are transforming
Transaction Entity -> Transaction CSV which is Exportable Entity 
1. We create new data class for Transaction CSV which Extends exportable
2. We write a extension to convert list of transaction entity to transaction CSV

So we have created,

data class TransactionsCSV(
    @CsvBindByName(column = "title")
    val title: String,
    @CsvBindByName(column = "amount")
    val amount: Double,
    @CsvBindByName(column = "transactionType")
    val transactionType: String,
    @CsvBindByName(column = "tag")
    val tag: String,
    @CsvBindByName(column = "date")
    val date: String,
    @CsvBindByName(column = "note")
    val note: String,
    @CsvBindByName(column = "createdAt")
    val createdAtDate: String
) : Exportable

fun List<Transaction>.toCsv() : List<TransactionsCSV> = map {
    TransactionsCSV(
        title = it.title,
        amount = it.amount,
        transactionType = it.transactionType,
        tag = it.tag,
        date = it.date,
        note = it.note,
        createdAtDate = it.createdAtDateFormat,
    )
}

Here TransactionsCSV has properties marked by @CsvBindByName(column = "<column-Name>"), it’s an open-csv annotation that automatically creates a schema that binds object to the corresponding column and row.



Step 6 : Setup Export Service

In Export Service, I had exposed a function called export(type: Exports, content: List<T>) which takes type of export you want to perform and content you need to export, to enforce that content has been designed for exporting your content need to be Exportable type.


This export function delegate its work to writeToCSV which creates the host directory, if not already created then write the transaction list to csv file.

object ExportService {

    fun <T : Exportable> export(type: Exports, content: List<T>) : Flow<Boolean> = 
      when (type) {
          is Exports.CSV -> writeToCSV<T>(type.csvConfig, content)
      }

    @WorkerThread
    private fun <T : Exportable> writeToCSV(csvConfig: CsvConfig, content: List<T>) = 
      flow<Boolean>{
        with(csvConfig) {
            
            hostPath.ifEmpty { throw IllegalStateException("Wrong Path") }
            val hostDirectory = File(hostPath)
            if (!hostDirectory.exists()) {
                hostDirectory.mkdir() // πŸ‘ˆ create directory 
            }
            
            // πŸ‘‡ create csv file
            val csvFile = File("${hostDirectory.path}/$fileName")
            val csvWriter = CSVWriter(FileWriter(csvFile)) 
            
            // πŸ‘‡ write csv file
            StatefulBeanToCsvBuilder<T>(csvWriter)
                .withSeparator(CSVWriter.DEFAULT_SEPARATOR)
                .build()
                .write(content)

            csvWriter.close()
        }
        // πŸ‘‡ emit success
        emit(true)
    }
}

Since it is a flow we can catch any failure in the catch chain no need to explicit use the try-catch, this keeps api simple and flat.


Step 7 : How to Trigger file export?

You can see the project code for demonstration from the file TransactionViewModel.kt, here is what the snippet looks like :

fun exportTransactionsToCsv() = viewModelScope.launch(IO) {
    // πŸ‘‡ state manager for loading | error | success
    _exportCsvState.value = ViewState.Loading uccess
    
    // πŸ‘‡ get all trasnaction detail from repository  
    val transactions = transactionRepo.getAllTransactions().first()
    
    // πŸ‘‡ call export function from Export serivce
    ExportService.export<TransactionsCSV>(
        type = Exports.CSV(CsvConfig()), // πŸ‘ˆ apply config + type of export
        content = transactions.toCsv() // πŸ‘ˆ send transformed data of exportable type
    ).catch { error ->
        // πŸ‘‡ handle error here
        _exportCsvState.value = ViewState.Error(error) 
    }.collect { _ ->
        // πŸ‘‡ do anything on success
        _exportCsvState.value = ViewState.Success(emptyList())
    }
}

Conclusion πŸ’†πŸ»β€β™€οΈ

That’s all fokes! from the above example, we have successfully implemented CSV export functionality.

P.S. If you have any other way to implement it or any feedback or simply want to do code review and suggest me some improvements do reach out!

Hope you find it informative and if you have any feedback or post request or want to subscribe to my mailing list forms are below.

Do consider Clap to show your appreciation, until next time. Happy Hacking! πŸ‘©β€πŸ’»


Enjoy the article?

a clap is much appreciated if you enjoyed. No sign up or cost associated :)




Chetan gupta

Hi there! call me Ch8n, I'm a mobile technology enthusiast! love Android #kotlinAlltheWay, CodingMantra: #cleanCoder #TDD #SOLID #designpatterns

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.