Writing iOS code in Kotlin Multiplatform. Part 1 — expect/actual

Andrii Seredenko
4 min readNov 8, 2024

--

In previous articles (onetwo), I covered the basics of starting with Kotlin Multiplatform. Now, I’d like to share insights on writing iOS-specific code within the framework.

expect/actual

Imagine you have a Payment button intended to trigger Google Pay on Android and Apple Pay on iOS. While the general usage is similar, the button’s UI differs on each platform, and clicking it initiates a distinct flow depending on the operating system.

The final result of the Payment button for Android and iOS

First, we need to declare an expect function and provide an actual implementation for each platform target (Android and iOS).

expect defines a contract between the common and native layers. With experience, you'll get a sense of how best to design this contract, as actual implementations often differ significantly across platforms.

actual provides the platform-specific implementation of this contract, enabling access to features unique to each platform (such as Java packages on Android and Core Foundation on iOS).

@Composable
expect fun PaymentButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
)

Both Android and iOS will have their own actual declarations

@Composable
actual fun PaymentButton(
onClick: () -> Unit,
modifier: Modifier
) {
// TODO implement the specific platform logic
}
expect/actual files placement

PaymentButton usage just like any other Compose function

@Composable
fun PaymentScreen() {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.secondaryContainer)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize().statusBarsPadding()
) {
Text(
text = "Payment Data",
style = MaterialTheme.typography.headlineLarge
)
Text(
"Total: $34.99",
style = MaterialTheme.typography.titleSmall
)
}
// Use PaymentButton as a ordinar Compose function
PaymentButton(
onClick = {
// TODO implement
},
modifier = Modifier
.navigationBarsPadding()
.widthIn(720.dp)
.padding(32.dp)
.align(Alignment.BottomCenter)
)
}
}

Android layer

Let’s start by creating an Android button. For demonstration purposes, we’ll simplify the implementation by focusing on the button itself, ignoring some required rules.

Add the necessary dependencies:

// libs.versions.toml
compose-pay-button = { module = "com.google.pay.button:compose-pay-button", version = "0.1.3" }

// user-interface/build.gradle
kotlin{
sourceSets {
androidMain.dependencies {
implementation(libs.compose.pay.button)
}
// ...
}
}

Modify PaymentButton.android.kt to implement the button using Android-specific dependencies.

@Composable
actual fun PaymentButton(
onClick: () -> Unit,
modifier: Modifier
) {
PayButton(
type = ButtonType.Plain,
modifier = modifier.fillMaxWidth(),
theme = if (isSystemInDarkTheme()) ButtonTheme.Light else ButtonTheme.Dark,
onClick = {
// TODO implement click action
},
allowedPaymentMethods = ""
)
}

iOS layer

Next, implement the iOS button in PaymentButton.ios.kt using UIKitView, which allows us to render iOS components inside a composable, similar to how AndroidView works on Android with legacy components.

import platform.PassKit.PKPaymentButtonStyleBlack
import platform.PassKit.PKPaymentButtonStyleWhite
import platform.PassKit.PKPaymentButtonTypePlain
import platform.UIKit.UIControlEventTouchUpInside

@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
@Composable
actual fun PaymentButton(
onClick: () -> Unit,
modifier: Modifier
) {
val isDarkMode = isSystemInDarkTheme()
UIKitView(
factory = {
val button = object : PKPaymentButton(
paymentButtonType = PKPaymentButtonTypePlain,
paymentButtonStyle = if (isDarkMode) PKPaymentButtonStyleWhite else PKPaymentButtonStyleBlack
) {
// Object + @ObjC annotation are required to make button clickable
@ObjCAction
fun payAction() {
// TODO implement payment click
}
}
button.setCornerRadius(100.0)
button.addTarget(
target = button,
action = NSSelectorFromString(button::payAction.name),
forControlEvents = UIControlEventTouchUpInside
)
button
},
modifier = modifier
.height(48.dp)
.clip(RoundedCornerShape(32.dp))
)
}

Notice how onClick works with iOS components: to handle clicks, define a function within object : PKPaymentButton and annotate it with @ObjCAction. This function is triggered when UIControlEventTouchUpInside event occurs as we specify NSSelectorFromString(button::payAction.name) as the action.

In this section, we explored the simplest way to separate code by platform using expect/actual declarations. This is the first challenge you’ll encounter when writing iOS code in Kotlin Multiplatform. However, there may be cases where you need to pass a Context or other device-specific dependencies. We will discuss how to achieve this in the next chapter.

Check the created code here

https://github.com/sereden/kmp-architecture-demo/commit/36dc52867aea9d90cf7f7e8f00aa3a72243d95f7

--

--

Responses (2)