Locale-aware string resource blueprints in Android and KMP projects with the UIText Compose library
UIText Compose
A Kotlin Multiplatform library for creating text blueprints in Compose applications, supporting both plain and styled text with String Resources integration. (GitHub link)
Overview
UIText Compose addresses a critical challenge in Compose UI applications: handling text resources efficiently while avoiding the ViewModel antipattern with locale changes.
The library provides a solution to a common problem: ViewModels that directly expose localized strings become stale when the user changes their device language because ViewModels survive configuration changes. This leads to partially localized apps showing obsolete text.
UIText Compose solves this by enabling:
- Proper separation of text definition from rendering – Create text blueprints (containing resource IDs, not resolved strings) in your ViewModels, mappers, etc. – then pass them to composables for locale-aware rendering.
- Automatic adaptation to locale changes – UIText instances automatically update when the locale changes, ensuring your app is fully localized.
- Rich styling and formatting – Add spans, paragraph styles, and link annotations to your text while maintaining the proper architecture.
- Composition of complex text patterns – Combine text from multiple sources (raw text, string resources, plural resources) into a single text object.
This approach follows the best practice recommended by the Android team: exposing resource IDs from ViewModels rather than resolved strings, allowing the view layer to properly handle configuration changes.
Getting Started
Option 1: Android string resources
Pick this option if you use Android string resources in your project.
- Add the dependency to your module’s build.gradle.kts (make sure to use the latest version):
dependencies {
implementation("com.radusalagean:ui-text-compose-android:1.0.0")
}
- Define the string resource:
<resources>
<string name="greeting">Hi, %1$s!</string>
</resources>
- Import the necessary classes:
import com.radusalagean.uitextcompose.android.UIText
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.graphics.Color
- Create a UIText instance and use it in your Composable:
class MyViewModel {
val helloText = UIText {
res(R.string.greeting) {
arg("Radu")
}
}
}
@Composable
fun MyScreen(viewModel: MyViewModel) {
Text(text = viewModel.helloText.buildStringComposable())
}
Option 2: Compose Multiplatform string resources
Pick this option if you use Compose Multiplatform string resources in your project.
Compatible platforms:
Integration
- Add the dependency to your module’s build.gradle.kts (make sure to use the latest version):
commonMain.dependencies {
implementation("com.radusalagean:ui-text-compose-multiplatform:1.0.0")
}
- Define the string resource:
<resources>
<string name="greeting">Hi, %1$s!</string>
</resources>
- Import the necessary classes:
import com.radusalagean.uitextcompose.multiplatform.UIText
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.graphics.Color
- Create a UIText instance and use it in your Composable:
class MyViewModel {
val helloText = UIText {
res(Res.string.greeting) {
arg("Radu")
}
}
}
@Composable
fun MyScreen(viewModel: MyViewModel) {
Text(text = viewModel.helloText.buildStringComposable())
}
Examples (replace R. with Res. for multiplatform usage)
String resources used in the examples
<resources>
<string name="greeting">Hi, %1$s!</string>
<string name="shopping_cart_status">You have %1$s in your %2$s.</string>
<string name="shopping_cart_status_insert_shopping_cart">shopping cart</string>
<string name="proceed_to_checkout">Proceed to checkout</string>
<string name="legal_footer_example">This is how you can create a %1$s and %2$s text with links.</string>
<string name="legal_footer_example_insert_terms_of_service">Terms of Service</string>
<string name="legal_footer_example_insert_privacy_policy">Privacy Policy</string>
<plurals name="products">
<item quantity="one">%1$s product</item>
<item quantity="other">%1$s products</item>
</plurals>
</resources>
Raw text
Define:
val uiText = UIText {
raw("Radu")
}
Use in composable:
Text(uiText.buildStringComposable())

String resource
Define:
val uiText = UIText {
res(R.string.greeting) {
arg("Radu")
}
}
Use in composable:
Text(uiText.buildStringComposable())

Plural string resource
Define:
val uiText = UIText {
pluralRes(R.plurals.products, 30)
}
Use in composable:
Text(uiText.buildStringComposable())

String resource – annotated
Define:
val uiText = UIText {
res(R.string.shopping_cart_status) {
arg(
UIText {
pluralRes(R.plurals.products, 30)
}
)
arg(
UIText {
res(R.string.shopping_cart_status_insert_shopping_cart) {
+SpanStyle(color = Color.Red)
}
}
)
}
}
Use in composable:
Text(uiText.buildAnnotatedStringComposable())

Plural string resource – annotated
Define:
val uiText = UIText {
pluralRes(R.plurals.products, 30) {
arg(30.toString()) {
+SpanStyle(color = CustomGreen)
}
+SpanStyle(fontWeight = FontWeight.Bold)
}
}
Use in composable:
Text(uiText.buildAnnotatedStringComposable())

Compound – example 1
Define:
val uiText = UIText {
res(R.string.greeting) {
arg("Radu")
}
raw(" ")
res(R.string.shopping_cart_status) {
arg(
UIText {
pluralRes(R.plurals.products, 30) {
arg(30.toString()) {
+SpanStyle(color = CustomGreen)
}
+SpanStyle(fontWeight = FontWeight.Bold)
}
}
)
arg(
UIText {
res(R.string.shopping_cart_status_insert_shopping_cart) {
+SpanStyle(color = Color.Red)
}
}
)
}
}
Use in composable:
Text(uiText.buildAnnotatedStringComposable())

