Skip to Content
Course content

191: Building a Kotlin Multiplatform Calculator

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably already set up your Kotlin Multiplatform (KMP) project and decided to build a calculator. It's a classic project for a reason—it lets you nail down the boundary between shared business logic and platform-specific UI. But there is a trap that almost every developer falls into when they start moving logic into commonMain. I fell into it myself on my first KMP project.

Take a look at this snippet from a shared CalculatorEngine class. The goal here is simple: perform a calculation and return a nicely formatted string for the UI to display.

// In commonMain/src/commonMain/kotlin/CalculatorEngine.kt
class CalculatorEngine {
    fun calculate(a: Double, b: Double, operator: String): String {
        val result = when (operator) {
            "+" -> a + b
            "-" -> a - b
            "*" -> a * b
            "/" -> if (b != 0.0) a / b else Double.NaN
            else -> 0.0
        }
        
        // This looks perfectly fine in Android Studio...
        return String.format("%.2f", result) 
    }
}

The "JVM-Only" Compilation Error

If you are running this on an Android emulator, it works perfectly. You won't see a single red line. But the moment you try to build the iOS target or run the ./gradlew assemble task for all targets, the compiler will hit you with a wall of errors. It'll tell you that String.format is not available in the common source set.

Here is why: String.format isn't actually a part of the Kotlin language; it's a wrapper around java.lang.String.format. Since your commonMain code is intended to be compiled for both the JVM (Android) and Native (iOS), you can't use any classes that live inside the java.* package. iOS has no idea what a JVM is, let alone java.lang.String.

Decoupling Formatting via Expect/Actual

You might be tempted to just return the Double and handle the formatting in the UI layer. In a massive production app, that's actually the right move. But for a calculator, you often want the "formatting rule" (like two decimal places) to be a shared business requirement. To do this without breaking the iOS build, we use the expect and actual keywords.

First, we define the expectation in commonMain. We aren't providing the logic yet; we're just telling the compiler, "I promise that every platform will provide its own way to do this."

// In commonMain
expect fun formatDecimal(value: Double): String

class CalculatorEngine {
    fun calculate(a: Double, b: Double, operator: String): String {
        val result = when (operator) {
            "+" -> a + b
            "-" -> a - b
            "*" -> a * b
            "/" -> if (b != 0.0) a / b else Double.NaN
            else -> 0.0
        }
        return formatDecimal(result)
    }
}

Now, the project won't compile until you satisfy those promises. You need to go into your platform-specific modules and provide the actual implementation.

For Android (androidMain), we can keep using the JVM's power:

// In androidMain
actual fun formatDecimal(value: Double): String {
    return String.format("%.2f", value)
}

For iOS (iosMain), we use the Foundation framework's formatting capabilities via Kotlin/Native's interop:

// In iosMain
import platform.Foundation.NSNumber
import platform.Foundation.NSNumberFormatter
import platform.Foundation.NSNumberFormatterDecimalStyle

actual fun formatDecimal(value: Double): String {
    val formatter = NSNumberFormatter().apply {
        minimumFractionDigits = 2u
        maximumFractionDigits = 2u
        numberStyle = NSNumberFormatterDecimalStyle
    }
    return formatter.stringFromNumber(NSNumber(value)) ?: value.toString()
}

By shifting the formatting logic to the platform modules, we keep our business logic in commonMain while respecting the technical constraints of the underlying OS. The CalculatorEngine still controls when the formatting happens, but the platform controls how it happens.




📋 Practical Task

Implementing a Shared Percentage Calculation Feature

Your calculator needs a "Percentage" feature (e.g., calculating 15% of a value). However, for financial accuracy, you want to ensure that the percentage operation is handled with a specific rounding mode that differs slightly between Android and iOS to match platform-specific accounting standards.

Your Task:

  • Create an expect function in commonMain called calculatePercentage(total: Double, percent: Double): Double.
  • In androidMain, implement the actual function using java.math.BigDecimal to ensure high precision rounding (HALF_UP).
  • In iosMain, implement the actual function using standard Kotlin Double math, but wrap it in a logic block that rounds the result to 4 decimal places using kotlin.math.round.
  • Update your CalculatorEngine in commonMain to include a "%" operator case in the calculate function that calls your new calculatePercentage function.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.