2

I created a compose-multiplatform library for sharing UI between my projects following the guide of this website https://medium.com/@shubhamsinghshubham777/how-to-write-a-compose-multiplatform-library-66ae1b7edb81. In the library I have some composeables where I use drawables (e.g. icon_xy.xml) in an Image-Composable. E.g:

@Composable
fun SimpleLoadingBar(
    modifier: Modifier = Modifier.size(LOADING_BAR_WIDTH.dp),
    isLoading: MutableState<Boolean> = remember { mutableStateOf(true) },
) {
    val rotation = remember { Animatable(0f) }
    LaunchedEffect(isLoading) {
        if (isLoading.value) {
            // Animiere die Rotation unendlich
            rotation.animateTo(
                targetValue = 360f,
                animationSpec = infiniteRepeatable(
                    animation = tween(durationMillis = 1000, easing = EaseOutQuart),
                    repeatMode = RepeatMode.Restart
                )
            )
        } else {
            // Stoppe die Animation, wenn nicht geladen wird
            rotation.stop()
        }
    }

    Image(
        painter = painterResource(DrawableResource("icons/icon_xy.xml")),
        contentDescription = "Loading Animation",
        modifier = modifier.graphicsLayer(rotationZ = rotation.value),
        contentScale = ContentScale.Fit
    )
}

When I use the library in the sample compose app (included by implementation(project(":library")) in the commonMain dependencies or by using an artifactory), the resources are working for desktop and android, but in web and iOS the resources are not found/working. When I use them in the compose-app directly, the resources are working. Anyone got a clue what i could do?

Here is my library build-gradle:

import org.jetbrains.kotlin.cli.common.toBooleanLenient
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl

plugins {
    //Versions are being handled in libs.versions.toml

    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidLibrary)
    alias(libs.plugins.jetbrainsCompose)
    alias(libs.plugins.kotlinCocoapods)
    alias(libs.plugins.mavenPublish)
}

kotlin {
    androidTarget {
        publishLibraryVariants("release")
    }

    jvm("desktop")

    @OptIn(ExperimentalWasmDsl::class)
    wasmJs {
        browser()
        binaries.executable()
    }

    iosArm64()
    iosX64()
    iosSimulatorArm64()

    cocoapods {
        version = when (shouldPublish()) {
            true -> getArtifactVersion()
            false -> "local"
        }
        summary = "Shared UI Elements for stackoverflow"
        homepage = "empty"

        ios.deploymentTarget = libs.versions.ios.deploymentTarget.get()
        framework {
            baseName = "ComposeApp"
            isStatic = true
        }
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material3)
                implementation(compose.components.resources)
                implementation(libs.kotlin.dateTime)
            }
        }
        val androidMain by getting {
            dependsOn(commonMain)
            dependencies {
            }
        }
        val desktopMain by getting {
            dependsOn(commonMain)
            dependencies {
                implementation(compose.desktop.common)
            }
        }
        val wasmJsMain by getting {
            dependsOn(commonMain)
            dependencies {
            }
        }
        val iosMain by creating {
            dependsOn(commonMain)
            dependencies {
            }
        }
        val iosX64Main by getting {
            dependsOn(iosMain)
        }
        val iosArm64Main by getting {
            dependsOn(iosMain)
        }
        val iosSimulatorArm64Main by getting {
            dependsOn(iosMain)
        }
    }

    withSourcesJar()
}

android {
    compileSdk = libs.versions.android.compileSdk.intValue
    namespace = "com.stackoverflow.shared_ui"

    sourceSets["main"].res.srcDirs("src/androidMain/res")
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    sourceSets["main"].resources.srcDirs("src/commonMain/resources")

    defaultConfig {
        minSdk = libs.versions.android.minSdk.intValue
    }

    compileOptions {
        sourceCompatibility = JavaVersion.toVersion(libs.versions.android.javaVersion.intValue)
        targetCompatibility = JavaVersion.toVersion(libs.versions.android.javaVersion.intValue)
    }
    kotlin {
        jvmToolchain(libs.versions.android.javaVersion.intValue)
    }
}

publishing {
    if (!shouldPublish()) return@publishing

    repositories {
        maven {
            url = uri(getEnvValue("REGISTRY_URL"))
            credentials {
                username = getEnvValue("REGISTRY_USERNAME")
                password = getEnvValue("REGISTRY_PASSWORD")
            }
        }
    }
}

tasks.register("printPackageName") {
    doLast {
        println("Published and Released version ${getArtifactVersion()} ✅")
    }
}

