
Same Concepts, Native Experience
Series
Mobile SDK Design
7 of 7 in the series
A series about designing mobile SDKs that are easy to adopt, clear to integrate, and safe under production constraints.
Article 1
Designing a Mobile SDK That Developers Actually Want to Use
Article 2
The Public API Surface Is the Product
Article 3
Initialization, State, and Thread Safety in Mobile SDKs
Article 4
Developers Do Not Mind Errors. They Mind Unclear Errors.
Article 5
Shipping the SDK Is Where Trust Starts
Article 6
Secure Defaults for Mobile SDKs
Article 7
Same Concepts, Native Experience
Android and iOS SDKs should share concepts, not identical syntax. Consistency belongs in the mental model, not in the method signatures.
Cross-Platform Drift is the failure mode where an Android SDK and an iOS SDK expose different names, different error models, or different initialization sequences for the same product concept.
It happens when Android and iOS SDKs are built by independent teams without shared API ownership. Each team makes locally reasonable decisions. The cumulative effect is two separate mental models for one product. A developer who integrates both platforms has to learn the SDK twice. A backend team building their first mobile SDK discovers that "auth failed" is named four different things across the Android and iOS implementations.
This article covers the structural questions: what should be shared, what should stay native, and where the boundary belongs.
Shared Concepts vs Native Idioms
The distinction that resolves most cross-platform design decisions:
Share the mental model. Names for product concepts, initialization sequences, error hierarchies, event names, configuration keys, state machine states, and documentation structure should be consistent across platforms. These are the things a developer carries in their head.
Keep the idioms native. Syntax, concurrency model, type system details, platform lifecycle integration, and naming case conventions should follow each platform's established patterns. These are the things that make the SDK feel like it belongs in the host app's codebase.
A developer who has integrated the Android SDK should be able to open the iOS documentation and recognize the same initialization flow, the same error types by name, and the same event vocabulary. The syntax will look different. The mental model should not require translation.
Aligning the Error Hierarchy
Error types and error codes are the clearest signal of whether the Android and iOS SDKs were designed as a coherent product or as independent implementations.
Android (Kotlin) iOS (Swift)
----------------------- -----------------------
SDKException SDKError (protocol)
AuthorizationFailed AuthorizationFailed
ConfigurationMissing ConfigurationMissing
NetworkUnavailable NetworkUnavailable
RateLimited RateLimited
SDKErrorCode (enum) SDKErrorCode (enum)
AUTHORIZATION_FAILED .authorizationFailed
NOT_CONFIGURED .notConfigured
NETWORK_UNAVAILABLE .networkUnavailable
RATE_LIMITED .rateLimitedThe names match. Case conventions differ because the platforms have different conventions: SCREAMING_SNAKE_CASE for Kotlin enum entries, camelCase for Swift enum cases. But AuthorizationFailed is AuthorizationFailed on both platforms, not AuthError on one and InvalidCredentials on the other.
When error names differ between platforms, support tickets from cross-platform teams will include the Android error code when the issue is on iOS, or mention a state name that does not exist on the platform being debugged. The SDK team has to translate. That translation cost is paid on every support interaction.
Aligning Names for Product Concepts
Platform idioms differ in syntax, but the names for SDK operations should be agreed on before implementation starts.
| Concept | Android (Kotlin) | iOS (Swift) |
|---|---|---|
| Initialize | SDK.bootstrap() | SDK.bootstrap() |
| Configure | SDK.configure {} | SDK.configure {} |
| Reconfigure | SDK.configure {} | SDK.configure {} |
| Session ID | sessionId | sessionId |
| User ID | userId | userId |
| Configuration complete | onConfigurationComplete | configurationComplete |
| Payment result type | PaymentResult | PaymentResult |
| Auth failure | AuthorizationFailed | AuthorizationFailed |
Method names follow platform conventions (Kotlin uses a trailing {} lambda DSL; Swift uses a different closure syntax) but name the same concept with the same word. The developer who knows bootstrap on Android does not have to discover setup, start, or initialize on iOS.
What drifts without explicit agreement before implementation: method name phrasing, parameter names, event names in listener interfaces, and the names of configuration fields. These accumulate into a noticeably inconsistent API.
Concurrency: Kotlin Coroutines and Swift Async/Await
Both Kotlin coroutines and Swift's async/await model structured concurrency. Both suspend rather than block. Both propagate failure through the language's standard error mechanisms. The behavioral contract maps well across platforms even though the syntax differs.
Design SDK async APIs so that the same contract is available in each platform's native model.
// Android: standard Kotlin suspend function
// Result is PaymentResult or throws SDKException
suspend fun requestPayment(
customerId: String,
method: PaymentMethod
): PaymentResult// iOS: standard Swift async function
// Returns PaymentResult or throws SDKError
func requestPayment(
for customerId: String,
method: PaymentMethod
) async throws -> PaymentResultBoth:
- Suspend the caller without blocking the thread
- Return a typed result on success
- Propagate typed errors through the language's standard error mechanism
- Are cancellable through their respective scope (Kotlin
CoroutineScope, Swift structured task tree)
A developer familiar with either platform reads the other platform's API and understands the pattern. The behavioral contract transfers. The syntax is different.
Avoid designing one platform's API to match the other platform's concurrency model. Exposing a Kotlin-style callback interface on iOS because the Android SDK uses callbacks, or using a completion handler on Android because the iOS SDK does, produces SDK code that feels foreign in the host app.
Kotlin Multiplatform as an Architectural Choice
KMP lets teams write shared Kotlin code that compiles to JVM bytecode for Android and native code for iOS. It is stable and officially supported by Google for sharing business logic between Android and iOS.
The right position for KMP in SDK design is conditional. Share implementation when it reduces divergence without affecting iOS developer experience. Do not share when the shared implementation surfaces in the public API.
Where KMP adds value in SDKs:
- Serialization and protocol models: wire DTOs, request and response structures, and serialization logic are implementation details that do not need to differ between platforms. Do not expose Kotlin data classes as the public iOS model layer. Public Swift-facing models should still feel native, even when backed by shared Kotlin types internally.
- Protocol logic: auth flows, retry strategies, token refresh, and state machine transitions encode business rules that should be identical on both platforms. A single implementation eliminates the possibility of behavioral drift.
- Validation: input validation that should produce the same result on both platforms benefits from a shared implementation.
Where KMP creates problems:
- When the iOS public API becomes a thin wrapper over Kotlin classes. Kotlin sealed classes, companion objects, and data classes with
copy()expose a Kotlin-shaped API to iOS developers who expect Swift-shaped types. - When the Kotlin/Native runtime adds meaningful binary size to the iOS framework.
- When the shared code uses Kotlin-specific patterns (coroutines,
Flow,Sequence) that do not map cleanly to Swift idioms and require non-trivial wrapping. - When iOS developers need to debug through the shared layer and encounter Kotlin stack frames and generated interop layers that are harder to debug from a Swift-first workflow.
The rule: share the implementation, design the API natively. The KMP layer is an implementation detail. An iOS developer should not need to know that the auth token logic is written in Kotlin to use the iOS SDK.
Shared Kotlin layer (KMP) Native iOS API layer
-------------------------- --------------------
TokenManager.kt TokenManagerBridge.swift
refreshToken() ----> func refreshToken() async throws -> Token
isTokenValid() ----> func isTokenValid(_ token: Token) -> Bool
tokenStore ----> (internal, not in public surface)The iOS developer interacts only with TokenManagerBridge. They never see the Kotlin types.
Documentation Structure Alignment
Documentation drift is as damaging as API drift. If the Android README uses different section names, describes concepts in a different order, or uses different terminology from the iOS README, a developer reading both documents cannot build a unified mental model.
Both platform READMEs should follow the same order:
- Installation (platform-specific syntax, concept is the same)
- Bootstrap (same concept name, platform-specific code)
- Configure (same concept name, platform-specific code)
- Making your first API call (same structure, platform-specific code)
- Error handling (same error type names, platform-specific syntax)
- Changelog
The section names and the order are shared. The code examples are platform-specific. A developer reading both READMEs recognizes the same product.
This requires that someone owns the documentation structure as a cross-platform concern and reviews both READMEs before a release that changes either one.
Shared Test Contracts
Both platforms should validate the same behavioral invariants. This does not require shared test code. It requires a shared behavioral specification.
A behavioral contract for SDK initialization states:
Invariant Android iOS
--------- ------- ---
bootstrap() is idempotent Test A1 Test I1
configure() from Bootstrapped transitions to Configured Test A2 Test I2
configure() failure from Bootstrapped returns to Bootstrapped Test A3 Test I3
configure() from Configured transitions to Reconfiguring Test A4 Test I4
reconfigure() failure keeps prior Configured state Test A5 Test I5
bootstrap() failure is terminal Test A6 Test I6
Core APIs available in Bootstrapped Test A7 Test I7
Context-dependent APIs fail gracefully in Bootstrapped Test A8 Test I8Each row is an invariant that both platform implementations must satisfy. The Android tests are written in Kotlin. The iOS tests are written in Swift. They validate the same contract independently.
A simple way to maintain this: store the behavioral specification as a table in the repository. Each row has an Android test reference and an iOS test reference. A CI gate that requires both tests to be present and passing ensures the contract holds on both platforms, and makes any behavioral regression on one platform immediately visible as a missing entry.
A Cross-Platform Alignment Checklist
Before a release that changes either platform's API or documentation, validate against these questions:
- Do the Android and iOS SDKs use the same names for initialization operations?
- Do the error names map one-to-one across platforms, while preserving native casing and type conventions?
- Do the error code or enum case names match across platforms?
- Do the event names in the listener or delegate interface match across platforms?
- Do the configuration parameter names match across platforms?
- Is the initialization flow conceptually identical, even if the syntax differs?
- Do both platform READMEs use the same section structure and concept names?
- Are the behavioral invariants documented in a shared specification with both Android and iOS test references?
- If KMP is used, is the Kotlin/Native boundary wrapped so that iOS developers work with Swift-idiomatic APIs?
- Can a developer who has integrated one platform open the other platform's documentation and recognize the same product?