In today’s world, many applications require users to accept their terms, conditions, privacy policy, distance sales contract and more. These documents often contain a clickable text in a sentence and it plays a role in navigates to agreement screen, display a popup or bottom sheet when you click it and generally have different color and font weight to represent clickable texts. In this article I’ll try to focus on managing clicks on localized text in your Jetpack Compose applications.

What Jetpack Compose Offers
Jetpack Compose provides a builder class for annotating text, which makes easier the process of styling and appending text. If your app supports only one language, you can easily make clickable texts directly within the annotated strings.
Let’s say we have the string of “By clicking the continue, you agree to our Terms and Privacy Policy.” You could simply annotate “Terms” and “Privacy Policy” together for clickability if you provide just one language. But things get a bit tricky when dealing with other languages. The word order changes, making it hard to use the same structure. You can start with “prefix_terms,” but it’s not always that straightforward.
<resources> | |
<string name="prefix_terms">By clicking the continue, you agree to our\u0020</string> | |
<string name="terms">Terms</string> | |
<string name="privacy_policy">Privacy Policy</string> | |
<string name="and">\u0020and\u0020</string> | |
<string name="agreement_text">By clicking the continue, you agree to our Terms and Privacy Policy</string> | |
</resources> | |
<resources> | |
<string name="prefix_terms">Devam ederek\u0020</string> | |
<string name="terms">Şartlarımızı</string> | |
<string name="privacy_policy">Gizlilik Politikamızı</string> | |
<string name="and">\u0020ve\u0020</string> | |
<string name="agreement_text">Devam ederek Şartlarımızı ve Gizlilik Politikamızı kabul etmiş olursunuz.</string> | |
</resources> |
Like I said above we can use them directly if an an application has one language. Let say default language is en-US.
val annotatedString = buildAnnotatedString { | |
append(stringResource(id = R.string.prefix_terms)) | |
pushStringAnnotation(tag = stringResource(id = R.string.terms), annotation = "https://termify.io/terms-and-conditions-generator") | |
withStyle( | |
style = SpanStyle( | |
color = MaterialTheme.colorScheme.primary, | |
textDecoration = TextDecoration.Underline | |
) | |
) { | |
append(stringResource(id = R.string.terms)) | |
} | |
append(stringResource(id = R.string.and)) | |
pushStringAnnotation(tag = stringResource(id = R.string.privacy_policy), annotation = "https://termify.io/privacy-policy-generator") | |
withStyle( | |
style = SpanStyle( | |
color = MaterialTheme.colorScheme.primary, | |
textDecoration = TextDecoration.Underline | |
) | |
) { | |
append(stringResource(id = R.string.privacy_policy)) | |
} | |
pop() | |
} | |
ClickableText( | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(16.dp), | |
text = annotatedString, | |
style = MaterialTheme.typography.bodyMedium, | |
onClick = { offset -> | |
annotatedString.getStringAnnotations(tag = "policy", start = offset, end = offset) | |
.firstOrNull()?.let { | |
Timber.d("policy URL: ${it.item}") | |
} | |
annotatedString.getStringAnnotations(tag = "terms", start = offset, end = offset) | |
.firstOrNull()?.let { | |
Timber.d("terms URL: ${it.item}") | |
} | |
}) |

The Problem About Internationalized Texts
Directly embedding text can cause to incomplete or incorrect sentences due to differences in word order and sentence structure across languages. This causes a problem below, resulting in an incomplete Turkish sentence. Therefore, we need a generic way to handle it.

