Building a Cross-Platform Invoice Generator with Compose Multiplatform & Kotlin Multiplatform

Muhammad Khubaib Imtiaz
4 min readJun 12, 2024

--

Photo by Nathana Rebouças on Unsplash

Building a Invoice generator with Compose & Kotlin Multiplatform can be a tricky thing to do. But Today, I’ll write a full detailed guide about creating a Invoice generator For Kotlin and Compose Multiplatform. So, Let’s get started.

In this article, We are going to use the expect and actual mechanism of the Compose Multiplatform & Kotlin Multiplatform to provide the Native functionality to each platform.

I’m generating some custom invoices for one of mine open source Project Flexi-Store-KMP. Flexi Store KMP is a Ecommerce Platform that is developed using Compose Multiplatform with a Custom Server using Ktor.

Common Module Code:
In the Common Main Module, We need to define the expect and actual functions to get the native functionality of each platforms.

// CommonMain Path:  composeApp/src/commonMain/kotlin/org/flexi/app/App.kt
expect fun generateInvoicePdf(order: Order): ByteArray
expect fun saveInvoiceToFile(data: ByteArray, fileName: String)

Android Module Code:

// Android Main Path: composeApp/src/androidMain/kotlin/org/flexi/app/App.android.kt

actual fun generateInvoicePdf(order: Order): ByteArray {
val pdfDocument = android.graphics.pdf.PdfDocument()
val pageInfo =
android.graphics.pdf.PdfDocument.PageInfo.Builder(595, 842, 1).create() // A4 size
val page = pdfDocument.startPage(pageInfo)

val canvas = page.canvas
val paint = android.graphics.Paint()
paint.color = android.graphics.Color.BLACK
paint.textSize = 12f

canvas.drawText("Invoice", 100f, 50f, paint)
canvas.drawText("Order #${order.id}", 100f, 70f, paint)

canvas.drawText("Customer ID: #${order.userId}", 100f, 100f, paint)

canvas.drawText("Order Date:", 100f, 130f, paint)
canvas.drawText(order.orderDate, 200f, 130f, paint)
canvas.drawText("Delivery Date:", 100f, 150f, paint)
canvas.drawText(order.deliveryDate, 200f, 150f, paint)

canvas.drawText("Product", 100f, 180f, paint)
canvas.drawText("Quantity", 250f, 180f, paint)
canvas.drawText("Price", 350f, 180f, paint)
var yPosition = 200f
canvas.drawText(order.productIds.toString(), 100f, yPosition, paint)
canvas.drawText(order.totalQuantity.toString(), 250f, yPosition, paint)
canvas.drawText("${order.totalPrice}", 350f, yPosition, paint)
yPosition += 20f

canvas.drawText("Total:", 350f, yPosition + 20f, paint)
canvas.drawText(order.totalPrice.toString(), 400f, yPosition + 20f, paint)

pdfDocument.finishPage(page)

val outputStream = java.io.ByteArrayOutputStream()
pdfDocument.writeTo(outputStream)
pdfDocument.close()

return outputStream.toByteArray()
}

actual fun saveInvoiceToFile(data: ByteArray, fileName: String) {
val context = AndroidApp.INSTANCE.applicationContext
val file = File(context.getExternalFilesDir(null), fileName)
file.writeBytes(data)
val uri = androidx.core.content.FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
file
)
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/pdf")
addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}

iOS Module Code:

// iosMain: composeApp/src/iosMain/kotlin/org/flexi/app/App.ios.kt

actual fun generateInvoicePdf(order: Order): ByteArray {
val data = PDFGenerator.generateInvoice(
orderId = order.id,
userId = order.userId,
orderDate = order.orderDate,
deliveryDate = order.deliveryDate,
productIds = order.productIds.toIntArray(),
totalQuantity = order.totalQuantity,
totalPrice = order.totalPrice
)
return data.toByteArray()
}

actual fun saveInvoiceToFile(data: ByteArray, fileName: String) {
val fileManager = NSFileManager.defaultManager
val documentsURL = fileManager.URLsForDirectory(NSDocumentDirectory, NSUserDomainMask).lastObject as NSURL
val fileURL = documentsURL.URLByAppendingPathComponent(fileName)
data.usePinned {
NSData.dataWithBytes(it.addressOf(0), data.size.toULong()).writeToURL(fileURL, true)
}

val documentInteractionController = UIDocumentInteractionController.interactionControllerWithURL(fileURL)
documentInteractionController.delegate = object : NSObject(), UIDocumentInteractionControllerDelegateProtocol {}
documentInteractionController.presentOptionsMenuFromRect(CGRectZero, UIWindow.keyWindow, true)
}

JsMain Module Code:

