Android (Kotlin)将数据导出并自动生成Excel表格

代码的世界虽逻辑繁华,却又大道至简。

背景

大家或许有过跟我类似的一些开发经历,例如涉及到相关报表业务,有时候我们需要将某段时间内的数据以Excel文件的格式导出或存储,从而以便更加直观的统计或分析数据所表现出的深沉涵义。

支持库

我们知道Apache官网开发组件下有个Java版本的POI库可以用来生成Excel文件,但是经过实践,在Android端的gradle配置文件中直接引用,开发阶段编译时不会有任何问题,但是程序Run后,App直接奔溃或出现一系列类似于javax.xml.xxxx.ClassNotFoundException的异常,导致App无法正常工作。
那么Android端如何或者说还有其他库能支持生成Excel么?很幸运是有的,github已有大神将Apache的POI裁剪了一个版本用以支持Android, link: android5xlsx

环境与配置

1.去android5xlsx将poi-3.12-android-a.jar 和 poi-ooxml-schemas-3.12-20150511-a.jar下载下来并放到libs文件夹下

image.png

2.在项目的app根目录gradle文件里配置

dependencies {
    implementation files('libs/poi-3.12-android-a.jar')
    implementation files('libs/poi-ooxml-schemas-3.12-20150511-a.jar')
}

动态生成单元格数据核心算法

private fun createCell(currentSheet: Sheet, cellIndex: Int, saveValue: String) {
        //create fist row.
        val lastRowNum = currentSheet.lastRowNum
        if (lastRowNum == 0) {
            val firstRow = currentSheet.createRow(1)
            val firstCell = firstRow.createCell(cellIndex)
            firstCell.setCellValue(saveValue)
            return
        }
        
        //check is valid.
        val titleRowNum = 1
        val physicalRowNum = currentSheet.physicalNumberOfRows - titleRowNum
        if (physicalRowNum < 1) {
            return
        }
        
        //check and create new max row.
        val maxRow = currentSheet.getRow(physicalRowNum)
        val maxCell = maxRow.getCell(cellIndex)
        if (maxCell != null) {
            val newRow = currentSheet.createRow(physicalRowNum + 1)
            val newCell = newRow.createCell(cellIndex)
            newCell.setCellValue(saveValue)
            return
        }
        
        //populate first 'null' and next 'null' value of cell between the first and last rows.
        for (i in physicalRowNum downTo 1) {
            val popRow = currentSheet.getRow(i)
            val popCell = popRow.getCell(cellIndex)
            if (popCell != null) {
                val nextNullOfRow = currentSheet.getRow(i + 1)
                val nextNullOfCell = nextNullOfRow.createCell(cellIndex)
                nextNullOfCell.setCellValue(saveValue)
                break
            }
            if (i == 1) {
                val firstRow = currentSheet.getRow(1)
                val firstCell = firstRow.createCell(cellIndex)
                firstCell.setCellValue(saveValue)
                break
            }
        }
        
    }

完整可执行代码片段

package com.patrick.utils

import android.content.Context
import android.os.*
import android.util.Log
import kotlinx.coroutines.*
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.xssf.streaming.SXSSFWorkbook
import java.io.File
import java.io.FileOutputStream
import java.lang.NullPointerException
import java.lang.ref.WeakReference
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicInteger

/**
 * Represents excel create tool.
 * @dependency:
 * https://github.com/andruhon/android5xlsx
 */
object ExcelUtil {
    private const val TAG_LOG = "log_cell_data"
    private const val mSplitTag = "|"
    private const val mDelayShort = 500L
    private var mInterruptDuration = 0L
    private var mContextWeakReference: WeakReference<Context>? = null
    private var mLinkedBlockingQueue = LinkedBlockingQueue<String>(16)
    private var mSXSSFWorkbook: SXSSFWorkbook? = null
    private var mExcelPathPrefix = ""
    private var mWriteCounter = AtomicInteger(0)

    //condition
    private const val XNumber = 15

    @Volatile
    private var mStop: Boolean = false
        private set

    enum class CellTitleName(val cellIndex: Int) {
        A(1),
        B(2),
        C(3),
    }

    private fun initExcelTool() {
        mSXSSFWorkbook = SXSSFWorkbook(128)
    }

    fun putCellData(cellTile: CellTitleName, cellValue: String) {
        if (mContextWeakReference == null || mContextWeakReference?.get() == null) {
            throw NullPointerException("please call ExcelUtil.init(...) first")
        }
        mLinkedBlockingQueue.put("${cellTile.name}|$cellValue")
    }