Compound – example 2
Define:
val uiText = UIText {
res(R.string.greeting) {
arg("Radu")
}
raw(" ")
res(R.string.shopping_cart_status) {
arg(
UIText {
pluralRes(R.plurals.products, 30) {
arg(30.toString()) {
+SpanStyle(color = CustomGreen)
}
+SpanStyle(fontWeight = FontWeight.Bold)
}
}
)
arg(
UIText {
res(R.string.shopping_cart_status_insert_shopping_cart)
}
) {
+SpanStyle(color = Color.Red)
}
}
raw(" ")
res(R.string.proceed_to_checkout) {
+LinkAnnotation.Url(
url = "https://example.com",
styles = TextLinkStyles(
style = SpanStyle(
color = Color.Blue,
textDecoration = TextDecoration.Underline
)
)
)
}
}
Use in composable:
Text(uiText.buildAnnotatedStringComposable())

Compound – example 3
Define:
val uiText = UIText {
res(R.string.greeting) {
arg("Radu")
}
res(R.string.shopping_cart_status) {
+ParagraphStyle()
arg(
UIText {
pluralRes(R.plurals.products, 30) {
+SpanStyle(fontWeight = FontWeight.Bold)
arg(30.toString()) {
+SpanStyle(color = CustomGreen)
}
}
}
)
arg(
UIText {
res(R.string.shopping_cart_status_insert_shopping_cart)
}
) {
+SpanStyle(color = Color.Red)
}
}
res(R.string.proceed_to_checkout) {
+LinkAnnotation.Url(
url = "https://example.com",
styles = TextLinkStyles(
style = SpanStyle(
color = Color.Blue,
textDecoration = TextDecoration.Underline
)
)
)
}
}
Use in composable:
Text(uiText.buildAnnotatedStringComposable())

Terms of Service & Privacy Policy
Define:
val uiText = UIText {
val linkStyle = TextLinkStyles(
SpanStyle(color = Color.Blue, textDecoration = TextDecoration.Underline)
)
res(R.string.legal_footer_example) {
arg(
UIText {
res(R.string.legal_footer_example_insert_terms_of_service)
}
) {
+LinkAnnotation.Url(
url = "https://radusalagean.com/example-terms-of-service/",
styles = linkStyle
)
}
arg(
UIText {
res(R.string.legal_footer_example_insert_privacy_policy)
}
) {
+LinkAnnotation.Url(
url = "https://radusalagean.com/example-privacy-policy/",
styles = linkStyle
)
}
}
}
Use in composable:
Text(uiText.buildAnnotatedStringComposable())

Compatibility of string resources
Android Resources
UIText Compose for Android works with standard Android string resources as described in the official documentation.
It supports:
- String resource ids (
R.string.*
) - Plural resource ids (
R.plurals.*
)
Multiplatform Resources
For Kotlin Multiplatform projects, UIText Compose works with Compose Multiplatform’s string resources as described in the official documentation.
It supports:
- Type-safe string resources (
Res.string.*
) - Type-safe plural resources (
Res.plurals.*
)
Supported placeholders for string resources
- ui-text-compose-android:
%s
(unnumbered) and%1$s
,%2$s
, … (numbered) string placeholders - ui-text-compose-multiplatform:
%1$s
,%2$s
, … (numbered) string placeholders
API Description
Core Concepts
UITextBase
The base interface that defines methods for getting plain text or annotated text in your composables:
interface UITextBase {
@Composable
fun buildStringComposable(): String
@Composable
fun buildAnnotatedStringComposable(): AnnotatedString
}
UIText
The main class that implements UITextBase
. There are two implementations:
com.radusalagean.uitextcompose.android.UIText
– For Android string resourcescom.radusalagean.uitextcompose.multiplatform.UIText
– For Compose Multiplatform string resources
Represents the blueprint, which is used by your composables to build the strings.
Instances are created using the DSL builder:
val text = UIText {
raw("Hello")
// More builder methods here...
}
Builder Methods
Raw Text
raw("Hello, World!")
String Resources (replace R. with Res. for multiplatform usage)
res(R.string.greeting) {
// Optional arguments
arg("User")
}
Plural Resources (replace R. with Res. for multiplatform usage)
pluralRes(R.plurals.items_count, 5)
…or, if you need more flexibility for your arguments
pluralRes(R.plurals.items_count, 5) {
arg("5") {
+SpanStyle(color = Color.Red)
}
}
Arguments
Types of args supported:
CharSequence
– for example:String
orAnnotatedString
- other
UIText
instances
Styling
You can apply styling to arguments and the base string resource:
res(R.string.greeting) {
arg("Radu") {
// Apply a span style to the argument
+SpanStyle(
color = Color.Blue,
fontWeight = FontWeight.Bold
)
}
// Apply a span style to the base string resource
+SpanStyle(
color = Color.Red
)
}
⚠️ Do not forget the +
operator
Types of styling supported:
SpanStyle
– For character-level styling (color, font weight, etc.)ParagraphStyle
– For paragraph-level styling (alignment, indentation, etc.)LinkAnnotation
– For adding clickable links
Sample Apps
Sample apps are available in the uitextcompose-android-sample
and uitextcompose-multiplatform-sample
modules.
Support 🌟
If you use this library and enjoy it, please support it by starring it on GitHub. 🌟