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 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.
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.
Example of Fonts usage
The code below shows a simple implementation for using custom fonts.
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).
The next step is to run the resourceGeneratorTask.
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.
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.
Put all drawables under drawable/.
2. Then, run the resourceGeneratorTask.
3. Finally, you need to provide a drawable down the composition tree. By using imageResources, you’ll be able to get the drawable.
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.
Android
Kotlin-Multiplatform
More posts by this author