CellarKt is a Kotlin Multiplatform library for generating XLSX (Excel) documents entirely in-memory — no JVM spreadsheet engine, no native dependencies, just pure Kotlin.
Write-only. CellarKt creates XLSX files. It does not read or parse existing spreadsheets.
CellarKt was built to cover the common cases for a multiplatform Kotlin application. It is not a full replacement for Apache POI or other comprehensive spreadsheet libraries, but it handles the vast majority of real-world reporting needs.
Targets: Android · iOS (arm64 + simulatorArm64)
- Installation
- Quick Start
- DSL Overview
- Cell Types
- Styling
- Formulas
- Dates & Times
- Document Metadata
- Default Formats
- Named Ranges
- Style Resolution Priority
- Limitations
- Contributing
// build.gradle.kts
dependencies {
implementation("xyz.kandrac:cellar:0.0.1")
}Requirements:
- Kotlin 2.1+
- Kotlinx Coroutines 1.10+
- Android: compileSdk 37+, minSdk 26+
- iOS: arm64 / simulatorArm64
suspend fun createReport(): ByteArray = cellar {
sheet("Report") {
// Place cells at explicit coordinates (0-based)
cell("Employee Timesheet", x = 0, y = 0, horizontalSpan = 4)
// Sequential rows (left-to-right)
row(y = 1) {
cell("Name"); cell("Role"); cell("Hours"); cell("Rate")
}
// Typed convenience overloads
row(y = 2) {
cell("Alice")
cell("Engineer")
cell(40) // Int → Number
cell(42.50) // Double → Number
}
// Formula (no leading "=")
formula("C3*D3", x = 3, y = 2)
// Date (days since Unix epoch)
date(20241L, x = 0, y = 4, format = DateFormat.Custom("dd/mm/yyyy"))
// Boolean
cell(true, x = 1, y = 4)
// Error
error(ExcelError.DivisionByZero, x = 2, y = 4)
}
}.export().export() is a suspend function that runs on Dispatchers.Default. The returned ByteArray can be written to a file, shared via a content URI, or sent over the network.
The entire document is built inside a cellar { } lambda:
cellar {
metadata { ... } // Optional: author, timestamps, app info
default { ... } // Optional: document-wide font & format defaults
sheet("Sheet1") { ... } // One or more worksheets
namedRange(...) // Optional: workbook-level named ranges
}.export()
| Function | Description |
|---|---|
cell(value, x, y, horizontalSpan, verticalSpan, style) |
Place at explicit (x, y) coordinates with optional merge |
row(y, startX = 0) { cell(...) } |
Sequential cells left-to-right |
column(x, startY = 0) { cell(...) } |
Sequential cells top-to-bottom |
You can also use columnWidth(x, width) and rowHeight(y, height) to control dimensions.
Cell overlap (including merged cells) is detected at build time — an IllegalStateException is thrown when two cells occupy the same range.
| Type | DSL Function | OOXML Representation |
|---|---|---|
| Text | cell("Hello") |
Shared string or inline string |
| Number (Int/Double/Long/Float) | cell(42), cell(3.14) |
Numeric cell |
| Boolean | cell(true) |
t="b" |
| Formula | formula("SUM(A1:A10)") |
<f> element (no leading =) |
| Array Formula | cell(CellValue.ArrayFormula(...)) |
<f t="array"> |
| Date | date(epochDay) |
Serial number with date format |
| Date+Time | dateTime(epochMillis) |
Fractional serial with datetime format |
| Time | time(millisOfDay) |
Fractional serial with time format |
| Error | error(ExcelError.Value) |
t="e" |
| Empty | Omitted | Empty cell |
Use the style parameter on any cell() call to apply fonts, borders, background, and alignment:
cell("Header", x = 0, y = 0, style = CellStyle(
font = Font(size = 14, bold = true, color = ExcelColor.White),
background = Background(ExcelColor("FF1A73E8")),
border = Border.All,
horizontalAlignment = HorizontalAlignment.Center,
verticalAlignment = VerticalAlignment.Center,
))Font(
size = 11, // Font size in points
bold = false,
italic = false,
underline = false,
color = null, // ExcelColor ARGB or null for default
name = "Calibri", // Font family name
)Border(
top = BorderLine.Thin,
bottom = BorderLine.Thin,
left = BorderLine.Thin,
right = BorderLine.Thin,
)
// Shorthand:
Border.All // Thin on all sides
Border.AllThick // Thick on all sidesBackground(color = ExcelColor("FFFF0000")) // ARGB hex
Background() // Transparent (default)HorizontalAlignment.Start | Center | End
VerticalAlignment.Top | Center | Bottom
NumberFormat.Integer // "#,##0"
NumberFormat.Decimal2 // "#,##0.00"
NumberFormat.Percent // "0%"
NumberFormat.Dollar // "$#,##0.00"
NumberFormat.Euro // "€#,##0.00"
NumberFormat.Scientific // "0.00E+00"
// … plus many more, or any custom format stringExample:
cell(1234567.89, x = 0, y = 0, style = CellStyle(numberFormat = NumberFormat.Decimal2))Formulas are written without the leading =:
formula("SUM(C2:C10)", x = 0, y = 11)
formula("A1*B1", x = 2, y = 3)For array formulas (CSE, Ctrl+Shift+Enter) use the CellValue.ArrayFormula type directly:
cell(CellValue.ArrayFormula("SUM(B2:B11*C2:C11)", arrayRef = "A12"), x = 0, y = 11)Date/time values are specified in terms of the Unix epoch and automatically converted to Excel serial numbers:
| DSL Function | Input | Excel Representation |
|---|---|---|
date(epochDay, format) |
Days since 1970-01-01 | Integer serial |
dateTime(epochMillis, format) |
ms since 1970-01-01 UTC | Fractional serial |
time(millisOfDay, format) |
ms since midnight (0–86,399,999) | Decimal 0.0–1.0 |
// Using kotlinx-datetime:
date(LocalDate(2025, 6, 1).toEpochDays().toLong(), x = 0, y = 0)
dateTime(Clock.System.now().toEpochMilliseconds(), x = 0, y = 0)
time(LocalTime(14, 30).toMillisecondOfDay().toLong(), x = 0, y = 0)Built-in display presets in DateFormat:
| Preset | Format String | Excel Built-in ID |
|---|---|---|
ShortDate |
mm-dd-yy |
14 |
MediumDate |
d-mmm-yy |
15 |
MonthYear |
mmm-yy |
17 |
ShortDateTime |
m/d/yy h:mm |
22 |
ShortTime |
h:mm |
20 |
LongTime |
h:mm:ss |
21 |
Custom("dd/MM/yyyy") |
Any format code | 164+ |
Optional document properties written to docProps/core.xml and docProps/app.xml:
cellar {
metadata {
author = "Alice"
lastModifiedBy = "Bob"
createdEpochMillis = 1_700_000_000_000L
modifiedEpochMillis = 1_700_000_000_000L
appName = "ReportGenerator"
appVersion = "1.0"
company = "Acme Corp"
}
// …
}All fields are nullable and omitted from the output when unset.
Set document-wide defaults that apply to all cells unless overridden:
cellar {
default {
font = Font(size = 12, name = "Arial")
numberFormat = NumberFormat.Decimal2
dateFormat = DateFormat.Custom("dd/MM/yyyy")
dateTimeFormat = DateFormat.Custom("dd/MM/yyyy HH:mm")
timeFormat = DateFormat.LongTime
}
// …
}Define workbook-level names that can be used in formulas:
cellar {
sheet("Data") { /* … */ }
namedRange("TaxRate", ref = "Data!$C$1")
namedRange("TotalRevenue", ref = "Data!$B$2:$B$10")
}
// Usage in a formula:
formula("TotalRevenue * TaxRate", x = 0, y = 5)Settings are resolved in this order (highest priority first):
| Property | Cell Style | Document Default | Library Fallback |
|---|---|---|---|
| Font | CellStyle.font |
CellarDefaults.font |
Font() (11pt Calibri) |
| Number format | CellStyle.numberFormat |
CellarDefaults.numberFormat |
General |
| Date format | CellStyle.numberFormat / CellStyle.dateFormat / cell value format |
CellarDefaults.dateFormat/timeFormat/dateTimeFormat |
ShortDate / ShortTime / ShortDateTime |
| Border | CellStyle.border |
— | Border() (none) |
| Background | CellStyle.background |
— | Background() (none) |
CellarKt is intentionally focused on the common spreadsheet generation use cases. Known missing features include:
- Rich text, text wrapping, rotation, indentation
- Sheet tab color, freeze panes, zoom, auto-filter, print settings
- Excel tables (ListObject), conditional formatting, data validation
- Images, charts, shapes, sparklines
- Cell comments, hyperlinks, sheet protection
- ZIP compression (uses STORED method only, no DEFLATE)
- Reading/parsing existing XLSX files
See missing_features.md for the full list.
Large spreadsheets. CellarKt builds the entire document in memory, so it is not suitable for generating very large spreadsheets (millions of rows).
Contributions are welcome! Whether it's a bug report, a feature request, or a pull request — please open an issue or submit a PR.
This project is licensed under the MIT License.