athevon/genjutsu3 files

Compose Multiplatform

Compose Multiplatform / KMP patterns - expect/actual composables, platform-specific code, density and font handling cross-target, iOS/Android/Desktop interop.

Specification
Skill ID
athevon/genjutsu/compose-multiplatform
Publisher
athevon
Repository
genjutsu
Installs
0
Files
3
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

athevon/genjutsu/compose-multiplatformInstalls these files
  • SKILL.md
  • references/cmp-interop.md
  • references/cmp-platform-quirks.md

What this skill tells the agent

Version-sensitive. Every API name, SDK gate and browser-support claim below was verified on 2026-09-08 against primary sources. What against, and when, is in _jutsu/VERSIONS.md. If that date is old, re-verify before acting on a version number.

Compose Multiplatform

Compose Multiplatform (CMP) and Kotlin Multiplatform (KMP) patterns for cross-platform UI. Loaded for projects with org.jetbrains.compose plugin. Foundation: ../compose-motion/SKILL.md for animation API; this file covers what's specific to writing one Compose codebase for Android + iOS + Desktop + Web.

KMP vs CMP - quick clarification

KMP (Kotlin Multiplatform) is the language and build infrastructure: shared Kotlin code compiled to JVM, Native (iOS, macOS, Linux, Windows), and Wasm. CMP (Compose Multiplatform) is the UI framework on top of KMP, built by JetBrains as a port of Jetpack Compose. You write a single Compose codebase in commonMain that runs on Android, iOS, Desktop (JVM), and Web (Wasm). Platform-specific code lives in androidMain, iosMain, desktopMain, wasmJsMain and is wired in via expect/actual declarations.


Project structure

composeApp/
├── 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)

The commonMain folder should hold 80-95% of your code in a well-architected CMP project. If iosMain or androidMain start growing past a few hundred lines, you're probably leaking platform concerns into UI logic that could stay shared.


expect/actual pattern

The KMP escape hatch when you genuinely need different implementations per target. Declare the contract once in commonMain, implement it once per target.

// 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)
}

expect/actual works for top-level functions, classes, type aliases, and properties. The signature in actual must match exactly, including modifiers and default values.


expect/actual for composables

Composables follow the same rules. Useful when a feature needs a platform-specific Compose API (Android RuntimeShader, iOS UIKitView, Desktop SwingPanel).

// 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.effectWithStyle(
                        UIBlurEffectStyle.UIBlurEffectStyleSystemMaterial
                    )
                )
            },
            modifier = Modifier.matchParentSize()
        )
        content()
    }
}

Rule: expect composables should be the exception, not the rule. Most "platform feel" differences can be tuned via tokens (colors, corner radii, spring stiffness) in commonMain, not via separate code paths.


LocalDensity cross-platform

On Android, LocalDensity.current.density reflects the device DPI bucket (1.0, 1.5, 2.0, 3.0...). On iOS, density is computed from UIScreen.scale (typically 2.0 or 3.0 on Retina). On Desktop, density depends on the screen scaling factor (1.0 by default; 2.0 on Retina-class displays; user-configurable on Windows). On Wasm, density follows window.devicePixelRatio.

Don't hardcode Dp to pixel ratios; trust Dp and LocalDensity to handle conversion. If you need an exact pixel value (e.g., for a Canvas draw operation), do the conversion explicitly:

val density = LocalDensity.current
val pxValue = with(density) { 16.dp.toPx() }

Avoid reading density inside hot loops; cache the conversion.


LocalConfiguration and platform-aware UI

LocalConfiguration.current is Android-only and lives in androidMain. For CMP, prefer the cross-platform alternatives:

  • LocalWindowInfo.current.containerSize - the window/screen size as IntSize, available in commonMain.
  • LocalDensity.current - density, available in commonMain.
  • LocalLayoutDirection.current - LTR / RTL.
  • BoxWithConstraints { ... } - read maxWidth / maxHeight directly inside layout.

If you need real device characteristics (orientation, idiom, model), wrap the access in expect/actual and pass a typed object like PlatformInfo to the common layer.


Fonts cross-platform via Compose Resources

org.jetbrains.compose.resources is the shared resources plugin. Drop fonts in commonMain/composeResources/font/, and the Gradle plugin generates a typed Res accessor.

composeApp/src/commonMain/composeResources/
├── font/
│   ├── Inter-Regular.ttf
│   └── Inter-Bold.ttf
├── drawable/
│   └── logo.xml            ← Android XML vector. SVG works on every target EXCEPT Android
└── values/
    ├── strings.xml          ← default locale
└── values-fr/
    └── strings.xml          ← French overrides (qualifier goes on the DIRECTORY)

Usage in commonMain:

import 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)

Same pattern for Res.drawable.logo (image) and Res.string.app_name (localized string via stringResource(...)). Raw files are the exception: there is no generated Res.file.* accessor - you pass a path string to the suspend function Res.readBytes(path), e.g. val bytes = Res.readBytes("files/config.json"), for anything under composeResources/files/.


iOS interop with SwiftUI

CMP produces a UIViewController you can drop into a SwiftUI app. KMP generates a top-level Kotlin function (commonly named MainViewController() or ComposeUIViewController { ... }) that returns a UIViewController. Wrap it with UIViewControllerRepresentable.

// 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() }
}

Use this skill in a project

Open any RiverX project, search the Skills panel for athevon/genjutsu/compose-multiplatform, and install it in one click.

Use this skill