What code-sharing strategies exist across iOS and Android beyond 'pick one cross-platform framework'?
React Native and Flutter aren't the only way to avoid writing everything twice — teams share a Kotlin, Rust, or C++ core underneath fully native UIs all the time.
LEARN BY DOINGPut your intuition to work3 steps
What code-sharing strategies exist across iOS and Android beyond 'pick one cross-platform framework'?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Reframing the question
A junior answer to "how do you share code across iOS and Android" is "React Native or Flutter." A senior answer recognizes that those frameworks solve a specific problem — sharing UI and business logic together — and that plenty of teams deliberately keep the UI fully native while sharing everything underneath it. This distinction matters commercially: a fintech app with heavy compliance requirements or a camera app with custom rendering pipelines often cannot accept any abstraction over platform UI, but still wants to avoid maintaining two parallel implementations of pricing logic or an image-processing algorithm.
#Strategy 1: Kotlin Multiplatform (KMP) for a shared logic layer
Kotlin Multiplatform compiles the same Kotlin source to JVM bytecode for Android and to a native binary via Kotlin/Native for iOS (packaged as a .xcframework consumable from Swift). The UI stays 100% native — SwiftUI or UIKit on iOS, Jetpack Compose or Views on Android — while networking, caching, business rules, and data models live in one shared module.
The idiomatic pattern for anything that must differ per platform is expect/actual: you declare the shape once in shared code and provide a platform-specific body on each side.
// commonMain/kotlin/data/WeatherRepository.ktclass WeatherRepository(private val httpClient: HttpClient, private val logger: PlatformLogger) {suspend fun fetchForecast(city: String): Forecast {logger.log("WeatherRepository", "Fetching forecast for $city")return httpClient.get("https://api.example.com/forecast") {parameter("city", city)}.body()}}// commonMain/kotlin/platform/PlatformLogger.ktexpect class PlatformLogger() {fun log(tag: String, message: String)}
// androidMain/kotlin/platform/PlatformLogger.ktactual class PlatformLogger actual constructor() {actual fun log(tag: String, message: String) {android.util.Log.d(tag, message)}}
// iosMain/kotlin/platform/PlatformLogger.ktactual class PlatformLogger actual constructor() {actual fun log(tag: String, message: String) {platform.Foundation.NSLog("$tag: $message")}}
On the iOS side, Swift consumes the compiled framework as if it were any other Swift dependency:
let repository = WeatherRepository(httpClient: KtorClientFactory().create(), logger: PlatformLogger())Task {do {let forecast = try await repository.fetchForecast(city: "Bogotá")await MainActor.run { self.viewModel.update(forecast) }} catch {// handle error}}
#Strategy 2: a shared C++ or Rust core
Performance-critical or algorithm-heavy domains (codecs, cryptography, physics, camera pipelines, crypto wallets) often share a C or C++ core wrapped with JNI on Android and an Objective-C++ bridging layer (or a Swift/C interop header) on iOS. Rust has become popular for the same purpose because of memory safety without a garbage collector, plus tooling like UniFFI, which generates Kotlin and Swift bindings directly from a Rust interface definition, removing most of the hand-written glue code that made C++ sharing painful.
#Strategy 3: shared API contracts and codegen
Even without sharing a compiled runtime, teams reduce duplication by generating typed clients from a single source of truth — an OpenAPI spec or a GraphQL schema — into both Swift and Kotlin. Neither platform shares executable code, but the request/response models, validation rules, and endpoint signatures stay in lockstep automatically instead of being hand-copied into two languages and drifting apart.
#Strategy 4: server-driven UI
For catalog-style or promotional screens that change frequently (a merchandising carousel, a checkout flow with A/B-tested layouts), some teams push layout description — not code — from the backend: a JSON schema describing which components to render and in what order, interpreted by a thin native renderer on each platform. This isn't full code sharing, but it centralizes the part of the UI that changes most often, and lets a backend team ship a layout change without an app store release.
#Strategy 5: brownfield, incremental adoption
Rather than an all-or-nothing framework bet, some teams embed a React Native or Flutter engine inside specific screens of an existing fully native app — a technique commonly called "brownfield" integration. This lets a team pilot cross-platform code sharing on a low-risk surface (say, a settings screen or a support chat module) while the navigation shell, core flows, and anything with heavy platform-specific requirements stay native. It's a hedge: if the pilot module underperforms, the blast radius is one screen, not the whole app.