Blog Infos
Author
Published
Topics
, , , ,
Published

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.

Terms and Privacy Policy dialog from Facebook app
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>
view raw strings.xml hosted with ❤ by GitHub

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 result when we use with en-US
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.

Incompleted sentence in Turkish

 

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:

  1. Build an annotated string by given text, annotation and style.
  2. Create a ClickableText composable using the annotated string from step 1, and apply the annotation and style.
  3. 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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

/**
* 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.

Demonstration
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

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

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu