
Read and write .xlsx spreadsheets with a pure implementation, including cell styling, number/date formats, formulas, merged cells, row/column ops, find/replace and in-memory encode/decode.
Read and write Excel (.xlsx) spreadsheets from Kotlin Multiplatform — pure Kotlin, no Apache POI.
⚠️ Experimental — Kexcel is under active development. The API is unstable and may change without notice.
Kexcel is a Kotlin Multiplatform library for reading and writing Excel files (.xlsx,
the Office Open XML spreadsheet format). It is written in pure Kotlin with no platform-specific
dependencies, so the same spreadsheet code runs on JVM, Android and iOS — including inside a
Compose Multiplatform app.
Every other Kotlin Excel library is a wrapper around Apache POI, which is JVM-only. Kexcel
parses and writes the .xlsx format itself, which makes it a usable Apache POI alternative for
KMP/KMM projects that need spreadsheets outside the JVM.
📖 Documentation: guides, tutorials and the full API reference live at shreyashkore.github.io/kexcel.
Kexcel is a Kotlin port of the Dart excel library by justkawal.
.xlsx — parse an existing workbook, edit it, write it back out.iosArm64, iosSimulatorArm64) published today; wasmJs and macosArm64 build.commonMain.=SUM(A1:A10).encode() returns ByteArray; no filesystem access required.Add Kexcel to your project.
dependencies {
implementation("com.gyanoba.kexcel:kexcel:0.0.2")
}dependencies {
implementation 'com.gyanoba.kexcel:kexcel:0.0.2'
}The Excel class is the single entry point and represents the whole workbook. Use its companion object to create or load one:
// Create a new, empty workbook (contains a single sheet named "Sheet1")
val excel = Excel.createExcel()
// Load a workbook from a ByteArray
val excel = Excel.decodeBytes(bytes)
// Load a workbook from an InputStream
val excel = Excel.decodeStream(inputStream)| Method | Description |
|---|---|
Excel.createExcel(): Excel |
Creates a new, empty workbook with one sheet, Sheet1. |
Excel.decodeBytes(data: ByteArray): Excel |
Loads a workbook from raw .xlsx bytes. |
Excel.decodeStream(input: InputStream): Excel |
Loads a workbook from an input stream. |
A complete, runnable version of every snippet below lives in the sample app (
sample/sharedUI/.../ExcelSamples.kt). Run it with./gradlew :sample:desktopApp:run.
val excel = Excel.createExcel()
// Get a sheet by name. If it does not exist, it is created automatically.
val sheet = excel["Sheet1"]
// All sheets as a Map<String, Sheet>
val sheets: Map<String, Sheet> = excel.getSheets()
// Dimensions of a sheet
println("${sheet.maxRows} rows x ${sheet.maxColumns} columns")Cells are addressed with a CellIndex. Rows and columns are 0-based.
CellIndex.indexByString("A1") // column 0, row 0
CellIndex.indexByColumnRow(columnIndex = 2, rowIndex = 4) // "C5"Every value is wrapped in a CellValue. Update a cell either through the workbook or the sheet:
val sheet = excel["Sheet1"]
// Via the sheet
sheet.updateCell(CellIndex.indexByString("A1"), TextCellValue("Hello"))
// Via the workbook (equivalent)
excel.updateCell("Sheet1", CellIndex.indexByString("A2"), IntCellValue(42))Available CellValue types:
TextCellValue("Some text")
IntCellValue(42)
DoubleCellValue(3.14)
BoolCellValue(true)
DateCellValue(year = 2026, month = 7, day = 9)
TimeCellValue(hour = 14, minute = 30, second = 0)
DateTimeCellValue(year = 2026, month = 7, day = 9, hour = 14, minute = 30)
FormulaCellValue("=SUM(A1:A10)")Writing null clears a cell.
val sheet = excel["Sheet1"]
// A single cell
val value: CellValue? = sheet.cell(CellIndex.indexByString("A1")).value
// Iterate every row (each row is a List<Data?>, nulls are empty cells)
for (row in sheet.rows) {
for (data in row) {
println(data?.value)
}
}
// A range, as raw values
val values: List<List<Any?>> =
sheet.selectRangeValues(
CellIndex.indexByString("A1"),
CellIndex.indexByString("C3"),
)
// ...or with a string range
val values2 = sheet.selectRangeValuesWithString("A1:C3")CellValue is a sealed type — pattern-match to read the underlying value:
when (val v = sheet.cell(CellIndex.indexByString("A1")).value) {
is TextCellValue -> v.value.toString()
is IntCellValue -> v.value
is DoubleCellValue -> v.value
is BoolCellValue -> v.value
is DateCellValue -> v.asLocalDate()
else -> null
}Pass a CellStyle when updating a cell. Styles cover colors, fonts, alignment,
borders and number formats.
val style = CellStyle(
fontColorHex = ExcelColor.white,
backgroundColorHex = ExcelColor.blue,
bold = true,
italic = false,
fontSize = 14,
horizontalAlign = HorizontalAlign.Center,
verticalAlign = VerticalAlign.Center,
underline = Underline.Single,
leftBorder = Border(borderStyle = BorderStyle.Thin, borderColorHex = ExcelColor.black),
rightBorder = Border(borderStyle = BorderStyle.Thin, borderColorHex = ExcelColor.black),
numberFormat = NumFormat.standard_2, // "0.00"
)
sheet.updateCell(CellIndex.indexByString("A1"), TextCellValue("Header"), cellStyle = style)
// Styles are immutable — derive variants with copyWith
val redText = style.copyWith(fontColorHexVal = ExcelColor.red)Colors come from ExcelColor (named Material colors like ExcelColor.red, or
ExcelColor.fromHexString("FF00FF00")). Number formats come from NumFormat
(built-in standard_* constants, or CustomNumericNumFormat("0.00%")).
// Via a FormulaCellValue
sheet.updateCell(CellIndex.indexByString("A4"), FormulaCellValue("=SUM(A1:A3)"))
// Or on the Data object directly
sheet.cell(CellIndex.indexByString("A5")).setFormula("=AVERAGE(A1:A3)")// Append a row after the last filled row
excel.appendRow("Sheet1", listOf(TextCellValue("a"), IntCellValue(1)))
// Write an iterable at a specific row (optionally offset by a starting column)
excel.insertRowIterables(
"Sheet1",
listOf(TextCellValue("x"), TextCellValue("y")),
rowIndex = 5,
startingColumn = 1,
)
// Insert / remove blank rows and columns
excel.insertRow("Sheet1", rowIndex = 0)
excel.removeRow("Sheet1", rowIndex = 3)
excel.insertColumn("Sheet1", columnIndex = 0)
excel.removeColumn("Sheet1", columnIndex = 2)excel.merge(
"Sheet1",
CellIndex.indexByString("A1"),
CellIndex.indexByString("C1"),
customValue = TextCellValue("Merged title"),
)
val merged: List<String> = excel.getMergedCells("Sheet1") // e.g. ["A1:C1"]
excel.unMerge("Sheet1", "A1:C1")val sheet = excel["Sheet1"]
sheet.setColumnWidth(columnIndex = 0, columnWidth = 30.0)
sheet.setRowHeight(rowIndex = 0, rowHeight = 24.0)
sheet.setColumnAutoFit(columnIndex = 1)
sheet.setDefaultColumnWidth(15.0)
sheet.setDefaultRowHeight(18.0)Replaces text in TextCellValue cells. source is a Regex; the return value is the number of replacements.
val count = excel.findAndReplace("Sheet1", Regex("Widget"), "Gadget")excel.copy("Sheet1", "Backup") // copy a sheet
excel.rename("Backup", "Archive") // rename a sheet
excel.delete("Archive") // delete (keeps at least one sheet)
excel.setDefaultSheet("Sheet1") // sheet shown first when the file opens
val name: String? = excel.getDefaultSheet()
// Link two names to the same underlying sheet, then break the link
excel.link("Alias", excel["Sheet1"])
excel.unLink("Alias")// Serialize the workbook to .xlsx bytes (in memory)
val bytes: ByteArray? = excel.encode()
File("out.xlsx").writeBytes(bytes) // Write to disk to save as a .xlsx fileFull documentation is hosted at shreyashkore.github.io/kexcel:
The Usage section above covers the common operations. For a full,
runnable tour of the API see the sample app source in
ExcelSamples.kt.
The sample app runs each API example and prints the results.
./gradlew :sample:desktopApp:run
sample/androidApp configuration.sample/iosApp/iosApp.xcodeproj in Xcode and run../gradlew :kexcel:build
./gradlew :kexcel:allTests
./gradlew :kexcel:jvmTest
No. Kexcel is pure Kotlin and depends only on multiplatform libraries — kmp-zip
for the ZIP container and Ksoup for XML. That is why it runs
outside the JVM: Apache POI, and every Kotlin wrapper built on top of it, is JVM-only.
Yes. JVM, Android and iOS are published targets, so the same commonMain code works on all of
them — including in a Compose Multiplatform app.
No — only .xlsx (Office Open XML), the format written by Excel 2007 and later, Google Sheets,
Numbers and LibreOffice. The legacy binary .xls format is out of scope.
Not yet. Kexcel is 0.0.x and explicitly experimental; the API may change between releases. Bug
reports and pull requests are very welcome.
Issues and pull requests are welcome — see the contributing guide. If Kexcel is useful to you, a ⭐ helps other people find it.
Kexcel is released under the MIT License.
Read and write Excel (.xlsx) spreadsheets from Kotlin Multiplatform — pure Kotlin, no Apache POI.
⚠️ Experimental — Kexcel is under active development. The API is unstable and may change without notice.
Kexcel is a Kotlin Multiplatform library for reading and writing Excel files (.xlsx,
the Office Open XML spreadsheet format). It is written in pure Kotlin with no platform-specific
dependencies, so the same spreadsheet code runs on JVM, Android and iOS — including inside a
Compose Multiplatform app.
Every other Kotlin Excel library is a wrapper around Apache POI, which is JVM-only. Kexcel
parses and writes the .xlsx format itself, which makes it a usable Apache POI alternative for
KMP/KMM projects that need spreadsheets outside the JVM.
📖 Documentation: guides, tutorials and the full API reference live at shreyashkore.github.io/kexcel.
Kexcel is a Kotlin port of the Dart excel library by justkawal.
.xlsx — parse an existing workbook, edit it, write it back out.iosArm64, iosSimulatorArm64) published today; wasmJs and macosArm64 build.commonMain.=SUM(A1:A10).encode() returns ByteArray; no filesystem access required.Add Kexcel to your project.
dependencies {
implementation("com.gyanoba.kexcel:kexcel:0.0.2")
}dependencies {
implementation 'com.gyanoba.kexcel:kexcel:0.0.2'
}The Excel class is the single entry point and represents the whole workbook. Use its companion object to create or load one:
// Create a new, empty workbook (contains a single sheet named "Sheet1")
val excel = Excel.createExcel()
// Load a workbook from a ByteArray
val excel = Excel.decodeBytes(bytes)
// Load a workbook from an InputStream
val excel = Excel.decodeStream(inputStream)| Method | Description |
|---|---|
Excel.createExcel(): Excel |
Creates a new, empty workbook with one sheet, Sheet1. |
Excel.decodeBytes(data: ByteArray): Excel |
Loads a workbook from raw .xlsx bytes. |
Excel.decodeStream(input: InputStream): Excel |
Loads a workbook from an input stream. |
A complete, runnable version of every snippet below lives in the sample app (
sample/sharedUI/.../ExcelSamples.kt). Run it with./gradlew :sample:desktopApp:run.
val excel = Excel.createExcel()
// Get a sheet by name. If it does not exist, it is created automatically.
val sheet = excel["Sheet1"]
// All sheets as a Map<String, Sheet>
val sheets: Map<String, Sheet> = excel.getSheets()
// Dimensions of a sheet
println("${sheet.maxRows} rows x ${sheet.maxColumns} columns")Cells are addressed with a CellIndex. Rows and columns are 0-based.
CellIndex.indexByString("A1") // column 0, row 0
CellIndex.indexByColumnRow(columnIndex = 2, rowIndex = 4) // "C5"Every value is wrapped in a CellValue. Update a cell either through the workbook or the sheet:
val sheet = excel["Sheet1"]
// Via the sheet
sheet.updateCell(CellIndex.indexByString("A1"), TextCellValue("Hello"))
// Via the workbook (equivalent)
excel.updateCell("Sheet1", CellIndex.indexByString("A2"), IntCellValue(42))Available CellValue types:
TextCellValue("Some text")
IntCellValue(42)
DoubleCellValue(3.14)
BoolCellValue(true)
DateCellValue(year = 2026, month = 7, day = 9)
TimeCellValue(hour = 14, minute = 30, second = 0)
DateTimeCellValue(year = 2026, month = 7, day = 9, hour = 14, minute = 30)
FormulaCellValue("=SUM(A1:A10)")Writing null clears a cell.
val sheet = excel["Sheet1"]
// A single cell
val value: CellValue? = sheet.cell(CellIndex.indexByString("A1")).value
// Iterate every row (each row is a List<Data?>, nulls are empty cells)
for (row in sheet.rows) {
for (data in row) {
println(data?.value)
}
}
// A range, as raw values
val values: List<List<Any?>> =
sheet.selectRangeValues(
CellIndex.indexByString("A1"),
CellIndex.indexByString("C3"),
)
// ...or with a string range
val values2 = sheet.selectRangeValuesWithString("A1:C3")CellValue is a sealed type — pattern-match to read the underlying value:
when (val v = sheet.cell(CellIndex.indexByString("A1")).value) {
is TextCellValue -> v.value.toString()
is IntCellValue -> v.value
is DoubleCellValue -> v.value
is BoolCellValue -> v.value
is DateCellValue -> v.asLocalDate()
else -> null
}Pass a CellStyle when updating a cell. Styles cover colors, fonts, alignment,
borders and number formats.
val style = CellStyle(
fontColorHex = ExcelColor.white,
backgroundColorHex = ExcelColor.blue,
bold = true,
italic = false,
fontSize = 14,
horizontalAlign = HorizontalAlign.Center,
verticalAlign = VerticalAlign.Center,
underline = Underline.Single,
leftBorder = Border(borderStyle = BorderStyle.Thin, borderColorHex = ExcelColor.black),
rightBorder = Border(borderStyle = BorderStyle.Thin, borderColorHex = ExcelColor.black),
numberFormat = NumFormat.standard_2, // "0.00"
)
sheet.updateCell(CellIndex.indexByString("A1"), TextCellValue("Header"), cellStyle = style)
// Styles are immutable — derive variants with copyWith
val redText = style.copyWith(fontColorHexVal = ExcelColor.red)Colors come from ExcelColor (named Material colors like ExcelColor.red, or
ExcelColor.fromHexString("FF00FF00")). Number formats come from NumFormat
(built-in standard_* constants, or CustomNumericNumFormat("0.00%")).
// Via a FormulaCellValue
sheet.updateCell(CellIndex.indexByString("A4"), FormulaCellValue("=SUM(A1:A3)"))
// Or on the Data object directly
sheet.cell(CellIndex.indexByString("A5")).setFormula("=AVERAGE(A1:A3)")// Append a row after the last filled row
excel.appendRow("Sheet1", listOf(TextCellValue("a"), IntCellValue(1)))
// Write an iterable at a specific row (optionally offset by a starting column)
excel.insertRowIterables(
"Sheet1",
listOf(TextCellValue("x"), TextCellValue("y")),
rowIndex = 5,
startingColumn = 1,
)
// Insert / remove blank rows and columns
excel.insertRow("Sheet1", rowIndex = 0)
excel.removeRow("Sheet1", rowIndex = 3)
excel.insertColumn("Sheet1", columnIndex = 0)
excel.removeColumn("Sheet1", columnIndex = 2)excel.merge(
"Sheet1",
CellIndex.indexByString("A1"),
CellIndex.indexByString("C1"),
customValue = TextCellValue("Merged title"),
)
val merged: List<String> = excel.getMergedCells("Sheet1") // e.g. ["A1:C1"]
excel.unMerge("Sheet1", "A1:C1")val sheet = excel["Sheet1"]
sheet.setColumnWidth(columnIndex = 0, columnWidth = 30.0)
sheet.setRowHeight(rowIndex = 0, rowHeight = 24.0)
sheet.setColumnAutoFit(columnIndex = 1)
sheet.setDefaultColumnWidth(15.0)
sheet.setDefaultRowHeight(18.0)Replaces text in TextCellValue cells. source is a Regex; the return value is the number of replacements.
val count = excel.findAndReplace("Sheet1", Regex("Widget"), "Gadget")excel.copy("Sheet1", "Backup") // copy a sheet
excel.rename("Backup", "Archive") // rename a sheet
excel.delete("Archive") // delete (keeps at least one sheet)
excel.setDefaultSheet("Sheet1") // sheet shown first when the file opens
val name: String? = excel.getDefaultSheet()
// Link two names to the same underlying sheet, then break the link
excel.link("Alias", excel["Sheet1"])
excel.unLink("Alias")// Serialize the workbook to .xlsx bytes (in memory)
val bytes: ByteArray? = excel.encode()
File("out.xlsx").writeBytes(bytes) // Write to disk to save as a .xlsx fileFull documentation is hosted at shreyashkore.github.io/kexcel:
The Usage section above covers the common operations. For a full,
runnable tour of the API see the sample app source in
ExcelSamples.kt.
The sample app runs each API example and prints the results.
./gradlew :sample:desktopApp:run
sample/androidApp configuration.sample/iosApp/iosApp.xcodeproj in Xcode and run../gradlew :kexcel:build
./gradlew :kexcel:allTests
./gradlew :kexcel:jvmTest
No. Kexcel is pure Kotlin and depends only on multiplatform libraries — kmp-zip
for the ZIP container and Ksoup for XML. That is why it runs
outside the JVM: Apache POI, and every Kotlin wrapper built on top of it, is JVM-only.
Yes. JVM, Android and iOS are published targets, so the same commonMain code works on all of
them — including in a Compose Multiplatform app.
No — only .xlsx (Office Open XML), the format written by Excel 2007 and later, Google Sheets,
Numbers and LibreOffice. The legacy binary .xls format is out of scope.
Not yet. Kexcel is 0.0.x and explicitly experimental; the API may change between releases. Bug
reports and pull requests are very welcome.
Issues and pull requests are welcome — see the contributing guide. If Kexcel is useful to you, a ⭐ helps other people find it.
Kexcel is released under the MIT License.