Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
81: Sharing Business Logic Across Platforms
I was chatting with the iOS lead on my last project, and they were complaining—loudly—that their subscription pricing logic was slightly off compared to the Android app. Specifically, the way we handled the "Annual Discount" calculation was diverging. I looked at the Android code, then the Swift code, and it was a mess. Two different implementations of the same business rule. That's a recipe for customer support nightmares.
Moving the Logic into the Common Bucket
My first instinct was to just rip the logic out of the Android app and shove it into a Kotlin Multiplatform (KMP) shared module. I created a simple SubscriptionCalculator class. I wanted something that takes a base price, a tier, and returns the final cost.
// In commonMain
class SubscriptionCalculator {
fun calculateYearlyPrice(monthlyPrice: Double, discountPercent: Double): Double {
val annualBase = monthlyPrice * 12
return annualBase * (1.0 - (discountPercent / 100.0))
}
}
I compiled it, hooked it up to the Android app, and it worked perfectly. Then I pointed the iOS framework at it. To my surprise, it also worked. For pure mathematical logic—stuff that doesn't touch a screen, a database, or a file system—KMP is almost invisible. I just write Kotlin, and it works everywhere. But then I tried to make the output user-friendly.
The Moment the Compiler Screamed
I figured I'd add a helper method to format the price as a currency string. I'm used to JVM development, so I instinctively reached for java.text.NumberFormat. I added this to my shared class:
// Still in commonMain
fun formatPrice(price: Double): String {
return java.text.NumberFormat.getCurrencyInstance().format(price)
}
The IDE immediately turned red. The compiler started throwing a fit, telling me that java.text isn't available in the common source set. It makes sense—iOS doesn't know what a JVM is. I can't use Java libraries in commonMain because that code is intended to be compiled to both JVM bytecode and native LLVM binaries.
I could have just moved the formatting back to the platform-specific UI layers, but that feels like a cop-out. I wanted the logic of how we represent currency to be shared, even if the mechanism for doing it is different on each OS.
Bridging the Gap with Expect and Actual
This is where I had to pivot. I can't use java.text, but I can tell Kotlin: "I expect there to be a way to format currency, and I'll provide the specific implementation for each platform."
First, I defined the "expectation" in commonMain. I didn't write a body for the function; I just defined the signature:
// In commonMain
expect fun formatCurrency(amount: Double): String
Now the shared SubscriptionCalculator can actually use that function without knowing how it works under the hood. But the project won't compile until I fulfill that promise for every target platform. I hopped over to the Android (jvmMain) source set and wrote the "actual" implementation:
// In androidMain
actual fun formatCurrency(amount: Double): String {
return java.text.NumberFormat.getCurrencyInstance().format(amount)
}
Then, I switched to the iOS (iosMain) source set. Since I'm in Kotlin, I can actually call Apple's NSNumberFormatter directly through the interop layer:
// In iosMain
import platform.Foundation.NSNumberFormatter
import platform.Foundation.NSNumberFormatterCurrencyStyle
actual fun formatCurrency(amount: Double): String {
val formatter = NSNumberFormatter()
formatter.numberStyle = NSNumberFormatterCurrencyStyle
return formatter.stringFromNumber(amount.toNSNumber()) ?: "$$amount"
}
Now, when I call calculator.formatPrice(price) in the shared module, Kotlin handles the dispatch. On Android, it runs the Java code; on iOS, it runs the Foundation framework code. We've shared the intent and the orchestration of the business logic, while respecting the reality that platforms have different tools for the same job.
📋 Practical Task
Implement a Cross-Platform Validation Engine
You are building a shared validation library for a user registration flow. You need to ensure that a username is valid across both Android and iOS, but the "strictness" of the validation (specifically, checking for forbidden characters) needs to use a platform-specific Regular Expression engine for performance reasons.
- In
commonMain:- Create a class
UserValidator. - Define an
expect fun isUsernameAllowed(username: String): Boolean. - Create a shared method
validateRegistration(username: String)that returns aString: "Invalid Length" if the username is under 3 characters, and "Forbidden Characters" ifisUsernameAllowedreturns false. Otherwise, return "Valid".
- Create a class
- In
androidMain:- Implement the
actual fun isUsernameAllowedusing a JavaRegexthat returnstrueonly if the string contains alphanumeric characters.
- Implement the
- In
iosMain:- Implement the
actual fun isUsernameAllowedusing a simple Kotlinall { it.isLetterOrDigit() }check (simulating the native implementation).
- Implement the
There are no comments for now.