Firstly, we need tag, annotation and style to use buildAnnotatedString in our case. So, a simple data class to hold them is enough to use in ClickableLocalizedText function.
/** | |
* Data class representing a link annotation. | |
* | |
* @property tag The tag that will be used to identify this annotation in the text. | |
* @property annotation The actual annotation(url) that will be attached to the tag in the text. | |
* @property style The style that will be applied to the text span that matches the tag. | |
*/ | |
@Immutable | |
data class StringAnnotation( | |
val tag: String, | |
val annotation: String, | |
val style: SpanStyle | |
) |
Secondly we need a function to proceed each item inside LinkAnnotation. We can implement this in 3 steps:
- Build an annotated string by given text, annotation and style.
- Create a ClickableText composable using the annotated string from step 1, and apply the annotation and style.
- When a section of the text is clicked, the function finds the annotation that corresponds to that section and calls the onClick callback using the item of the clicked annotation.
Job Offers
/** | |
* Composable function that creates a text with clickable annotations. | |
* | |
* @param text The text that will be displayed. It should contain tags that match the tags in the annotations list. | |
* @param textStyle The style that will be applied to the text. If not provided, the default style is `MaterialTheme.typography.titleSmall`. | |
* @param stringsAnnotations A list of `LinkAnnotation` objects. Each `LinkAnnotation` contains a tag, an annotation, and a style. The tag is a substring of the text that will be annotated. The annotation is the actual annotation that will be attached to the tag in the text. The style is the style that will be applied to the text span that matches the tag. | |
* @param onClick A function that will be called when a text span with an annotation is clicked. The function takes the annotation as a parameter. | |
*/ | |
@Composable | |
fun ClickableLocalizedText( | |
text: String, | |
textStyle: TextStyle = MaterialTheme.typography.titleSmall, | |
stringsAnnotations: List<StringAnnotation>, | |
onClick: (String) -> Unit | |
) { | |
// Build an annotated string from the text and the annotations. | |
val annotatedString = | |
buildAnnotatedString { | |
append(text) | |
stringsAnnotations.forEach { stringAnnotation -> | |
val startIndex = text.indexOf(stringAnnotation.tag) | |
val endIndex = startIndex + stringAnnotation.tag.length | |
addStyle( | |
style = stringAnnotation.style, | |
start = startIndex, | |
end = endIndex | |
) | |
addStringAnnotation( | |
tag = stringAnnotation.tag, | |
annotation = stringAnnotation.annotation, | |
start = startIndex, | |
end = endIndex | |
) | |
} | |
} | |
ClickableText( | |
modifier = Modifier | |
.padding(16.dp) | |
.fillMaxWidth(), | |
text = annotatedString, | |
style = textStyle, | |
onClick = { position -> | |
stringsAnnotations.forEach { annotation -> | |
annotatedString | |
.getStringAnnotations(annotation.tag, position, position) | |
.firstOrNull()?.let { stringAnnotation -> | |
onClick.invoke(stringAnnotation.item) | |
return@forEach | |
} | |
} | |
} | |
) | |
} |
How Can We Use It?
Now, let’s see how to apply this article in practice. Below is the code showing the implementation of the ClickableLocalizedText
function. It allows us to create clickable text elements with given localization and styling in Jetpack Compose applications.
val annotations = listOf( | |
StringAnnotation( | |
tag = stringResource(id = R.string.terms), | |
annotation = "https://termify.io/terms-and-conditions-generator", | |
style = SpanStyle( | |
color = MaterialTheme.colorScheme.primary, | |
fontSize = MaterialTheme.typography.bodyMedium.fontSize, | |
fontStyle = MaterialTheme.typography.bodyMedium.fontStyle, | |
textDecoration = TextDecoration.Underline, | |
) | |
), | |
StringAnnotation( | |
tag = stringResource(id = R.string.privacy_policy), | |
annotation = "https://termify.io/privacy-policy-generator", | |
style = SpanStyle( | |
color = MaterialTheme.colorScheme.primary, | |
fontSize = MaterialTheme.typography.bodyMedium.fontSize, | |
fontStyle = MaterialTheme.typography.bodyMedium.fontStyle, | |
textDecoration = TextDecoration.Underline, | |
) | |
) | |
) | |
ClickableLocalizedText( | |
text = stringResource(id = R.string.agreement_text), | |
stringsAnnotations = annotations, | |
onClick = { url -> | |
// ... | |
} | |
) |
Now we are able to use clickable text for localized strings and get the annotation.

Conclusion
Handling clickable localized text in Jetpack Compose give out some challenges, but by applying dynamic text construction and styling strategies, developers can offer smooth user experience across languages. Using these methods shows that you care about making your apps accessible to everyone, which helps them reach more people around the world.
Resource
This article is previously published on proandroiddev.com