In the jsMain module, We are going to use the Blob & with the help of the Blob, We will convert the html code into a .html format file. Moreover, You can also use the jsPdf npm module too.

// jsMain Module: composeApp/src/jsMain/kotlin/org/flexi/app/App.js.kt

actual fun generateInvoicePdf(order: Order): ByteArray {
val invoiceHtml = """
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
h1 { text-align: center; }
.invoice-details { margin-bottom: 20px; }
.invoice-details div { margin: 5px 0; }
.invoice-table { width: 100%; border-collapse: collapse; margin-top: 20px; }
.invoice-table th, .invoice-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.invoice-table th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Invoice</h1>
<div class="invoice-details">
<div>Order #: ${order.id}</div>
<div>Customer ID: ${order.userId}</div>
<div>Order Date: ${order.orderDate}</div>
<div>Delivery Date: ${order.deliveryDate}</div>
</div>
<table class="invoice-table">
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Price</th>
</tr>
<tr>
<td>${order.productIds}</td>
<td>${order.totalQuantity}</td>
<td>${order.totalPrice}</td>
</tr>
<tr>
<td colspan="2">Total</td>
<td>${order.totalPrice}</td>
</tr>
</table>
</body>
</html>
""".trimIndent()

val blob = Blob(arrayOf(invoiceHtml), BlobPropertyBag("text/html"))
val url = URL.createObjectURL(blob)

val a = document.createElement("a") as HTMLAnchorElement
a.href = url
a.download = "invoice_${order.id}.html"
document.body?.appendChild(a)
a.click()
document.body?.removeChild(a)

return ByteArray(0)
}

actual fun saveInvoiceToFile(data: ByteArray, fileName: String) {
val blob = Blob(arrayOf(data), BlobPropertyBag("application/pdf"))
val url = URL.createObjectURL(blob)
val a = document.createElement("a") as HTMLAnchorElement
a.href = url
a.download = fileName
document.body?.appendChild(a)
a.click()
document.body?.removeChild(a)
}

jvmMain Module Code:

// jvm Main: composeApp/src/jvmMain/kotlin/org/flexi/app/App.jvm.kt

actual fun generateInvoicePdf(order: Order): ByteArray {
val document = PDDocument()
val page = PDPage()
document.addPage(page)

val contentStream = PDPageContentStream(document, page)
contentStream.beginText()
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12f)
contentStream.newLineAtOffset(100f, 750f)
contentStream.showText("Invoice")
contentStream.newLineAtOffset(0f, -20f)
contentStream.showText("Order #${order.id}")
contentStream.newLineAtOffset(0f, -20f)
contentStream.showText("Customer ID: #${order.userId}")
contentStream.newLineAtOffset(0f, -20f)
contentStream.showText("Order Date: ${order.orderDate}")
contentStream.newLineAtOffset(0f, -20f)
contentStream.showText("Delivery Date: ${order.deliveryDate}")
contentStream.newLineAtOffset(0f, -20f)
contentStream.showText("Product: ${order.productIds}")
contentStream.newLineAtOffset(0f, -20f)
contentStream.showText("Quantity: ${order.totalQuantity}")
contentStream.newLineAtOffset(0f, -20f)
contentStream.showText("Price: ${order.totalPrice}")
contentStream.newLineAtOffset(0f, -20f)
contentStream.showText("Total: ${order.totalPrice}")
contentStream.endText()
contentStream.close()

val outputStream = ByteArrayOutputStream()
document.save(outputStream)
document.close()

return outputStream.toByteArray()
}

actual fun saveInvoiceToFile(data: ByteArray, fileName: String) {
val file = File(System.getProperty("user.home"), fileName)
file.writeBytes(data)
java.awt.Desktop.getDesktop().open(file)
}

Now, We have completed all the steps to create an invoice including to download that file too. Let’s use this in a commonMain Module:

 Button(onClick = {
val invoiceData = generateInvoicePdf(order)
saveInvoiceToFile(invoiceData, "invoice_${order.id}.pdf")
}) {
Text("Download Invoice")
}

This way, You can generate any custom Invoices in your Compose Multiplatform & Kotlin Multiplatform Projects.

Conclusion:

By implementing the expect/actual mechanism, we’ve successfully created a cross-platform invoice generator that functions seamlessly across Android, iOS, JavaScript, and JVM platforms. This demonstrates the power of Kotlin Multiplatform and Compose Multiplatform for building truly universal applications with a single codebase. This solution not only streamlines development but also enhances code reusability and simplifies maintenance, ultimately making your application more efficient and scalable.

Demo:

--

--

Muhammad Khubaib Imtiaz

Software Engineer Android | Kotlin Mutlipaltform | Compose Multiplatform. Community Lead @Kotzilla