12 illustrated topics for a software engineer learning Kotlin and Android/iOS code sharing.
Scope and evidence
Level 2 · fundamentals and best practices for a software engineer. Twelve topic sheets cover language contracts, collections, models, functions, coroutines, flows, Android/iOS compilation, platform seams, UI sharing, Swift interop, memory, and validation. Official rolling documentation checked on 9 October 2026; current Swift export is Alpha. Code is illustrative and reasoned through, not compiled or executed on Android/iOS. This is a focused mobile introduction, not a complete Kotlin course or ecosystem comparison.
Part 1 · Fundamentals
Nullability: make absence explicit
Prerequisites: basic programming
Branch before dereferencing
The branches simplify the semantics of safe access; absence is handled before ordinary string operations. [S01]
Read nullable types and decide what absence means at an API boundary.
Mental model verified
String and String? are different contracts. A safe call returns null if its receiver is null; Elvis chooses a fallback. A stable null check can enable a smart cast. Java platform types weaken the compiler’s knowledge, so normalize external data before trusting it.
A guest label fits a display name; it is a poor fallback for an authentication token. Choose return, explicit failure, or fallback from domain semantics. Overusing !! moves the failure back to runtime. Nullable and invalid are separate conditions.
At a boundary, test null, blank, valid, and malformed values. Keep UI-friendly fallbacks away from security decisions. This is a proposed validation strategy.
Separate reference stability, a read-only API, and immutable data.
Mental model verified
val prevents assigning another value to the variable. A MutableList referenced by val still permits add and remove. List offers a read-only interface; it does not guarantee that another alias cannot change the backing collection.
val buffer = mutableListOf("Kotlin")
val readView: List<String> = buffer
buffer.add("KMP")
// readView now observes both values
val snapshot = buffer.toList()
// independent list structure; element references are still shared
Keep mutation inside the owner and expose snapshots or read-only state. A shallow snapshot is enough for immutable strings, but not for mutable nested objects. Passing a read-only view between concurrent tasks does not establish synchronization.
Test whether a consumer can observe an owner’s later mutation. If snapshots allocate too much, measure first, then consider a persistent immutable representation or a narrower API. These choices trade allocation against ownership clarity.
Model payloads and outcomes without inconsistent boolean combinations.
Mental model verified
A data class generates operations such as equals, hashCode and copy from primary-constructor properties. copy is shallow. A sealed hierarchy lets a Kotlin when expression cover the known variants exhaustively; multiplatform expect/actual sealed hierarchies add restrictions.
data class Quote(val id: String, val amountMinor: Long)
sealed interface LoadState {
data object Loading : LoadState
data class Ready(val quote: Quote) : LoadState
data class Failed(val reason: String) : LoadState
}
fun label(s: LoadState): String = when (s) {
LoadState.Loading -> "Loading"
is LoadState.Ready -> s.quote.id
is LoadState.Failed -> s.reason
}
For this single-request example, variants avoid combinations such as loading=true with error=true. A refreshable screen may legitimately need both existing content and refresh status; model that requirement explicitly. Do not assume a Kotlin data class exports as a Swift struct.
Test each transition and keep payloads immutable by convention. Put properties that define identity in the primary constructor, because body properties are excluded from generated equality.
Use behavior as a value while keeping business intent visible.
Mental model verified
A function type such as (Quote) -> Boolean can be passed as a parameter. map transforms elements; extension functions add callable syntax without changing the original class. Extensions are resolved from the declared receiver type, unlike virtual member dispatch.
Use a lambda for a small local decision; use a named function for a reusable eligibility rule. Long nested lambdas hide control flow. An extension cannot override a real member or gain access to private internals just because it looks like a method.
The illustrated List pipeline creates intermediate collections. For a hot path, compare it with a loop or lazy sequence using representative input sizes. Do not assume laziness improves a small collection. This is a measurement proposal.
Distinguish suspension, thread choice, concurrency, and cancellation.
Mental model verified
suspend allows a function to suspend and resume. It does not automatically start a coroutine or move work off the current thread. Dispatchers determine execution. coroutineScope waits for children; failure in a child cancels its siblings and rethrows to the caller.
import kotlinx.coroutines.*
// Independent requests; APIs are supplied by the caller.
suspend fun load(profile: suspend () -> String,
offers: suspend () -> List<String>) = coroutineScope {
val p = async { profile() }
val o = async { offers() }
p.await() to o.await()
}
Use sibling tasks when either failure invalidates the combined result. Consider supervision when failures are independent and handled. Blocking I/O or CPU work still needs an appropriate dispatcher. A detached application-global task can outlive the screen that requested it.
Preserve CancellationException when catching broadly. Long CPU loops should check cancellation with ensureActive or another cooperative point. Use finally for owned resource cleanup and test cancellation before completion; do not silently turn cancellation into success.
Choose the stream semantics that match a screen’s information needs.
Mental model verified
A flow built with flow { } is cold: its body runs when collected. StateFlow is hot, always has a current value, replays the latest state, and suppresses equal updates. Slow subscribers can miss intermediate values. StateFlow does not complete normally.
import kotlinx.coroutines.flow.*
class Counter {
private val mutable = MutableStateFlow(0)
val state: StateFlow<Int> = mutable.asStateFlow()
fun increment() = mutable.update { it + 1 }
}
Use StateFlow for a renderable snapshot, not a payment audit trail. Choose explicit replay, buffering, and durability for events; a SharedFlow alone does not promise durable exactly-once delivery. Mutating a nested object in place may conceal a state change from equality-based observers.
Tie collection to the consumer’s lifecycle. In the chosen Swift export route, verify the adapter’s cancellation behavior and stop collection on dismissal. Test late subscription and equal updates rather than requiring every intermediate render.
Locate code correctly and trace its Android and iOS compilation paths.
Mental model verified
A target chooses a compilation platform. Source sets group code and dependencies. Android builds compile common and Android Kotlin through Kotlin/JVM; iOS builds compile common and iOS code with Kotlin/Native. A mobile shared module therefore produces different platform artifacts.
shared/src/commonMain/kotlin/Quote.kt
shared/src/androidMain/kotlin/AndroidTokenStore.kt
shared/src/iosMain/kotlin/IosTokenStore.kt
shared/src/commonTest/kotlin/QuoteTest.kt
// Apple Silicon simulator and iOS device are distinct targets:
// iosSimulatorArm64() and iosArm64()
Keep domain rules in commonMain. Android-only APIs belong in Android sources; Foundation APIs belong in Apple-compatible sources. iosMain can share implementations across device and simulator targets. A source set is not itself a deployable application.
Build both platform artifacts early. Use the current compatibility guide for Kotlin, Gradle, AGP, and Xcode; migrate Android KMP library configuration using Google’s plugin guidance instead of copying stale androidTarget snippets. Dependency support must match every intended target.
Share policy while supplying OS-specific behavior through a small seam.
Mental model verified
expect declares a common contract and actual supplies target implementations. Ordinary common interfaces plus injected platform implementations are another option. They are especially useful when you need multiple instances, fakes, or a platform-supplied service.
Use expect/actual for a small fixed platform primitive; prefer injection for a capability whose lifetime or implementation can vary. For token storage, choose OS-backed secure storage according to app requirements. Hiding plaintext preferences behind an interface does not make them secure. This is architectural synthesis, not a storage implementation.
Create a fake TokenStore for common tests and run adapter integration tests on each platform. Verify failure, missing token, lifecycle, and cancellation. Keep Activity, UIViewController, and framework objects out of domain signatures.
Choose between shared logic with platform UI and additional shared Compose UI.
Mental model verified
KMP supplies cross-platform code sharing. Compose Multiplatform adds declarative shared UI and can coexist with existing platform UI. The shared logic / platform UI approach can use SwiftUI or UIKit on iOS and Android UI independently.
For a quote feature, share eligibility, parsing, and repository policy first. Keep an existing SwiftUI quote screen and Android screen. Alternatively, share that screen with Compose while retaining OS-specific camera, permissions, and app-shell integration. This is a proposed scope, not a guaranteed effort reduction.
Platform UI fits independent platform roadmaps and established teams. Shared UI fits substantial design overlap and coordinated releases. Compare accessibility, navigation, text input, and device integration with a realistic feature. Shared Compose UI does not mean every widget is a UIKit widget or that iOS engineering disappears.
Pilot one screen with loading, error, background/foreground, keyboard, and accessibility paths. Measure the duplicate changes avoided against adapter and release costs before extending UI sharing.
Keep this: Share the behavior that benefits the team; validate the experience on each OS.
Check yourself: Does adopting KMP force a UI rewrite?
No. Shared logic can serve existing platform UIs, and shared UI can be introduced incrementally.
Part 10 · Best practices
Swift interop: exported APIs need design
Prerequisites: 05-coroutines, 06-flows, 08-ports
Two export mechanisms
As verified on 2026-10-09. Callback/async and flow behavior are export-route dependent. Test cancellation propagation rather than inferring it. [S18][S19]
Choose an Apple export route and test its error, async, and cancellation behavior.
Mental model verified
Objective-C framework export exposes Kotlin APIs through Objective-C-compatible declarations. Its suspend APIs appear as completion handlers and can be callable as Swift async with documented limitations. Current Swift export instead generates Swift modules, supports suspend as async and Flow as AsyncSequence, but is Alpha and presently requires direct Xcode integration.
Export a small quote facade: loadQuote(id), a renderable state, and explicit close/cancel behavior if it owns work. Inspect the generated Swift surface. Test success, domain failure, early cancellation, and repeated subscribe/unsubscribe using the actual chosen route. The Kotlin declaration alone is not the contract test.
For Objective-C export, @Throws controls which expected exceptions become Swift errors; unexpected exceptions crossing the boundary can terminate the app. Keep common business failures explicit. Do not assume a Swift task’s cancellation cancels Kotlin work just because a call uses await. Verify the export route or wrapper.
Current Swift export uses Dispatchers.Default by default for exported suspending work. UI delivery still needs the appropriate UI context. Hide internals, inspect generic/type mappings, and keep a Swift consumer regression test before compiler upgrades. Alpha features can change.
Recognize ownership leaks and avoid treating interoperability as zero-cost.
Mental model verified
Kotlin/Native uses a shared heap and tracing GC. Swift/Objective-C objects use ARC. Mixed strong-reference cycles can prevent reclamation; interop object release can wait for GC. Thread-accessible objects are not automatically safe for unsynchronized mutation.
A Kotlin repository retains a Swift callback; the callback strongly captures a Swift screen owner that retains the repository. On dismissal, the feature stops its subscription and releases the callback. A weak Swift capture may also break the cycle. The right break point follows the ownership contract.
Use explicit close/detach semantics for owned listeners and resources. GC and ARC timing do not provide deterministic business cleanup. Avoid per-element cross-language calls in a hot path. Objective-C export can add string and collection conversion overhead; batching can help when it reduces conversions. Measure its memory and latency costs.
Repeat open/close flows while profiling retained objects, allocation, and UI stalls on release builds. Inspect native GC diagnostics and Xcode Instruments. Do not tune collector options or apply old freezing recipes before identifying a real bottleneck. No benchmark result is claimed here.
Build a small adoption loop with portable tests, boundary tests, and release metrics.
Mental model verified
commonTest uses portable assertions, but tests execute through target-specific runners. Run shared tests on JVM and an iOS simulator. Platform tests cover adapters and OS behavior. runTest supports virtual-time coroutine tests; its usual single-thread scheduler does not prove safety under true parallel execution.
import kotlin.test.*
class DisplayNameTest {
@Test fun blankMeansGuest() {
assertEquals("Guest", displayName(" "))
}
}
// Put this in commonTest and run it on each intended target.
// displayName is defined on sheet 01.
Adopt one self-contained rule or repository before expanding shared UI. Add a Swift consumer test for the exported framework. A successful JVM unit run cannot prove iOS linking, framework mappings, device permissions, or cancellation through Swift. Apple builds and signing need the Apple toolchain.
Baseline clean and incremental build time, release artifact size, startup, screen latency, and crash rates. Pin a compatible toolchain and review upgrades together. Objective-C-export frameworks can be packaged as an XCFramework for SwiftPM distribution; do not assume Alpha Swift export supports that same route. Compare benefits against both platforms’ maintenance costs. These are proposed measures, not measured improvements.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Nullable types, smart casts, safe calls and Elvis operator
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Read-only versus mutable interfaces; val and mutable collections
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Generated value operations and shallow copying
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Closed hierarchies and exhaustive when expressions
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Function types, lambdas and passing behavior
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Extension resolution and absence of real class modification
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Transforming collection values
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Suspension, dispatchers, scopes and task hierarchy
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Scoped child tasks, waiting and failure propagation
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Cancellation is a normal coroutine control signal
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Cooperative checks in non-suspending work
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Cold flow execution and collection
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Hot state, latest value, equality conflation and atomic update
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Common and platform sources compile together for a target
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Intermediate iOS sources and target hierarchy
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Platform implementations and interface-based alternatives
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Optional shared UI and gradual adoption
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Framework export, exception mapping and conversion overhead
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Shared heap, tracing garbage collection and profiling
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Mixed cycles, reclamation timing and completion thread caveats
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Common tests across targets and platform-specific validation
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Toolchain compatibility and Android plugin migration
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: XCFramework binary distribution through SwiftPM
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Native compilation and runtime overview
Study the mechanism described above, then verify it against the versions used in your project.
JetBrains / Kotlin project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Platform types and nullability at a Java boundary
Study the mechanism described above, then verify it against the versions used in your project.
Google / Android Developers · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: Java bytecode to DEX compilation in Android builds
Study the mechanism described above, then verify it against the versions used in your project.
Android Open Source Project · official documentation · accessed 2026-10-09 · Rolling documentation as accessed; use project-specific toolchain compatibility
Supports: ART executes DEX code on Android
Study the mechanism described above, then verify it against the versions used in your project.