Skip to content
Back to skills

Kmp Shared Resources

ASecurity

Sets up Compose Multiplatform Resources (compose-resources) for sharing strings, plurals, images, fonts, and raw files across all KMP targets (Android, iOS, Desktop, Web). Covers: resource directory structure, string localization, plurals, image assets, custom fonts, accessing resources from Composables and non-Composable code, and resource module setup. Assumes the project was scaffolded with kmp-feature-scaffold.

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 2, 2026
developmentgokotlintestingapi

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 2, 2026

npx -y skills add ronjunevaldoz/kmp-agent-skills --skill kmp-shared-resources --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Kmp Shared Resources?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Kmp Shared Resources
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ronjunevaldoz-kmp-shared-resources/badge)](https://www.skillsdirectory.com/skills/ronjunevaldoz-kmp-shared-resources)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: kmp-shared-resources
description: >
  Sets up Compose Multiplatform Resources (compose-resources) for sharing strings,
  plurals, images, fonts, and raw files across all KMP targets (Android, iOS,
  Desktop, Web). Covers: resource directory structure, string localization,
  plurals, image assets, custom fonts, accessing resources from Composables and
  non-Composable code, and resource module setup. Assumes the project was scaffolded
  with kmp-feature-scaffold.
license: Apache-2.0
metadata:
  author: kmp-agent-skills
  last-updated: '2026-07-08'
  keywords:
    - Compose Resources
    - KMP resources
    - strings
    - localization
    - fonts
    - images
    - Kotlin Multiplatform
    - Compose Multiplatform
    - plurals
    - px to dp
    - dp to px
    - density bucket
    - mdpi
    - hdpi
    - xhdpi
    - xxhdpi
    - xxxhdpi
    - density scale factor
    - density-independent pixels
---

## Overview

Compose Multiplatform Resources is the standard way to share assets across all KMP targets.
No platform-specific resource systems needed — one `composeResources/` directory works everywhere.

```
:core:ui (or any module applying composeResources)
  src/commonMain/composeResources/
    drawable/         # SVG, PNG, WebP images
    drawable-dark/    # Dark theme variants
    drawable-xxhdpi/  # Density-specific rasters (Android-style)
    font/             # .ttf / .otf fonts
    values/           # strings.xml, plurals.xml
    values-es/        # Localized strings (Spanish)
  files/            # Raw files (JSON, certificates, etc.)
```

## When to Use This Skill

Use this skill when you need to:
- Share strings, plurals, images, fonts, or raw files across KMP targets
- Set up `Res` for a Compose Multiplatform UI module
- Localize content or package shared fonts and icons
- Recheck Compose Resources docs before changing the resource layout

**Trigger keywords:** shared resources, compose resources, strings, strings.xml, hardcoded strings, stringResource, plurals, fonts,
images, localization, resource module, Res, resource packaging,
i18n, l10n, internationalization, translations, localize app, app strings KMP,
string resources KMP, multiplatform images, drawable KMP, translate strings.

**Freshness rule:** recheck the current Compose Multiplatform Resources docs before
upgrading the plugin or moving resource directories.

---

## Recommendation First

Default to **CMP Resources (`composeResources/`) in `commonMain` for strings, images, and fonts**.

Why:
- a single `Res.string.xxx` accessor works on all targets — no platform-specific resource files
- the Compose Resources plugin generates type-safe accessors at build time
- localization is handled through standard `values-xx/strings.xml`-style directories

Use platform resource bundles only when you need to share resources with non-Compose targets
(e.g., a pure-Kotlin server module that also needs the same string definitions).

---

## Prerequisites

- Project scaffolded with `kmp-feature-scaffold`
- Module applies `GROUP_ID.feature.ui` or `GROUP_ID.core` convention plugin with Compose enabled
- `compose-multiplatform` plugin applied (`id("org.jetbrains.compose")`)

---

## Version Reference

Compose Resources ships with Compose Multiplatform — no extra dependency needed.

```toml
[versions]
compose-multiplatform = "1.11.1"   # already in libs.versions.toml
```

The `compose.components.resources` accessor (already in `GROUP_ID.feature.ui` convention plugin) enables the resource system.

---

## Step 1: Enable resources in the module

In the module that owns shared resources (e.g. `:core:ui`), confirm `compose.components.resources` is in `commonMain`:

```kotlin
// GROUP_ID.feature.ui.gradle.kts or :core:ui/build.gradle.kts
sourceSets {
    commonMain.dependencies {
        implementation(compose.components.resources)
    }
}
```

Optionally set a custom package for the generated `Res` accessor:

```kotlin
// :core:ui/build.gradle.kts
compose.resources {
    publicResClass = true          // make Res accessible from other modules
    packageOfResClass = "GROUP_ID.core.ui.resources"
    generateResClass = always      // always, auto (default), never
}
```

---

## Step 2: Strings

Create `src/commonMain/composeResources/values/strings.xml`:

```xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">My App</string>
    <string name="welcome_message">Welcome, %1$s!</string>
    <string name="retry">Retry</string>
    <string name="error_generic">Something went wrong. Please try again.</string>
    <string name="empty_state_title">Nothing here yet</string>
    <string name="empty_state_body">Pull down to refresh or check back later.</string>
</resources>
```

**Localized strings** — create `values-<locale>/strings.xml`:

`src/commonMain/composeResources/values-es/strings.xml`:

```xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Mi App</string>
    <string name="welcome_message">¡Bienvenido, %1$s!</string>
    <string name="retry">Reintentar</string>
    <string name="error_generic">Algo salió mal. Inténtalo de nuevo.</string>
</resources>
```

---

## Step 3: Plurals

`src/commonMain/composeResources/values/plurals.xml`:

```xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <plurals name="item_count">
        <item quantity="one">%1$d item</item>
        <item quantity="other">%1$d items</item>
    </plurals>

    <plurals name="minutes_ago">
        <item quantity="one">%1$d minute ago</item>
        <item quantity="other">%1$d minutes ago</item>
    </plurals>
</resources>
```

---

## Step 4: Images

Place images in `src/commonMain/composeResources/drawable/`:

- Use **SVG** for icons (scales perfectly, smallest size) — see
  `kmp-imagevector-generator` to compile SVGs into `ImageVector` code
  instead of shipping them as raster/vector resource files
- Use **PNG/WebP** for photos or complex rasters
- Dark variants: `drawable-dark/` folder with same filenames
- Density rasters: `drawable-mdpi/`, `drawable-hdpi/`, `drawable-xhdpi/`, `drawable-xxhdpi/`, `drawable-xxxhdpi/`

```
composeResources/
  drawable/
    ic_logo.svg
    img_onboarding.png
  drawable-dark/
    ic_logo.svg    ← dark variant, same name
```

### Density buckets and px/dp conversion

**dp (density-independent pixels) is the design source of truth, not raw pixels.**
Design specs, spacing tokens, and Compose `.dp` values are always defined in dp; a
device's density scale factor determines how many physical pixels render 1dp.

| Bucket | Scale factor | `24.dp` icon exported at |
|---|---|---|
| ldpi | 0.75x | 18px |
| mdpi (baseline) | 1.0x | 24px |
| hdpi | 1.5x | 36px |
| xhdpi | 2.0x | 48px |
| xxhdpi | 3.0x | 72px |
| xxxhdpi | 4.0x | 96px |

**Primary direction — exporting a density raster (`px = dp × scale factor`):** when a
design tool hands you a fixed-size PNG/WebP per bucket for `drawable-<density>/`, use
this to verify the asset is the right size for its folder (a `24dp` icon needs a 96px
file in `drawable-xxxhdpi/`, not 24px).

**Reverse direction — a fixed raw pixel count shrinks in dp terms as density increases
(`dp = px / scale factor`):** this is why a **hardcoded 1px hairline border** is a real
bug, not a style nitpick — the same 1 raw pixel is 1.0dp on mdpi but only 0.25dp on
xxxhdpi, and sub-pixel dp values render inconsistently (sometimes invisible, sometimes
anti-aliased differently per device). Always specify strokes/borders in `.dp` (e.g.
`Modifier.border(1.dp, ...)`), never as a raw pixel count — Compose handles the
scale-factor math for you; hand-computing it per bucket is both unnecessary and the
source of this exact class of bug.

---

## Step 5: Fonts

Place `.ttf` or `.otf` files in `src/commonMain/composeResources/font/`:

```
composeResources/
  font/
    inter_regular.ttf
    inter_medium.ttf
    inter_bold.ttf
```

---

## Step 6: Raw files

Place arbitrary files in `src/commonMain/composeResources/files/`:

```
composeResources/
  files/
    config.json
    certificates/
      root_ca.pem
```

---

## Step 7: Using resources in Composables

The Gradle plugin generates a `Res` object. Import from the package set in `compose.resources {}`:

```kotlin
import GROUP_ID.core.ui.resources.Res
import GROUP_ID.core.ui.resources.*
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.Font

// Strings
@Composable
fun WelcomeScreen(userName: String) {
    Text(stringResource(Res.string.welcome_message, userName))
}

// Plurals
@Composable
fun ItemCount(count: Int) {
    Text(pluralStringResource(Res.plurals.item_count, count, count))
}

// Images
@Composable
fun AppLogo() {
    Image(
        painter = painterResource(Res.drawable.ic_logo),
        contentDescription = stringResource(Res.string.app_name)
    )
}

// Fonts
val InterRegular = FontFamily(Font(Res.font.inter_regular))
val InterBold    = FontFamily(Font(Res.font.inter_bold))

val AppTypography = Typography(
    bodyLarge = TextStyle(fontFamily = InterRegular, fontSize = 16.sp),
    titleLarge = TextStyle(fontFamily = InterBold, fontSize = 22.sp),
)
```

---

## Step 8: Using resources outside Composables

For non-Composable code (ViewModels, repositories), use the suspend API:

```kotlin
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.getStringArray

// In a ViewModel / use case:
suspend fun getErrorMessage(): String =
    getString(Res.string.error_generic)

suspend fun getItemCountLabel(count: Int): String =
    pluralString(Res.plurals.item_count, count, count)
```

---

## Step 9: Raw file access

```kotlin
import org.jetbrains.compose.resources.ExperimentalResourceApi

@OptIn(ExperimentalResourceApi::class)
suspend fun loadConfig(): String {
    val bytes = Res.readBytes("files/config.json")
    return bytes.decodeToString()
}
```

---

## Step 10: Theme wiring

Create `src/commonMain/kotlin/GROUP_ID/core/ui/theme/AppTheme.kt`:

```kotlin
package GROUP_ID.core.ui.theme

import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.sp
import GROUP_ID.core.ui.resources.Res
import GROUP_ID.core.ui.resources.*
import org.jetbrains.compose.resources.Font

@Composable
fun AppTheme(
    darkTheme: Boolean = false,
    content: @Composable () -> Unit,
) {
    MaterialTheme(
        typography = Typography(
            bodyLarge = TextStyle(fontFamily = FontFamily(Font(Res.font.inter_regular)), fontSize = 16.sp),
            titleLarge = TextStyle(fontFamily = FontFamily(Font(Res.font.inter_bold)), fontSize = 22.sp),
        ),
        content = content
    )
}
```

---

## Step 11: Share Res across modules

By default, `Res` is internal to the module it's generated in. To share across modules:

```kotlin
// :core:ui/build.gradle.kts
compose.resources {
    publicResClass = true
    packageOfResClass = "GROUP_ID.core.ui.resources"
}
```

Then in consuming modules:

```kotlin
// :feature:home:ui/build.gradle.kts
sourceSets {
    commonMain.dependencies {
        implementation(projects.core.ui)   // gets access to :core:ui Res
    }
}
```

---

## Guidelines

- Keep all shared resources in a single `:core:ui` or `:core:resources` module — avoid scattering across feature modules
- Always provide at least a default locale (`values/strings.xml`) — never rely on locale-only files
- Treat `values/strings.xml` as the source of truth for user-facing copy in Compose screens
- Use SVG for all icons — PNG is only for photos or assets that need per-density control
- Avoid hardcoded strings in Composables — always use `stringResource(Res.string.*)` or a resource-backed API
- Font files should be subset to only the characters your app uses — reduces binary size
- Use `files/` for config or data assets; never put secrets there

---

## Verification

1. `./gradlew :core:ui:generateCommonMainResourceAccessors` — `Res` object generated
2. `./gradlew :core:ui:compileKotlinMetadata` — resource accessors compile in commonMain
3. Launch Android — verify strings, images, and fonts render correctly
4. Launch Desktop (`./gradlew :desktopApp:run`) — verify same resources render
5. Change device language to a supported locale — verify localized strings appear

---

## Testing

Full content: `references/testing.md`.

## Common Anti-Patterns

- hardcoding string literals in composables instead of using `Res.string` — blocks localization
- putting user-facing copy directly in `Text("...")`, `AppText(text = "...")`, or `contentDescription = "..."`
- storing images outside `composeResources/` — they won't be picked up by the resource accessor generator
- using platform-specific string files (`strings.xml` on Android only) for shared strings
- importing `Res` from a non-Compose module — resource accessors require a Compose-enabled source set
- forgetting to add new locales to `gradle/libs.versions.toml` locale list — localized strings are silently ignored
- hardcoding a raw pixel count for a border/stroke/divider instead of `.dp` — the same raw pixel is 1.0dp on mdpi but only 0.25dp on xxxhdpi, rendering inconsistently (sometimes invisible) across densities; always specify strokes in `.dp` and let Compose apply the density scale factor
- exporting a density raster (`drawable-<bucket>/`) at the wrong pixel size for its folder — verify with `px = dp × scale factor` (e.g. a 24dp icon needs 96px in `drawable-xxxhdpi/`, not 24px)

If `Res.string.xxx` is unresolved, run `./gradlew generateCommonMainResourceAccessors` to regenerate accessors.

---

## Related Skills

- `kmp-feature-scaffold` — convention plugins that enable `composeResources/` in each module
- `kmp-compose-design-system` — fonts and icons distributed via `Res` accessors inside the design system
- `kmp-compose-preview-driven-development` — Desktop previews that consume `Res.string` and `Res.drawable`
- `kmp-expect-actual` — platform-specific resource loading when CMP `Res` is insufficient

---

## Output Style

When asked about shared resources or localization, respond in this order:
1. recommendation (CMP Resources, strings/images/fonts in commonMain)
2. project structure (`:core:ui/src/commonMain/composeResources/`)
3. code snippet (one string or image accessor)
4. why shared resources are preferred over platform-specific resource bundles
5. main alternative (expect/actual resource loading, platform bundles)

Keep the snippet to one resource type. Map to the user's actual resource names when provided.

---

## Changelog

| Date | Change |
|---|---|
| 2026-08-04 | Split "Testing" out of SKILL.md into `references/testing.md`, leaving a pointer stub. SKILL.md drops from 504 to 468 lines, clearing the agentskills.io 500-line recommendation. No content removed, only relocated. Last of the 22-skill KI-008 backlog — all now at or under 500 lines. |
| 2026-07-08 | Added a "Density buckets and px/dp conversion" reference: the scale-factor table (ldpi 0.75x through xxxhdpi 4.0x), the primary `px = dp × scale factor` direction for exporting correctly-sized density rasters, and the reverse `dp = px / scale factor` direction explaining why a hardcoded raw-pixel border/hairline renders inconsistently across densities. Also fixed the density raster list, which omitted `drawable-xxxhdpi/`. 2 new anti-patterns. |
| 2026-06-06 | Initial release. |

Files in this skill

  • SKILL.md15.3 KB
  • references/testing.md1.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.