
Resource sharing in KMP
A fast, lightweight, and type-safe Kotlin Multiplatform (KMP) engine to manage application themes, share design tokens, and bundle localized strings and graphics synchronously across Compose Multiplatform, Jetpack Compose, Android XML Layouts, SwiftUI, and iOS UIKit.
Instead of relying on heavy code generation, kmp-design-system translates design tokens and assets
directly into standard platform-native resource structures (such as Android XML Theme Attributes,
iOS Asset Catalogs, and iOS localized strings). This enables completely synchronous, zero-copy, and
frame-rate-safe resource rendering across all native and Compose UI stacks.
For a deep-dive conceptual explanation of the challenges of KMP resource sharing and how the Resource Contracts pattern solves them, see docs/resource_contracts_concept.md.
Below is the sample application demonstrating dynamic theme variants (Global Purple vs. Emerald Green), light/dark mode swaps, localized greeting text, and vector image rendering.
| Android | iOS |
|---|---|
![]() |
![]() |
To use this engine, your development environment and target project must satisfy the following:
8.5 or higher2.1.0 or higher (tested with 2.1.20)1.8.0 or higher (tested with 1.8.2)29, compileSdk 36
15.0 or higher (Xcode 15.0+ required for framework assembly)To use the design system library in any KMP application, configure it using Gradle's Version Catalog.
Add the design system plugin and runtime dependency to your client application's version catalog (gradle/libs.versions.toml):
[versions]
# Check the live Maven Central badge at the top for the latest release version
design-system = "1.0.0-beta08"
[libraries]
design-system = { group = "com.savantarch", name = "design-system", version.ref = "design-system" }
[plugins]
design-system = { id = "com.savantarch.designsystem", version.ref = "design-system" }In your shared UI module's build.gradle.kts (e.g. :app-shared), apply the plugin using its catalog alias and configure the designSystem extension block:
plugins {
alias(libs.plugins.design.system)
}
designSystem {
// Directory containing Apple .xcassets folder
// default: "src/iosMain/resources/Assets.xcassets"
xcassetsDir.set(layout.projectDirectory.dir("src/iosMain/resources/Assets.xcassets"))
// Directory containing raw localizations and assets
// default: "src/iosMain/resources"
resourcesDir.set(layout.projectDirectory.dir("src/iosMain/resources"))
// Optional: iOS deployment target
// default: "15.0"
minimumDeploymentTarget.set("15.0")
// Optional: Target iOS devices
// default: ["iphone", "ipad"]
targetDevices.set(listOf("iphone", "ipad"))
}Add the core design system runtime library to your shared UI module's commonMain dependencies:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.design.system)
}
}
}The :design-system module is app-agnostic. It provides token interfaces, asset contracts, and the
core DesignSystemTheme Composable. Client applications are responsible for satisfying these
contracts by defining concrete values for standard tokens (which are based on Material 3 designs),
and optionally extending them with custom tokens.
Here is how each type of design resource is modeled, extended, and provided via
staticCompositionLocalOf:
[!TIP] Use Delegation Wrappers (
AppXyz) Even if your application does not initially require custom design tokens, we recommend that you always declare custom client delegation wrapper classes (e.g.AppColors,AppShapes,AppTypographywrapping their library-levelDsColors,DsShapes,DsTypographyequivalents) and expose them on your publicAppThemeAPI.Doing so future-proofs your theme implementation: if you need to introduce new custom tokens in the future, you can add them directly to your application wrappers without altering
AppTheme's return types, preventing breaking API changes across your Kotlin and Swift codebases.
DsColors interface containing standard Material 3 color
properties (e.g., primary, background, surface). The client app must define values for these
properties.promoBrand) wrapping the
base DsColors.class AppColors(
private val base: DsColors,
val promoBrand: Long
) : DsColors by baseBox(modifier = Modifier.background(AppTheme.colors.background.toColor()))Text(text = "Hello", color = AppTheme.colors.primary.toColor())<View android:background="?attr/colorPrimary" />@ObservedObject private var theme = AppThemeSwift.shared
Text("Hello").foregroundColor(theme.colors.primary.color)view.backgroundColor = AppThemeSwift.shared.colors.background.uiColorDsShapes interface containing shape tokens (e.g.,
small, medium, large). The client app must define values for these properties.buttonShape) using class
delegation.class AppShapes(
private val base: DsShapes,
val buttonShape: ShapeAppearance
) : DsShapes by baseBox(modifier = Modifier.clip(AppTheme.shapes.buttonShape.toComposeShape()))Surface(shape = AppTheme.shapes.medium.toComposeShape()) { }<View android:background="?attr/shapeMedium" />Text("Button").cornerRadius(theme.shapes.medium.topLeft)@ObservedObject private var theme = AppThemeSwift.shared
button.layer.cornerRadius = AppThemeSwift.shared.shapes.medium.topLeftDsTypography interface containing text style tokens (
e.g., titleLarge, bodyMedium). The client app must define values for these properties.promoTitle) using
class delegation.class AppTypography(
private val base: DsTypography,
val promoTitle: FontSpec
) : DsTypography by baseText("Header", style = AppTheme.typography.titleLarge)Text("Sub", style = AppTheme.typography.bodyMedium)<TextView
android:textSize="?attr/fontSizeTitle"
android:textColor="?attr/colorOnSurface" />@ObservedObject private var theme = AppThemeSwift.shared
Text("Header").font(Font(theme.typography.titleLargeFont))titleLabel.font = AppThemeSwift.shared.typography.titleLargeFontCore Model: The library does not define any core strings. The custom Gradle plugin packages
localized string resource folders (.lproj) into the iOS bundle. The client app defines its own
resource keys (typically as an enum) and platform-specific resolvers.
Storage Location: Localized text is placed directly in standard native resource folders within your shared module:
values/strings.xml, values-es/strings.xml).src/iosMain/resources/en.lproj/Localizable.strings).Definition:
Define a localized resource enum in commonMain implementing DsStrings. In the platform source
sets, implement the platform-specific contract interfaces:
AndroidDsStrings): Maps each key to its style theme attribute ID (
R.attr.welcomeMsg) and active theme variant.IosDsStrings): Maps each key to its exact Cocoa localization dictionary key (
"welcomeMsg").// commonMain
expect enum class AppStrings : DsStrings {
WELCOME_MSG
}
// androidMain
actual enum class AppStrings : AndroidDsStrings {
WELCOME_MSG;
override fun toAttrId(): Pair<Int, String> =
Pair(R.attr.welcomeMsg, currentVariant.name)
}
// iosMain
actual enum class AppStrings : IosDsStrings {
WELCOME_MSG;
override fun toIosKey(): String = "welcomeMsg"
}Text(text = stringResource(AppStrings.APP_TITLE))Text(text = stringResource(AppStrings.WELCOME_MSG))<TextView android:text="?attr/welcomeMsg" />Text(AppStrings.appTitle.localized)titleLabel.text = AppStrings.appTitle.localizedCore Model: The library does not define any core images. The custom Gradle plugin compiles and
packages the .xcassets catalog into Assets.car inside the iOS bundle. The client app defines
its own image references/contracts and platform-specific mapping.
Storage Location: Raw vector files and asset catalogs are placed in standard platform-specific resource folders:
src/androidMain/res/drawable/.src/iosMain/resources/Assets.xcassets/.Definition:
Define an image asset enum in commonMain implementing DsImages. In the platform source sets,
implement the platform-specific contract interfaces:
AndroidDsImages): Maps each key to its style theme attribute ID (
R.attr.logo).IosDsImages): Maps each key to its Apple Asset Catalog asset name ("ic_logo").// commonMain
expect enum class AppImages : DsImages {
LOGO
}
// androidMain
actual enum class AppImages : AndroidDsImages {
LOGO;
override fun toAttrId(): Int = R.attr.logo
}
// iosMain
actual enum class AppImages : IosDsImages {
LOGO;
override fun toImageName(): String = "ic_logo"
}Compose Multiplatform (Android & iOS):
DsImage(
image = AppImages.LOGO,
contentDescription = "Logo",
modifier = Modifier.size(64.dp)
)Jetpack Compose (Android):
DsImage(image = AppImages.LOGO, contentDescription = "Logo")Android XML Layouts:
<ImageView android:src="?attr/logo" android:tint="?attr/colorPrimary" />SwiftUI (iOS):
Image(uiImage: AppImages.logo.uiImage)
.foregroundColor(theme.colors.primary.color)UIKit (iOS):
logoImageView.image = AppImages.logo.uiImage
logoImageView.tintColor = AppThemeSwift.shared.colors.primary.uiColorAdvanced Image Loading (Coil 3):
DsImages with Coil 3 using our platform-agnostic helper toCoilModel(),
or by registering a custom Coil 3 Mapper.AppTheme Composable. It resolves the current
theme variant tokens, passes them to a CompositionLocalProvider (e.g., LocalAppColors,
LocalAppShapes, LocalAppTypography), and maps them to standard MaterialTheme:
@Composable
fun AppTheme(
variant: AppThemeVariant = AppThemeSettings.currentVariant,
isDark: Boolean = AppThemeSettings.isDark,
content: @Composable () -> Unit
) {
val (appColors, colorScheme) = remember(variant, isDark) {
val resolved = variant.resolveColors(isDark)
resolved to resolved.toColorScheme(isDark)
}
val (appShapes, shapes) = remember(variant) {
val resolved = variant.resolveShapes()
resolved to resolved.toShapes()
}
val (appTypography, typography) = remember(variant) {
val resolved = variant.resolveTypography()
resolved to resolved.toTypography()
}
CompositionLocalProvider(
LocalAppColors provides appColors,
LocalAppShapes provides appShapes,
LocalAppTypography provides appTypography
) {
MaterialTheme(
colorScheme = colorScheme,
shapes = shapes,
typography = typography,
content = content
)
}
}[!NOTE] The sample app's AppThemeSpecs.kt provides a full, concrete example of how to implement colors, shapes, typography, platform-specific string/image maps, and dynamic theme switching.
Build the Android debug configurations:
./gradlew assembleDebug[!IMPORTANT] macOS & Xcode Requirement
Building and linking iOS targets requires a macOS machine with Xcode 15.0 or higher installed.
Clean and build the iOS framework package and simulator targets:
xcodebuild -project app-ios/app-ios.xcodeproj \
-scheme app-ios \
-destination "generic/platform=iOS Simulator" \
-configuration Debug \
clean buildWe welcome contributions to help improve kmp-design-system! Whether you are reporting a bug, suggesting a new design token, or submitting a Pull Request, please follow these guidelines:
main).This project is licensed under the MIT License. See the LICENSE file in the root directory for the full legal text.
A fast, lightweight, and type-safe Kotlin Multiplatform (KMP) engine to manage application themes, share design tokens, and bundle localized strings and graphics synchronously across Compose Multiplatform, Jetpack Compose, Android XML Layouts, SwiftUI, and iOS UIKit.
Instead of relying on heavy code generation, kmp-design-system translates design tokens and assets
directly into standard platform-native resource structures (such as Android XML Theme Attributes,
iOS Asset Catalogs, and iOS localized strings). This enables completely synchronous, zero-copy, and
frame-rate-safe resource rendering across all native and Compose UI stacks.
For a deep-dive conceptual explanation of the challenges of KMP resource sharing and how the Resource Contracts pattern solves them, see docs/resource_contracts_concept.md.
Below is the sample application demonstrating dynamic theme variants (Global Purple vs. Emerald Green), light/dark mode swaps, localized greeting text, and vector image rendering.
| Android | iOS |
|---|---|
![]() |
![]() |
To use this engine, your development environment and target project must satisfy the following:
8.5 or higher2.1.0 or higher (tested with 2.1.20)1.8.0 or higher (tested with 1.8.2)29, compileSdk 36
15.0 or higher (Xcode 15.0+ required for framework assembly)To use the design system library in any KMP application, configure it using Gradle's Version Catalog.
Add the design system plugin and runtime dependency to your client application's version catalog (gradle/libs.versions.toml):
[versions]
# Check the live Maven Central badge at the top for the latest release version
design-system = "1.0.0-beta08"
[libraries]
design-system = { group = "com.savantarch", name = "design-system", version.ref = "design-system" }
[plugins]
design-system = { id = "com.savantarch.designsystem", version.ref = "design-system" }In your shared UI module's build.gradle.kts (e.g. :app-shared), apply the plugin using its catalog alias and configure the designSystem extension block:
plugins {
alias(libs.plugins.design.system)
}
designSystem {
// Directory containing Apple .xcassets folder
// default: "src/iosMain/resources/Assets.xcassets"
xcassetsDir.set(layout.projectDirectory.dir("src/iosMain/resources/Assets.xcassets"))
// Directory containing raw localizations and assets
// default: "src/iosMain/resources"
resourcesDir.set(layout.projectDirectory.dir("src/iosMain/resources"))
// Optional: iOS deployment target
// default: "15.0"
minimumDeploymentTarget.set("15.0")
// Optional: Target iOS devices
// default: ["iphone", "ipad"]
targetDevices.set(listOf("iphone", "ipad"))
}Add the core design system runtime library to your shared UI module's commonMain dependencies:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.design.system)
}
}
}The :design-system module is app-agnostic. It provides token interfaces, asset contracts, and the
core DesignSystemTheme Composable. Client applications are responsible for satisfying these
contracts by defining concrete values for standard tokens (which are based on Material 3 designs),
and optionally extending them with custom tokens.
Here is how each type of design resource is modeled, extended, and provided via
staticCompositionLocalOf:
[!TIP] Use Delegation Wrappers (
AppXyz) Even if your application does not initially require custom design tokens, we recommend that you always declare custom client delegation wrapper classes (e.g.AppColors,AppShapes,AppTypographywrapping their library-levelDsColors,DsShapes,DsTypographyequivalents) and expose them on your publicAppThemeAPI.Doing so future-proofs your theme implementation: if you need to introduce new custom tokens in the future, you can add them directly to your application wrappers without altering
AppTheme's return types, preventing breaking API changes across your Kotlin and Swift codebases.
DsColors interface containing standard Material 3 color
properties (e.g., primary, background, surface). The client app must define values for these
properties.promoBrand) wrapping the
base DsColors.class AppColors(
private val base: DsColors,
val promoBrand: Long
) : DsColors by baseBox(modifier = Modifier.background(AppTheme.colors.background.toColor()))Text(text = "Hello", color = AppTheme.colors.primary.toColor())<View android:background="?attr/colorPrimary" />@ObservedObject private var theme = AppThemeSwift.shared
Text("Hello").foregroundColor(theme.colors.primary.color)view.backgroundColor = AppThemeSwift.shared.colors.background.uiColorDsShapes interface containing shape tokens (e.g.,
small, medium, large). The client app must define values for these properties.buttonShape) using class
delegation.class AppShapes(
private val base: DsShapes,
val buttonShape: ShapeAppearance
) : DsShapes by baseBox(modifier = Modifier.clip(AppTheme.shapes.buttonShape.toComposeShape()))Surface(shape = AppTheme.shapes.medium.toComposeShape()) { }<View android:background="?attr/shapeMedium" />Text("Button").cornerRadius(theme.shapes.medium.topLeft)@ObservedObject private var theme = AppThemeSwift.shared
button.layer.cornerRadius = AppThemeSwift.shared.shapes.medium.topLeftDsTypography interface containing text style tokens (
e.g., titleLarge, bodyMedium). The client app must define values for these properties.promoTitle) using
class delegation.class AppTypography(
private val base: DsTypography,
val promoTitle: FontSpec
) : DsTypography by baseText("Header", style = AppTheme.typography.titleLarge)Text("Sub", style = AppTheme.typography.bodyMedium)<TextView
android:textSize="?attr/fontSizeTitle"
android:textColor="?attr/colorOnSurface" />@ObservedObject private var theme = AppThemeSwift.shared
Text("Header").font(Font(theme.typography.titleLargeFont))titleLabel.font = AppThemeSwift.shared.typography.titleLargeFontCore Model: The library does not define any core strings. The custom Gradle plugin packages
localized string resource folders (.lproj) into the iOS bundle. The client app defines its own
resource keys (typically as an enum) and platform-specific resolvers.
Storage Location: Localized text is placed directly in standard native resource folders within your shared module:
values/strings.xml, values-es/strings.xml).src/iosMain/resources/en.lproj/Localizable.strings).Definition:
Define a localized resource enum in commonMain implementing DsStrings. In the platform source
sets, implement the platform-specific contract interfaces:
AndroidDsStrings): Maps each key to its style theme attribute ID (
R.attr.welcomeMsg) and active theme variant.IosDsStrings): Maps each key to its exact Cocoa localization dictionary key (
"welcomeMsg").// commonMain
expect enum class AppStrings : DsStrings {
WELCOME_MSG
}
// androidMain
actual enum class AppStrings : AndroidDsStrings {
WELCOME_MSG;
override fun toAttrId(): Pair<Int, String> =
Pair(R.attr.welcomeMsg, currentVariant.name)
}
// iosMain
actual enum class AppStrings : IosDsStrings {
WELCOME_MSG;
override fun toIosKey(): String = "welcomeMsg"
}Text(text = stringResource(AppStrings.APP_TITLE))Text(text = stringResource(AppStrings.WELCOME_MSG))<TextView android:text="?attr/welcomeMsg" />Text(AppStrings.appTitle.localized)titleLabel.text = AppStrings.appTitle.localizedCore Model: The library does not define any core images. The custom Gradle plugin compiles and
packages the .xcassets catalog into Assets.car inside the iOS bundle. The client app defines
its own image references/contracts and platform-specific mapping.
Storage Location: Raw vector files and asset catalogs are placed in standard platform-specific resource folders:
src/androidMain/res/drawable/.src/iosMain/resources/Assets.xcassets/.Definition:
Define an image asset enum in commonMain implementing DsImages. In the platform source sets,
implement the platform-specific contract interfaces:
AndroidDsImages): Maps each key to its style theme attribute ID (
R.attr.logo).IosDsImages): Maps each key to its Apple Asset Catalog asset name ("ic_logo").// commonMain
expect enum class AppImages : DsImages {
LOGO
}
// androidMain
actual enum class AppImages : AndroidDsImages {
LOGO;
override fun toAttrId(): Int = R.attr.logo
}
// iosMain
actual enum class AppImages : IosDsImages {
LOGO;
override fun toImageName(): String = "ic_logo"
}Compose Multiplatform (Android & iOS):
DsImage(
image = AppImages.LOGO,
contentDescription = "Logo",
modifier = Modifier.size(64.dp)
)Jetpack Compose (Android):
DsImage(image = AppImages.LOGO, contentDescription = "Logo")Android XML Layouts:
<ImageView android:src="?attr/logo" android:tint="?attr/colorPrimary" />SwiftUI (iOS):
Image(uiImage: AppImages.logo.uiImage)
.foregroundColor(theme.colors.primary.color)UIKit (iOS):
logoImageView.image = AppImages.logo.uiImage
logoImageView.tintColor = AppThemeSwift.shared.colors.primary.uiColorAdvanced Image Loading (Coil 3):
DsImages with Coil 3 using our platform-agnostic helper toCoilModel(),
or by registering a custom Coil 3 Mapper.AppTheme Composable. It resolves the current
theme variant tokens, passes them to a CompositionLocalProvider (e.g., LocalAppColors,
LocalAppShapes, LocalAppTypography), and maps them to standard MaterialTheme:
@Composable
fun AppTheme(
variant: AppThemeVariant = AppThemeSettings.currentVariant,
isDark: Boolean = AppThemeSettings.isDark,
content: @Composable () -> Unit
) {
val (appColors, colorScheme) = remember(variant, isDark) {
val resolved = variant.resolveColors(isDark)
resolved to resolved.toColorScheme(isDark)
}
val (appShapes, shapes) = remember(variant) {
val resolved = variant.resolveShapes()
resolved to resolved.toShapes()
}
val (appTypography, typography) = remember(variant) {
val resolved = variant.resolveTypography()
resolved to resolved.toTypography()
}
CompositionLocalProvider(
LocalAppColors provides appColors,
LocalAppShapes provides appShapes,
LocalAppTypography provides appTypography
) {
MaterialTheme(
colorScheme = colorScheme,
shapes = shapes,
typography = typography,
content = content
)
}
}[!NOTE] The sample app's AppThemeSpecs.kt provides a full, concrete example of how to implement colors, shapes, typography, platform-specific string/image maps, and dynamic theme switching.
Build the Android debug configurations:
./gradlew assembleDebug[!IMPORTANT] macOS & Xcode Requirement
Building and linking iOS targets requires a macOS machine with Xcode 15.0 or higher installed.
Clean and build the iOS framework package and simulator targets:
xcodebuild -project app-ios/app-ios.xcodeproj \
-scheme app-ios \
-destination "generic/platform=iOS Simulator" \
-configuration Debug \
clean buildWe welcome contributions to help improve kmp-design-system! Whether you are reporting a bug, suggesting a new design token, or submitting a Pull Request, please follow these guidelines:
main).This project is licensed under the MIT License. See the LICENSE file in the root directory for the full legal text.