How to Handle Resources in Kotlin Multiplatform

Photo of Krzysztof Kansy

Krzysztof Kansy

Updated Mar 7, 2023 • 9 min read
kotlin_multiplatform_resources

When we work with a user-facing application we would like to use resources in our application like colors, texts, images, etc.

Currently, Kotlin Multiplatform does not provide a resource management solution as it is in the case of native Android applications. Because of it, we want to provide you with a guide with recommendations on how to achieve a good, scalable solution for app resources.

In this article, I’ll try to demonstrate some of the solutions we’ve discovered for some key Kotlin multiplatform resources.

Configuration for Android

In most cases, the second solution for Android wouldn’t work without the line below. Place this block of code in build.gradle.kts in the module where you’re planning to keep resources.

android {
    sourceSets["main"].apply {
        res.srcDirs("src/androidMain/res", "src/commonMain/resources")
    }
}

Colors

The Material library offers the color schemes lightColors and darkColors. You should create your own data class for colors only if you have a custom palette that doesn’t follow those from the Material library. Here’s an example of how you can use custom colors in your app.

@Immutable
data class Colors(
    val primary: Color,
    val onPrimary: Color
)

private val LightColors = Colors(
    primary = Color(0xFF3D7FF9),
    onPrimary = Color(0xFFFFFFFF)
)

@Composable
fun ApplicationTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colors = MaterialTheme.colors.copy(
            primary = LightColors.primary,
            onPrimary = LightColors.onPrimary
        ),
        content = content
    )
}

Dimension

The approach for the dimension resource is similar to the solution above for colors. Look at the example below.

@Immutable
data class Dimens(val margin: Dp)

val smallDimens = Dimens(margin = 10.dp)

val mediumDimens = Dimens(margin = 12.dp)

val largeDimens = Dimens(margin = 14.dp)

val LocalDimens = staticCompositionLocalOf { smallDimens }

@Composable
fun ApplicationTheme(windowSize: WindowSize, content: @Composable () -> Unit) {
    val dimens = when (windowSize) {
        WindowSize.COMPACT -> smallDimens
        WindowSize.MEDIUM -> mediumDimens
        WindowSize.EXPANDED -> largeDimens
    }
    CompositionLocalProvider(LocalDimens provides dimens) {
        content()
    }
}

@Composable
fun ApplicationScreen() {
    ApplicationTheme(WindowSize.COMPACT) {
        Column(Modifier.padding(LocalDimens.current.margin)) {

        }
    }
}

Fonts

You can read fonts from the file by using the fontResources method. You’ll need to create FontFamily and assign it to defaultFontFamily. Then, you just need to provide your custom font to the theme.

font_location

Example of Fonts usage

The code below shows a simple implementation for using custom fonts.

@Composable
private fun CustomTypography() = Typography(
    defaultFontFamily = FontFamily(
        fontResources("aeonik_regular.otf", FontWeight.Normal, FontStyle.Normal),
        fontResources("aeonik_medium.otf", FontWeight.W500, FontStyle.Normal),
        fontResources("aeonik_bold.otf", FontWeight.Bold, FontStyle.Normal)
    )
)

@Composable
fun ApplicationTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        typography = CustomTypography(),
        content = content
    )
}

FontResources Implementation

Every platform has a different mechanism to retrieve fonts from its files. Thanks to the expect / actual feature, you can use specific implementations.

commonMain/kotlin/FontResources

import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight

@Composable
expect fun fontResources(
    font: String,
    weight: FontWeight,
    style: FontStyle
): Font

androidMain/kotlin/FontResources

@Composable
actual fun fontResources(
    font: String,
    weight: FontWeight,
    style: FontStyle
): Font {
    val context = LocalContext.current
    val name = font.substringBefore(".")
    val fontRes = context.resources.getIdentifier(name, "font", context.packageName)
    return Font(fontRes, weight, style)
}

desktopMain/kotlin/FontResources

import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.platform.Font

actual fun fontResources(
    font: String,
    weight: FontWeight,
    style: FontStyle
): Font = Font("font/$font", weight, style)

Strings

There’s an issue of how to handle translations when it comes to Strings. We found two ways in handling strings translations: with and without automation.

First solution: Without automation

This solution is quite simple but it requires implementing everything manually.

@Immutable
data class StringResources(
  val appBackground: String = ""
)

fun stringResourcesEn() = StringResources(
  appBackground = "App background"
)

fun stringResourcesDe() = StringResources(
  appBackground ="App hintergrund" 
)

val LocalStringResources = staticCompositionLocalOf { stringResourcesEn() }

