Sharing business logic between iOS and Android without sacrificing a native feel is possible — if you draw the line between shared and platform-specific code in the right place.
On Astera and MakersMark, everything that isn't UI — networking, caching, validation, state machines for onboarding and payouts — lives in a shared Kotlin module. The UI itself stays fully native: Jetpack Compose on Android, SwiftUI on iOS. Neither platform ever renders someone else's widgets.
The shared core is where the business rules actually live — how a campaign payout is calculated, how an offline queue reconciles when connectivity returns. Writing and testing that logic once, instead of twice in two languages, is where the real time savings show up, not in the UI layer.
// commonMain — shared by both apps
class PayoutEngine(private val ledger: LedgerRepository) {
suspend fun payout(creatorId: CreatorId): PayoutResult {
val balance = ledger.balanceFor(creatorId)
return ledger.disburse(creatorId, balance)
}
}
// androidApp: PayoutViewModel calls PayoutEngine directly
// iosApp: PayoutViewModel (Swift) calls the same class via the Kotlin/Native frameworkWe don't fight the platform for things users can feel — haptics, animation curves, share sheets, permission prompts. Trying to unify those usually costs more in workarounds than it saves in shared code.
Put anything with no "import android.*" or "import UIKit" in "commonMain" — networking clients, repositories, validators, state machines. Keep "androidMain" and "iosMain" limited to platform entry points and a thin ViewModel/Presenter layer that calls into the shared core.
Multiplatform pays off when you share the logic that's expensive to get wrong twice, and leave everything the user actually touches to the native platform.
Tell us about your system. We respond within one business day with whether we're the right studio for it — and a rough path forward.