Loading...
Loading...
Compose Multiplatform / KMP patterns - expect/actual composables, platform-specific code, density and font handling cross-target, iOS/Android/Desktop interop.
npx skill4agent add athevon/genjutsu compose-multiplatformCompose Multiplatform (CMP) and Kotlin Multiplatform (KMP) patterns for cross-platform UI. Loaded for projects withplugin. Foundation:org.jetbrains.composefor animation API; this file covers what's specific to writing one Compose codebase for Android + iOS + Desktop + Web.../compose-motion/SKILL.md
commonMainandroidMainiosMaindesktopMainwasmJsMainexpectactualcomposeApp/
├── src/
│ ├── commonMain/ ← shared Compose code (most of the app)
│ │ └── kotlin/
│ ├── androidMain/ ← Android-specific (uses Activity, Context)
│ ├── iosMain/ ← iOS-specific (uses UIKit/UIView interop)
│ ├── desktopMain/ ← JVM desktop (uses java.awt/swing if needed)
│ └── wasmJsMain/ ← Wasm web target
├── build.gradle.kts
iosApp/ ← Xcode project consuming the generated framework
androidApp/ ← Android Application module (often merged into composeApp)commonMainiosMainandroidMainexpectactualcommonMain// commonMain
expect fun openShareSheet(text: String)
// androidMain
actual fun openShareSheet(text: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
}
context.startActivity(Intent.createChooser(intent, null))
}
// iosMain
actual fun openShareSheet(text: String) {
val activityVC = UIActivityViewController(
activityItems = listOf(text),
applicationActivities = null
)
UIApplication.sharedApplication.keyWindow
?.rootViewController
?.presentViewController(activityVC, true, null)
}expectactualactualexpectactualRuntimeShaderUIKitViewSwingPanel// commonMain
@Composable
expect fun PlatformBlur(modifier: Modifier = Modifier, content: @Composable () -> Unit)
// androidMain (uses RuntimeShader on Android 13+)
@Composable
actual fun PlatformBlur(modifier: Modifier, content: @Composable () -> Unit) {
Box(modifier.graphicsLayer { renderEffect = blurEffect }) { content() }
}
// iosMain (uses UIVisualEffectView via UIKitView)
@Composable
actual fun PlatformBlur(modifier: Modifier, content: @Composable () -> Unit) {
Box(modifier) {
UIKitView(
factory = { UIVisualEffectView(effect = UIBlurEffect.systemMaterial()) },
modifier = Modifier.matchParentSize()
)
content()
}
}expectcommonMainLocalDensityLocalDensity.current.densityUIScreen.scalewindow.devicePixelRatioDpDpLocalDensityCanvasval density = LocalDensity.current
val pxValue = with(density) { 16.dp.toPx() }densityLocalConfigurationLocalConfiguration.currentandroidMainLocalWindowInfo.current.containerSizeIntSizecommonMainLocalDensity.currentcommonMainLocalLayoutDirection.currentBoxWithConstraints { ... }maxWidthmaxHeightexpectactualPlatformInfoorg.jetbrains.compose.resourcescommonMain/composeResources/font/RescomposeApp/src/commonMain/composeResources/
├── font/
│ ├── Inter-Regular.ttf
│ └── Inter-Bold.ttf
├── drawable/
│ └── logo.svg
└── values/
├── strings.xml ← default locale
└── strings.fr.xml ← French overridescommonMainimport myproject.composeapp.generated.resources.Inter_Regular
import myproject.composeapp.generated.resources.Inter_Bold
import myproject.composeapp.generated.resources.Res
val InterFamily = FontFamily(
Font(Res.font.Inter_Regular, FontWeight.Normal),
Font(Res.font.Inter_Bold, FontWeight.Bold),
)
Text("Hello", fontFamily = InterFamily)Res.drawable.logoRes.string.app_namestringResource(...)Res.file.configRes.readBytes(...)UIViewControllerMainViewController()ComposeUIViewController { ... }UIViewControllerUIViewControllerRepresentable// iosMain/kotlin/main.ios.kt
fun MainViewController(): UIViewController = ComposeUIViewController {
AppContent() // commonMain composable
}// iOS app target
import SwiftUI
import ComposeApp // KMP-generated framework
struct ComposeContent: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
Main_iosKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View { ComposeContent().ignoresSafeArea() }
}Main_iosKt.MainViewController()main.ios.ktsetContent { ... }// androidApp/src/main/kotlin/MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppContent() // commonMain composable
}
}
}ContextActivitycommonMainexpect class PlatformContextcommonMainactual class PlatformContext(val context: Context)androidMainUIKitViewUIViewUIKitViewControllerUIViewController// iosMain
UIKitView(
factory = {
UISwitch().apply {
addTarget(target, action = NSSelectorFromString("onToggle:"), forControlEvents = UIControlEventValueChanged)
}
},
modifier = Modifier.size(48.dp, 32.dp)
)UIHostingController@objcreferences/cmp-interop.mdanimate*AsStateAnimatedVisibilityupdateTransitionSharedTransitionLayoutcommonMainModifier.draggableModifier.pointerInput../compose-motion/SKILL.mdModalNavigationDrawerLayoutDirection.RtlimePadding()IOSKeyboardEventListenerWindowInsetsColor.parseHex(...)Color(0xFFRRGGBB)FontFamily.SansSerifUIKitViewUILabeljava.util.UUIDjava.io.FilecommonMainkotlinx.uuidkotlinx-iookioSharedTransitionLayoutLocalLifecycleOwnerSwitchDatePickercompose-multiplatform-resourcesmoko-resourceskotlinx.coroutines.wasmwasmJsBrowserDistributionLayout Inspector| BAD | GOOD | Why |
|---|---|---|
Reflection trick or | | Reflection breaks on Wasm/Native; |
Assuming Android | Inject a typed dependency via | |
| Hardcoding Material colors that look great on Android but jarring on iOS | Define a | Cross-platform consistency is good, but iOS users notice when a Material blue feels alien on iPhone |
| | Tight coroutine loops drain battery on iOS; infinite transitions pause when offscreen |
| Need | Load |
|---|---|
| iOS / Android interop deep-dive | |
| Per-platform behavior catalog | |
| Animation API | |
| Advanced graphics (M3 Expressive, AGSL on Android only) | |
| iOS-side native interop with SwiftUI | |
| Mobile UX context | |
| Desktop UX context | |
| Foundation | |