@Composable
fun ApplicationTheme(content: @Composable () -> Unit) {
    val stringResources = when (Locale.current.language) {
        "DE" -> stringResourcesDe()
        else -> stringResourcesEn()
    }
    CompositionLocalProvider(LocalStringResources provides stringResources) {
        content()
    }
}

@Composable
fun ApplicationScreen() {
    ApplicationTheme {
        Text(text = LocalStringResources.current.app_background)
    }
}

Second solution: With automation

The second way will require you to use the tool that defines strings in traditional .xml. Then, by using Gradle, this can automatically generate all required classes with useful extensions.

  • The first step is to create .xml files with strings and put them in the appropriate directories for the language (e.g. values-de, values-en).

resources/values/strings_en.xml

<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<string name="app_background">App background</string>
</resources>

resources/values-de/strings_de.xml

<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<string name="app_background">App hintergrund</string>
</resources>

resources

Strings location

  • The next step is to run the resourceGeneratorTask.

example_of_the_generated_file
The example of the generated file

  • Lastly, we need to provide strings down the composition tree through CompositionLocalProvider.
val LocalResources = staticCompositionLocalOf { ResourcesImpl("EN") }

@Composable
fun ApplicationTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalResources provides ResourcesImpl(Locale.current.language)) {
        content()
    }
}

@Composable
fun ApplicationScreen() {
    ApplicationTheme {
        Text(text = getString().app_background)
    }
}

Drawables

Similarly, the two solutions when dealing with drawables involve one with automation and the other without.

First solution: Without automation

The first approach is to duplicate the same drawable for each platform. Then, you need to implement the PainterRes object for each platform by using a specific way of reading the drawable. The code below is an example of how you can implement the PainterRes object for desktop and Android platforms. Look carefully as you’ll notice that we’re referencing to a drawable by path on the desktop but by resource id on Android.

PainterRes implementation

commonMain/Kotlin/PainterRes.kt

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter

expect object PainterRes {
    @Composable
    fun loginBackground(): Painter
}

desktopMain/kotlin/PainterRes.kt

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource

actual object PainterRes {
    @Composable
    actual fun loginBackground(): Painter = painterResource("images/login_background.png")
}

androidMain/kotlin/PainterRes.kt

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource

actual object PainterRes {
    @Composable
    actual fun loginBackground(): Painter = painterResource(R.drawable.login_background)
}

Here’s an example when using the solution above.

@Composable
fun ApplicationScreen() {
    ApplicationTheme {
        Image(painter = PainterRes.loginBackground())
    }
}

Now, imagine that you have 100 drawables in your app. First, you must duplicate every drawable for each platform. Then, you’ll need to implement PainterRes. This approach requires a lot of manual work.

Second solution: With automation

The other way uses a well-known code generation mechanism. A bit of a different method using expect / actual will help you simplify this solution.

  1. Put all drawables under drawable/.

Put_all_drawable_under_drawable

2. Then, run the resourceGeneratorTask.

example_of_generated DrawableResources

3. Finally, you need to provide a drawable down the composition tree. By using imageResources, you’ll be able to get the drawable.

val LocalResources = staticCompositionLocalOf { ResourcesImpl(“EN”) }

@Composable
fun ApplicationTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalResources provides ResourcesImpl(Locale.current.language)) {
        content()
    }
}

@Composable
fun ApplicationScreen() {
    ApplicationTheme {
        Image(painter = imageResources(image = getDrawbles().red_circle_exclamation_mark))
    }
}

Most of the work will be done by the Gradle task. You just need to provide the drawable down the tree.

ImageResources implementation

commonMain/kotlin/imageResources

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter

@Composable
expect fun imageResources(image: String): Painter

androidMain/kotlin/imageResources

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource

@Composable
actual fun imageResources(image: String): Painter {
    val context = LocalContext.current
    val name = image.substringBefore(“.”)
    val drawable = context.resources.getIdentifier(name, “drawable”, context.packageName)
    return painterResource(drawable)
}

desktopMain/kotlin/imageResources

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource

@Composable
actual fun imageResources(image: String): Painter = painterResource(“drawable/$image”)

Conclusion

As you can see, because it’s not particularly obvious how to handle resources in Kotlin Multiplatform, we had to come up with some creative workarounds. Admittedly, resourceGeneratorTask plays a vital role as it does most of the work for us. When these things happen, it’s important to think of solutions that are as simple as possible to implement.

Photo of Krzysztof Kansy

More posts by this author

Krzysztof Kansy

Former Android Developer at Netguru
Kotlin Multiplatform Guidelines  Architectural aspects of multiplatform projects developed in Kotlin Read now!

We're Netguru!

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency
Let's talk business!

Trusted by: