
Creates text blueprints for Compose applications, enabling efficient text resource handling, rich styling, and automatic locale adaptation, while preventing stale ViewModel strings during language changes.
A Kotlin Multiplatform library for creating text blueprints in Compose applications, supporting both plain and styled text with String Resources integration.
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.
Pick this option if you use Android string resources in your project.
dependencies {
implementation("com.radusalagean:ui-text-compose-android:1.0.0")
}<resources>
<string name="greeting">Hi, %1$s!</string>
</resources>import com.radusalagean.uitextcompose.android.UIText
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.graphics.Colorclass MyViewModel {
val helloText = UIText {
res(R.string.greeting) {
arg("Radu")
}
}
}
@Composable
fun MyScreen(viewModel: MyViewModel) {
Text(text = viewModel.helloText.buildStringComposable())
}Pick this option if you use Compose Multiplatform string resources in your project.
commonMain.dependencies {
implementation("com.radusalagean:ui-text-compose-multiplatform:1.0.0")
}<resources>
<string name="greeting">Hi, %1$s!</string>
</resources>import com.radusalagean.uitextcompose.multiplatform.UIText
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.graphics.Colorclass MyViewModel {
val helloText = UIText {
res(Res.string.greeting) {
arg("Radu")
}
}
}
@Composable
fun MyScreen(viewModel: MyViewModel) {
Text(text = viewModel.helloText.buildStringComposable())
}<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>Define:
val uiText = UIText {
raw("Radu")
}Use in composable:
Text(uiText.buildStringComposable())Define:
val uiText = UIText {
res(R.string.greeting) {
arg("Radu")
}
}Use in composable:
Text(uiText.buildStringComposable())Define:
val uiText = UIText {
pluralRes(R.plurals.products, 30)
}Use in composable:
Text(uiText.buildStringComposable())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())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())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())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())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())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())UIText Compose for Android works with standard Android string resources as described in the official documentation.
It supports:
R.string.*)R.plurals.*)For Kotlin Multiplatform projects, UIText Compose works with Compose Multiplatform's string resources as described in the official documentation.
It supports:
Res.string.*)Res.plurals.*)%s (unnumbered) and %1$s, %2$s, ... (numbered) string placeholders%1$s, %2$s, ... (numbered) string placeholdersThe 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
}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 resourcesRepresents 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...
}raw("Hello, World!")res(R.string.greeting) {
// Optional arguments
arg("User")
}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)
}
}Types of args supported:
CharSequence - for example: String or AnnotatedString
UIText instancesYou 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
)
}+ 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 linksSample apps are available in the uitextcompose-android-sample and uitextcompose-multiplatform-sample modules.
Found a bug or have a suggestion? Please open an issue.
If you use this library and enjoy it, please support it by starring it on GitHub. π
Apache License 2.0, see the LICENSE file for details.
A Kotlin Multiplatform library for creating text blueprints in Compose applications, supporting both plain and styled text with String Resources integration.
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.
Pick this option if you use Android string resources in your project.
dependencies {
implementation("com.radusalagean:ui-text-compose-android:1.0.0")
}<resources>
<string name="greeting">Hi, %1$s!</string>
</resources>import com.radusalagean.uitextcompose.android.UIText
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.graphics.Colorclass MyViewModel {
val helloText = UIText {
res(R.string.greeting) {
arg("Radu")
}
}
}
@Composable
fun MyScreen(viewModel: MyViewModel) {
Text(text = viewModel.helloText.buildStringComposable())
}Pick this option if you use Compose Multiplatform string resources in your project.
commonMain.dependencies {
implementation("com.radusalagean:ui-text-compose-multiplatform:1.0.0")
}<resources>
<string name="greeting">Hi, %1$s!</string>
</resources>import com.radusalagean.uitextcompose.multiplatform.UIText
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.graphics.Colorclass MyViewModel {
val helloText = UIText {
res(Res.string.greeting) {
arg("Radu")
}
}
}
@Composable
fun MyScreen(viewModel: MyViewModel) {
Text(text = viewModel.helloText.buildStringComposable())
}<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>Define:
val uiText = UIText {
raw("Radu")
}Use in composable:
Text(uiText.buildStringComposable())Define:
val uiText = UIText {
res(R.string.greeting) {
arg("Radu")
}
}Use in composable:
Text(uiText.buildStringComposable())Define:
val uiText = UIText {
pluralRes(R.plurals.products, 30)
}Use in composable:
Text(uiText.buildStringComposable())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())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())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())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())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())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())UIText Compose for Android works with standard Android string resources as described in the official documentation.
It supports:
R.string.*)R.plurals.*)For Kotlin Multiplatform projects, UIText Compose works with Compose Multiplatform's string resources as described in the official documentation.
It supports:
Res.string.*)Res.plurals.*)%s (unnumbered) and %1$s, %2$s, ... (numbered) string placeholders%1$s, %2$s, ... (numbered) string placeholdersThe 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
}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 resourcesRepresents 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...
}raw("Hello, World!")res(R.string.greeting) {
// Optional arguments
arg("User")
}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)
}
}Types of args supported:
CharSequence - for example: String or AnnotatedString
UIText instancesYou 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
)
}+ 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 linksSample apps are available in the uitextcompose-android-sample and uitextcompose-multiplatform-sample modules.
Found a bug or have a suggestion? Please open an issue.
If you use this library and enjoy it, please support it by starring it on GitHub. π
Apache License 2.0, see the LICENSE file for details.