mavenPublishing {
    if (!shouldPublish()) return@mavenPublishing

    coordinates("com.stackoverflow.shared_ui", "shared-ui", getArtifactVersion())

    pom {
        name.set(project.name)
        description.set("Shared UI Elements for stackoverflow.")
        inceptionYear.set("2023")
        scm {
            val projectLocation = "github.com/${getEnvValue("GITHUB_REPOSITORY")}"

            url.set("https://$projectLocation")
            connection.set("scm:git:git://$projectLocation.git")
            developerConnection.set("scm:git:ssh://git@$projectLocation.git")
        }
    }
}

val Provider<String>.intValue get() = get().toInt()

fun getEnvValue(name: String) = System.getenv(name) ?: throw Exception("$name not given")

fun findIntProperty(name: String) = (findProperty(name) as String?)?.toInt() ?: throw Exception("$name not given")

fun shouldPublish() = System.getenv("PUBLISH")?.toBooleanLenient() == true

fun getArtifactVersion() = getEnvValue("GITHUB_HEAD_REF").replace('/', '-') +
        "-${getEnvValue("GITHUB_RUN_ID")}-${getEnvValue("GITHUB_RUN_ATTEMPT")}"

And here is my sample compose-app build-gradle:

import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl

plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidApplication)
    alias(libs.plugins.jetbrainsCompose)
}

kotlin {
    @OptIn(ExperimentalWasmDsl::class)
    wasmJs {
        moduleName = "composeApp"
        browser {
            commonWebpackConfig {
                outputFileName = "composeApp.js"
            }
        }
        binaries.executable()
    }
    
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "1.8"
            }
        }
    }
    
    jvm("desktop")
    
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "ComposeApp"
            isStatic = true
        }
    }
    
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(project(":library"))
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material)
                implementation(compose.ui)
                @OptIn(ExperimentalComposeLibrary::class)
                implementation(compose.components.resources)
            }
        }
        val desktopMain by getting {
            dependsOn(commonMain)
            dependencies {
                implementation(compose.desktop.currentOs)
            }
        }
        
        androidMain.dependencies {
            implementation(libs.compose.ui.tooling.preview)
            implementation(libs.androidx.activity.compose)
        }
        val wasmJsMain by getting {
            dependsOn(commonMain)
            dependencies {
            }
        }

        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting
        val iosMain by creating {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            iosSimulatorArm64Main.dependsOn(this)
        }
    }
}

android {
    namespace = "com.stackoverflow.shared.ui"
    compileSdk = libs.versions.android.compileSdk.get().toInt()

    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    sourceSets["main"].res.srcDirs("src/androidMain/res")
    sourceSets["main"].resources.srcDirs("src/commonMain/resources")

    defaultConfig {
        applicationId = "com.stackoverflow.shared.ui"
        minSdk = libs.versions.android.minSdk.get().toInt()
        targetSdk = libs.versions.android.targetSdk.get().toInt()
        versionCode = 1
        versionName = "1.0"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    dependencies {
        debugImplementation(libs.compose.ui.tooling)
    }
}

compose.desktop {
    application {
        mainClass = "MainKt"

        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "com.stackoverflow.shared.ui"
            packageVersion = "1.0.0"
        }
    }
}

compose.experimental {
    web.application {}
}

And the versions are defined in libs.versions.toml:

[versions]
agp = "8.2.2"
android-compileSdk = "34"
android-minSdk = "24"
android-targetSdk = "34"
android-javaVersion = "17"
ios-deploymentTarget = "11.0"
androidx-activityCompose = "1.8.2"
androidx-appcompat = "1.6.1"
androidx-constraintlayout = "2.1.4"
androidx-core-ktx = "1.12.0"
androidx-espresso-core = "3.5.1"
androidx-material = "1.11.0"
androidx-test-junit = "1.1.5"
compose = "1.6.1"
compose-plugin = "1.6.0-beta02"
junit = "4.13.2"
kotlin = "1.9.22"
publish-version = "0.25.3"
kotlin-dateTime = "0.5.0"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
kotlin-dateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlin-dateTime"}

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" }
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "publish-version" }

1
  • 3
    > Multimodule projects are not supported yet. The JetBrains team is working on adding this functionality in future releases. For now, store all resources in the main application module. jetbrains.com/help/kotlin-multiplatform-dev/… Commented Feb 24, 2024 at 20:17

1 Answer 1

7

Starting with 1.6.10, you can place resources in any module or source set, as long as you are using Kotlin 2.0.0 or newer, and Gradle 7.6 or newer.

source: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-images-resources.html

I made it work after adding the following to my KMM library:

compose.resources {
    publicResClass = true
    packageOfResClass = "me.sample.library.resources"
    generateResClass = always
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.