Skip to content

Compose Multiplatform (KMP) Setup

Recommended for new projects

Compose Multiplatform is the recommended integration path for new Maverick AI applications. It lets you share UI and business logic across Android and iOS in a single Kotlin codebase.

Overview

A Compose Multiplatform project uses a single shared module (composeApp) that contains all SDK logic and UI. The SDK is consumed as a Maven dependency in commonMain, making the same code available to both Android and iOS targets.

Component Description
Shared module composeApp/ - Kotlin Multiplatform module with commonMain, androidMain, and iosMain source sets
Android entry point MainActivity in androidMain - calls Evs.init(context) and hosts the Compose UI
iOS entry point MainViewController in iosMain - calls Evs.init() and returns a ComposeUIViewController

Project Setup

1) Create the project

Use the Kotlin Multiplatform Wizard or start from the kmp-compose-sample.

2) Configure plugins

In your root build.gradle.kts:

build.gradle.kts (root)
plugins {
    id("com.android.application") version "8.12.0" apply false
    id("org.jetbrains.kotlin.multiplatform") version "2.2.20" apply false
    id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
    id("org.jetbrains.compose") version "1.6.11" apply false
}

3) Add the Maven repository

Add the GitHub Packages Maven repository to your settings.gradle.kts:

settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://maven.pkg.github.com/everysight-maverick-AI/mav-ai-android-maven")
            credentials {
                username = providers.gradleProperty("gpr.user").orNull
                password = providers.gradleProperty("gpr.key").orNull
            }
        }
    }
}

Note

You must create a GitHub personal access token with the read:packages scope.

Create credentials in ~/.gradle/gradle.properties:

gpr.user=YOUR_GITHUB_USERNAME
gpr.key=YOUR_GITHUB_TOKEN_WITH_read:packages

4) Add the SDK dependency

In your shared module composeApp/build.gradle.kts:

composeApp/build.gradle.kts
kotlin {
    androidTarget {
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_17)
        }
    }
    listOf(iosArm64(), iosSimulatorArm64()).forEach { target ->
        target.binaries.framework {
            baseName = "ComposeApp"
        }
    }

    sourceSets {
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material3)
            implementation(compose.ui)
            implementation("com.everysight.mav2:maverick-ai-sdk:0.1.0")
        }
        androidMain.dependencies {
            implementation("androidx.activity:activity-compose:1.10.1")
        }
    }
}

android {
    namespace = "com.example.myapp"
    compileSdk = 36
    defaultConfig {
        minSdk = 30
        targetSdk = 36
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

Note

Use commonMain.dependencies so the SDK is available to both Android and iOS targets from a single declaration.

SDK Initialization

Initialize the SDK and register one IM2ResourcesResolver per platform. Required on KMP (and only on KMP — see the Resources guide for the rules per project type). The same one-liner body works on both platforms.

MainActivity.kt
class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalResourceApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        Evs.init(applicationContext)
        Evs.resourcesService.setResourcesResolver(object : IM2ResourcesResolver {
            override fun loadResource(path: String): ByteArray? = try {
                runBlocking { Res.readBytes("files/$path") }
            } catch (e: Exception) { null }
        })

        setContent {
            App(
                initSdk = { /* already initialized above */ },
                stopSdk = { Evs.stop() }
            )
        }
    }

    override fun onDestroy() {
        Evs.stop()
        super.onDestroy()
    }
}
MainViewController.kt
@OptIn(ExperimentalResourceApi::class)
fun MainViewController(): UIViewController {
    Evs.init()
    Evs.resourcesService.setResourcesResolver(object : IM2ResourcesResolver {
        override fun loadResource(path: String): ByteArray? = try {
            runBlocking { Res.readBytes("files/$path") }
        } catch (e: Exception) { null }
    })
    return ComposeUIViewController { App() }
}

Res is your app's generated <your-app-package>.generated.resources.Res. The files/ prefix is required.

Resources

Put files once in composeApp/src/commonMain/composeResources/... and they are shared between Android and iOS:

Kind Where
API key (sdk.key / app.key) composeApp/src/commonMain/composeResources/files/sdk.key (one shared file, found via the resolver above). For different keys per app store, use androidMain/assets/sdk.key + iosMain/resources/sdk.key.
Glasses content (M2Image, M2Font, AR .obj/.png) composeApp/src/commonMain/composeResources/files/<your-path> (e.g. composeResources/files/images/apple.png, composeResources/files/fonts/foo.evfn, composeResources/files/obj_files/earth/earth.obj)
Phone-UI assets composeApp/src/commonMain/composeResources/{drawable,font,values}/..., accessed via painterResource(Res.drawable.foo) / stringResource(Res.string.foo)

Full picture (including how :composeApp:syncComposeResourcesForIos and the resolver each fit in) is in the Resources guide.

iOS Build Configuration

In your Xcode project's Compile Kotlin Framework Run Script, append :composeApp:syncComposeResourcesForIos. The script in the kmp-compose-sample is:

cd "$SRCROOT/.."
unset EXECUTABLE_BLANK_INJECTION_DYLIB_PATH
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
  echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
else
  SDK_VERSION_ARG=""
  if [ -n "$MAV2_SDK_VERSION" ]; then
    SDK_VERSION_ARG="-PsdkVersion=$MAV2_SDK_VERSION"
  fi
  ./gradlew $SDK_VERSION_ARG \
    :composeApp:embedAndSignAppleFrameworkForXcode \
    :composeApp:syncComposeResourcesForIos
fi

The two block headers are optional but recommended:

  • unset EXECUTABLE_BLANK_INJECTION_DYLIB_PATH clears a Xcode 15+ env var that, when present, makes the Kotlin/Native linker emit a spurious dylib reference.
  • OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED=YES lets you skip the gradle invocation from the IDE when iterating on Swift-only changes — Xcode will still link against whatever framework Kotlin already produced.

The MAV2_SDK_VERSION / SDK_VERSION_ARG lines are also optional for normal usage. If MAV2_SDK_VERSION is unset (the default for everyone outside the SDK release pipeline), SDK_VERSION_ARG stays empty and Gradle resolves the SDK version from gradle.properties:sdkVersion. The env var is only used by the SDK build_system to validate a candidate release before publishing.

If you use CocoaPods (kotlin("native.cocoapods")), the [CP] Copy Pods Resources build phase already handles syncComposeResourcesForIos — no manual addition needed.

Required on KMP iOS

Without :composeApp:syncComposeResourcesForIos (or the CocoaPods equivalent), Res.drawable.* and SDK Compose UI screens (Evs.showUI(...)) will throw MissingResourceException at runtime.

Gradle Properties

Add these to your gradle.properties for optimal KMP/iOS builds:

gradle.properties
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
android.useAndroidX=true
compose.kotlin.native.manageCacheKind=false
kotlin.native.cacheKind=none

Building

# Android debug APK
./gradlew :composeApp:assembleDebug

# iOS framework (from terminal - for CI)
./gradlew :composeApp:compileKotlinIosArm64

For iOS development, build and run through Xcode - select the iOS target scheme and run on a device or simulator. The Gradle build phase in Xcode handles framework compilation and resource sync automatically.


See Also