
What Is Really Worth Sharing
🎅 Introduction
End of year always has a very specific feeling. Projects slow down a bit, pull requests get smaller, and many of us start reviewing what we built over the past months. It is also the time where we look at our Kotlin Multiplatform modules and ask ourselves a very honest question, did we actually share the right things, or did we just share more things.
Kotlin Multiplatform is about sharing, but sharing blindly is like buying random Christmas gifts at the last minute. Some will be useful, some will not, and a few will quietly become technical debt before the new year even starts.
This article is a practical checklist. No theory, no evangelism. Just a clear guide to what usually belongs in the shared module, what might belong there, and what almost never should.
🎁 1. The Gift List, What Should Live in the Shared Module
These are the gifts that almost always fit. They are platform agnostic by nature and benefit the most from being shared.
Business logic and use cases
Anything that represents a decision, a rule, or a flow of data belongs here. Use cases are usually the first thing I move to the shared module.
class LoadUserProfileUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(userId: String): UserProfile {
return repository.loadProfile(userId)
}
}
This code does not care if it runs on Android or iOS. That is exactly why it is a perfect gift.
Domain models and validation rules
Data models, sealed states, and validation logic are another strong candidate. They define what your app is, not how it renders.
data class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email" }
}
}
Networking and repositories
Shared networking logic keeps behavior consistent and avoids subtle differences between platforms.
This is where libraries like Ktor or your own abstractions shine, especially when combined with Flows or suspend functions.
Shared state holders
State machines, reducers, or shared ViewModels that expose immutable state are excellent candidates, as long as they do not depend on platform lifecycles.
🤔 2. The “Maybe” Gifts, Think Twice Before Sharing
These are the gifts that look great in the store but require a bit more thought.
Date and time handling
Date handling usually fails because platforms disagree on locales or time zone databases. To fix this, you have to be disciplined about where the “math” happens versus where the “formatting” happens.
- The Refactor: Use kotlinx-datetime. Stop passing platform-specific date objects through your shared logic.
- The Rule: Your shared module should only speak in Instants (UTC) and Periods. Let the Android and iOS apps handle the final string formatting (like “Dec 25, 2024”) based on the user’s local phone settings.
Serialization details
Serialization is a “maybe” because a small change in a JSON key can crash one platform while the other handles it fine.
- The Refactor: Centralize your
Jsonconfiguration. Don’t let your Repositories use a defaultJson {}instance. Create a single, sharedNetworkJsonobject withignoreUnknownKeys = trueand explicit naming strategies. - The Rule: If you can’t control the API, use a Data Transfer Object (DTO) in shared code and map it to a clean Domain Model before it reaches the UI.
Concurrency assumptions
Kotlin Coroutines are great, but the way Swift consumes them can be messy. It’s a “maybe” because of the friction it creates for iOS developers.
- The Refactor: Use a tool like SKIE or KMP-NativeCoroutines. These tools act like a translator, turning Kotlin
Flowsinto something that looks and feels like a native SwiftAsyncSequence. - The Rule: Shared code should always define its own
CoroutineScopeandDispatcher. Never leave it up to the platform to decide which background thread a shared task should run on.
Platform APIs behind interfaces
Over-abstracting a platform API (like Bluetooth or Camera) is a recipe for a maintenance nightmare.
- The Refactor: Instead of trying to mirror the entire platform API in an expect/actual interface, only abstract the data you need.
- The Rule: If your interface has more than three methods, you aren’t “sharing logic”, you are just building a second-rate version of a native library. Keep the interface “thin” and the implementation “dumb.”
🚫 3. The Naughty List, What You Should Not Share
This is where many KMP projects go wrong.
UI code
Compose, SwiftUI, UIKit, XML layouts. None of this belongs in shared code when you are using Kotlin Multiplatform with native UIs.
If you are using Compose Multiplatform to share UI across platforms, this rule obviously changes. In that case, UI is part of the shared strategy by design.
The key distinction is intent. When platforms are expected to express themselves independently, UI should remain platform specific. The UI is where platforms should be free to express themselves.
Navigation logic
Navigation is deeply tied to platform conventions. Trying to unify it usually creates more friction than value.
Lifecycle handling
Android lifecycles and iOS lifecycles are fundamentally different. Abstracting them rarely pays off.
Over abstracted platform wrappers
If you need five interfaces and three layers just to call a system API, it is probably not worth sharing.
🎀 4. Wrapping Presents Properly, Using expect and actual
expect and actual are powerful, but they should be used with restraint.
A good use case is when the shared code needs a capability, not an implementation.
expect class PlatformLogger {
fun log(message: String)
}
actual class PlatformLogger {
actual fun log(message: String) {
println(message)
}
}
The key rule is simple. Keep the expected surface small and focused. If the abstraction starts leaking platform details, it is time to reconsider.
🧹 5. The End of Year Cleanup, Reviewing Your Shared Code
End of year is the perfect moment to audit your shared module.
Ask yourself:
- Does this code still represent a shared rule or behavior?
- Is this abstraction pulling its weight?
- Could this be simpler if it lived on the platform side?
Removing code from shared can be just as valuable as adding new shared code.
⭐ 6. A Simple Rule of Thumb for the Next Year
If a piece of code represents a business rule, a decision, or a state, it probably belongs in shared.
If it represents a user interaction, a visual concern, or a platform behavior, it probably does not.
This rule is not perfect, but it will save you from many painful refactors.
🎄 Conclusion
Kotlin Multiplatform is not about sharing everything. It is about sharing the right things.
A small, boring, predictable shared module is usually a sign of a healthy KMP project. As the year ends, take the time to clean it up, remove what no longer belongs there, and prepare it for the next cycle.
Do that, and your future self will probably thank you, sometime after the holidays, when things start moving fast again.
📱 New: The KMP Bits App
If you want an easier way to read articles like this, I just released the KMP Bits mobile app, built entirely with Kotlin Multiplatform.
This kind of more personal, experience-based content is exactly what I like to share first on KMP Bits.
The app makes reading and navigating content much faster, and it is the best way to stay up to date with new posts.
You can follow along directly in the app:
➡️ App Store
➡️ Google Play
If you enjoyed this article, thank you for reading. I hope this checklist helps you make better KMP decisions in your own projects.
If you want to keep following along:
- Follow KMP Bits for more Kotlin and Kotlin Multiplatform content.
- Check out the app for a better reading experience.
- Share this article if you think others might benefit from it.