    fun init(context: Context?) {
        initExcelTool()
        mContextWeakReference = WeakReference(context)
        mExcelPathPrefix = "${
            mContextWeakReference?.get()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
        }${File.separator}measureResult"
        startLoop()
    }
    
    private fun fileHandle() {
        val file = File("$mExcelPathPrefix.xlsx")
        if (file.exists()) {
            file.copyTo(File("${mExcelPathPrefix}_last.xlsx"), true)
            file.delete()
        }
    }

    private fun startLoop() {
        mStop = false
        GlobalScope.launch {
            fileHandle()
            while (true) {
                try {
                    if (mStop) {
                        break
                    }
                    //interrupt but not fatal error that means app is still alive, then write data which has created within 6 min.
                    if (!mStop && mInterruptDuration > 10000 * 60 * 5) {
                        writeIntoExcelFile(mSXSSFWorkbook)
                        break
                    }
                    consumer()
                    delay(mDelayShort)
                    yield()
                } catch (e: CancellationException) {
                    break
                }
            }
        }
    }

    private suspend fun consumer() {
        mInterruptDuration = 0L
        val durationUnit: String = mLinkedBlockingQueue.take()
        val cellInfos = durationUnit.split(mSplitTag)
        Log.d(TAG_LOG, "${cellInfos[0]}:${cellInfos[1]}")
        prepareForXLSXWrite(cellInfos)
    }

    /**
     * create cell data before execute write excel file.
     * @param cellInfos | String[2]: format "cellTileA|cellValue"
     */
    private fun prepareForXLSXWrite(cellInfos: List<String>, count: Int = 0) {
        val sheetName = "Measurement"
        /**
         *
         * do not coding as below ,otherwise it work only once.next time will Exception:streamed closed.
         * <code>
         * object?.use{
         * ``````
         * `````
         * }
         * </code>
         */
        mSXSSFWorkbook?.let { workbook ->
            var currentSheet: Sheet? = workbook.getSheet(sheetName)
            if (currentSheet == null) {
                currentSheet = workbook.createSheet(sheetName)
                currentSheet.defaultRowHeight = (currentSheet.defaultRowHeight * 2).toShort()
                currentSheet.defaultColumnWidth = currentSheet.defaultColumnWidth * 2
                //create title row.
                val titleRow = currentSheet.createRow(0)
                //create title of 'A'.
                val loginCell = titleRow.createCell(CellTitleName.A.cellIndex)
                loginCell.setCellValue(CellTitleName.A.name)
                //create title of 'B'.
                val tabSwitchCell = titleRow.createCell(CellTitleName.B.cellIndex)
                tabSwitchCell.setCellValue(CellTitleName.B.name)
                //create title of 'C'.
                val tabPopupCell = titleRow.createCell(CellTitleName.C.cellIndex)
                tabPopupCell.setCellValue(CellTitleName.C.name)
            }
            currentSheet?.let {
                when (cellInfos[0]) {
                    CellTitleName.A.name -> {
                        createCell(it, CellTitleName.A.cellIndex, cellInfos[1])
                    }
                    CellTitleName.B.name -> {
                        createCell(it, CellTitleName.B.cellIndex, cellInfos[1])
                    }
                    CellTitleName.C.name -> {
                        createCell(it, CellTitleName.C.cellIndex, cellInfos[1])
                    }
                    else -> {
                    }
                }
                mWriteCounter.incrementAndGet()
            }
            if (mWriteCounter.get() == XNumber) {
                writeIntoExcelFile(workbook)
            }
        } ?: kotlin.run {
            if (count < 1) {
                mSXSSFWorkbook = SXSSFWorkbook()
                prepareForXLSXWrite(cellInfos, 999)
            }
        }
    }

    /**
     * 1.check
     * 2.create a new row and a new cell if need
     * 3.populate data into cell
     * @param currentSheet | Sheet
     * @param saveValue | String
     */
    private fun createCell(currentSheet: Sheet, cellIndex: Int, saveValue: String) {
        //create fist row.
        val lastRowNum = currentSheet.lastRowNum
        if (lastRowNum == 0) {
            val firstRow = currentSheet.createRow(1)
            val firstCell = firstRow.createCell(cellIndex)
            firstCell.setCellValue(saveValue)
            return
        }
        val titleRowNum = 1
        val physicalRowNum = currentSheet.physicalNumberOfRows - titleRowNum
        if (physicalRowNum < 1) {
            return
        }
        //check and create new max row.
        val maxRow = currentSheet.getRow(physicalRowNum)
        val maxCell = maxRow.getCell(cellIndex)
        if (maxCell != null) {
            val newRow = currentSheet.createRow(physicalRowNum + 1)
            val newCell = newRow.createCell(cellIndex)
            newCell.setCellValue(saveValue)
            return
        }
        //populate first 'null' and next 'null' value of cell between the first and last rows.
        for (i in physicalRowNum downTo 1) {
            val popRow = currentSheet.getRow(i)
            val popCell = popRow.getCell(cellIndex)
            if (popCell != null) {
                val nextNullOfRow = currentSheet.getRow(i + 1)
                val nextNullOfCell = nextNullOfRow.createCell(cellIndex)
                nextNullOfCell.setCellValue(saveValue)
                break
            }
            if (i == 1) {
                val firstRow = currentSheet.getRow(1)
                val firstCell = firstRow.createCell(cellIndex)
                firstCell.setCellValue(saveValue)
                break
            }
        }
    }

    /**
     * execute write
     * @param aSXSSFWorkbook | SXSSFWorkbook?
     */
    private fun writeIntoExcelFile(aSXSSFWorkbook: SXSSFWorkbook?) {
        aSXSSFWorkbook?.let { workbook ->
            val filePath = "$mExcelPathPrefix.xlsx"
            val outputStream = FileOutputStream(File(filePath))
            try {
                workbook.write(outputStream)
                outputStream.flush()
            } catch (e: Exception) {
                Log.d(TAG_LOG, "${e.printStackTrace()}")
            } finally {
                outputStream.close()
                workbook.close()
                mStop = true
                mWriteCounter.set(0)
                mSXSSFWorkbook = null
            }
        }
    }
}
我也是有底线的,感谢您的耐心。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容