Blog Infos
Author
Published
Topics
, , , ,
Published
Photo by camilo jimenez on Unsplash

With Compose Multiplatform 1.6, Jetbrains finally provides an official solution to declare string resources for every supported platform in Kotlin Multiplatform projects. As described in the official blog post, you’ll be able to create composeResources, a new resource set, located at the same level as the commonMain source set. This resource set supports images, fonts, strings or any raw files that you want to display or use in your project.

In this article, we’ll focus on string resources. Before Compose Multiplatform, you probably used moko-resources or lyricist. These two libraries are good and do the job pretty well. But what we needed was an official solution to handle this key feature in a frontend application, evolving with Kotlin versions and tooling around the language. It is now a reality since it landed in Compose Multiplatform as a new feature.

Configuring your KMP module

Before starting to try the new resources API, don’t forget to update your Compose Multiplatform Gradle plugin to be able to test these new features.

plugins {
   id("org.jetbrains.compose").version("1.6.0")
}

When it is done, the plugin will generate a Kotlin class named Res, inside your build folder and add the target build folder as an additional source set in your module. With this new Gradle plugin, you’ll be able to add a new Gradle dependency, compose.components.resources. Declare it in commonMain source set in your build.gradle.kts file and you’ll be able to use string resources in your codebase.

kotlin {
   sourceSets {
       val commonMain by getting {
           dependencies {
               implementation(compose.components.resources)
           }
       }
   }
}

Now, if you are an Android developer, you’ll feel at home. Declaring a string resource is an XML file where you can add your string item or array. This strings.xml file is saved in a values folder (default language) under composeResources but you can add a language code (ISO 639–1) and a regional code (ISO 3166–1 alpha-2) to switch automatically at the runtime to the correct translation. e.g. values-fr contains all translations for the French language and values-fr-rFR only for the France region.

How to use string resources?

Here is an example of a string item you can declare in a strings.xml file.

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <string name="key1">My value</string>
</resources>

This string key1 item is accessible by Res.string.key1 and returns a StringResource instance class. This is the first big difference compared to the native way in Android development.

If you are an Android developer, you know that R.string.key1 is an identifier that needs to be given to the Android SDK to get the correct translation from the resource files.

With Compose Multiplatform, the string resource location is described in StringResource and is read by string rendering functions to get the correct translation. The Android SDK is no more used by the UI framework.

// string rendering Compose functions
fun stringResource(resource: StringResource): String
fun stringResource(resource: StringResource, vararg formatArgs: Any): String
fun stringArrayResource(resource: StringResource): List<String>

// string rendering no Compose functions
suspend fun getString(resource: StringResource): String
suspend fun getString(resource: StringResource, vararg formatArgs: Any): String
suspend fun getStringArray(resource: StringResource): List<String>

Probably intentionally, string rendering functions are similar to the official ones in Jetpack Compose for Android. The only difference is essentially on the import side:

import org.jetbrains.compose.resources.stringResource

Text(text = stringResource(Res.string.key1))

Information: string rendering functions dedicated to non-Compose code doesn’t need anymore Android context. This allows you to use these functions anywhere in a Kotlin Multiplatform module without actual/expect mechanism to hide the Android context.

String templates

If your strings are correctly configured, you can inject two kinds of arguments, string or decimal. You just need to follow this specification: %{order}${d or s}. “order” is the argument order in your string, “d” means decimal et “s” means string. Here are some examples:

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <string name="key1">My value</string>
   <string name="key2">%1$s value</string>
   <string name="key3">%1$s and %2$d values</string>
</resources>
  • key1 is a classic string.
  • key2 is a string template where you can insert a string.
  • key3 is another string template but with two arguments. The first one is for a string, the second one is for a decimal.

The usage is pretty simple, you can use string rendering functions with arguments parameter to inject your data at runtime.

import org.jetbrains.compose.resources.stringResource

Text(text = stringResource(Res.string.key3, “Text”, 0))

Warning: In Compose Multiplatform, the string template implementation is very basic and doesn’t have any check. More information in the last section about friction points.

Current limitations

Information: These limitations will probably be fixed in the future. They are true with Compose Multiplatform 1.6.x but the more Jetbrains will work on resource management, the better will be the support of string resources.

Plurals

In the Android documentation, quantity strings are declared with a plurals item and inside zero, one, two, few, many or other. According to the language, you can use some of them to handle plurals for your strings.

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

If you try to declare a plural in the compose resource set, you won’t have any error but the string won’t be accessible from the Res class either. As a workaround, you need to implement by yourself the choice between singular or plural which can become increasingly difficult depending on the countries you support.

Just declare a string with a suffix or prefix for the quantity and in the Kotlin codebase, choose the best translation according to your context.

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <string name="key_plural_one">%1$d other</string>
   <string name="key_plural_many">%1$d others</string>
</resources>

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Sharing code across platforms is a wonderful superpower. But sometimes, sharing 100% of your codebase isn’t the goal. Maybe you’re migrating existing apps to multiplatform, maybe you have platform-specific libraries or APIs you want to…
Watch Video

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform Developer

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform ...

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform Deve ...

Jobs

Arguments limitations

When you are using an argument inside a string, you have to declare the argument order after the “%” character and if it is a string or a decimal after the “$” character. But when you check how these arguments are interpreted by Compose Multiplatform, you can notice something strange:

private val SimpleStringFormatRegex = Regex("""%(\d)$[ds]""")

@OptIn(ExperimentalResourceApi::class)
private suspend fun loadString(
   resource: StringResource,
   args: List<String>,
   resourceReader: ResourceReader,
   environment: ResourceEnvironment
): String {
   val str = loadString(resource, resourceReader, environment)
   return SimpleStringFormatRegex.replace(str) { matchResult ->
       args[matchResult.groupValues[1].toInt() - 1]
   }
}

No matter the order or if it is a string or a decimal, it’ll simply iterate over all arguments and replace argument tags by the value of the current argument.

Yes, this first implementation is a bit naive and will probably be enhanced in future versions.

Multimodule support

You can’t declare a compose resource set in a Gradle module and use these resources outside this module but the reason is pretty simple. When you build your project, the Compose Multiplatform plugin generates a Res object class with a string object class and every string resource is a Kotlin extension on this last object.

Even if string is public, Res is internal and therefore can’t be accessed outside the module.

package project.module.generated.resources

internal object Res {
 public object string
}

@ExperimentalResourceApi
internal val Res.string.key1: StringResource
  get() = String0.key1

This limitation is a real constraint, since it requires you to create several compose resource sets in your modules, potentially duplicating resources, or to create a module dedicated to resources and expose your resources through your own object resource, which won’t be private.

object Resource {
   object string
}


@OptIn(ExperimentalResourceApi::class)
val Resource.string.key1: StringResource
   get() = Res.string.key1

It requires you to rewrite every generated Kotlin extension but it works. Note that the multi module support is already in the roadmap followed by Jetbrains and will be fixed in the future.

Android optimizations

You probably know but when you use an Android resource folder and package your application with aab, the Google Play Store is smart enough to optimize your application based on the device configuration (cpu architecture, locales, etc.) when your user downloads it.

The compose resource set isn’t an Android resource folder and won’t be optimized by the Google Play Store. So you have to accept the fact that your final artifact will be bit bigger.

Conclusion

Kotlin Multiplatform is an incredible technology and Compose Multiplatform is very interesting. Jetbrains will need time to clarify the future direction of this technology but is working hard to remedy certain shortcomings.

If you already develop a mobile application with Compose Multiplatform, I wouldn’t recommend basing your resources on this new feature yet. There are too many friction points for an application available in several countries and with stakes in reducing the size of the final archive.

But track new versions Compose Multiplatform to know when these friction points will be resolved. This official solution has the potential to become the new standard in Kotlin Multiplatform projects!

Thanks for reading this blog post. If you like it, please clap this article and follow me on MastodonXGitHub and Medium to be notified about my next blog posts!

Thanks to my reviewer David Ta for this article.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu