Handling Multiple Links in Text Using AnnotatedString in Jetpack Compose

Sushil Kafle
4 min readJun 27, 2024

--

Jetpack Compose has made styling text incredibly easy. Whether applying different colors, sizes, or styles to certain parts of the text, it’s now very straightforward with Jetpack Compose. Today, we’ll explore how to use AnnotatedString from Jetpack Compose to show links in text and handle link clicks. Let's dive in!

Data Classes for Links

To hold the sentence or paragraph that contains the links, let’s create a LinkData data class.

data class LinkData(
val fullText: String,
val linksList: List<Link>
)

The Link class holds information about the links present in the full text, which could be a single sentence or a paragraph.

data class Link(
val linkText: String,
val linkInfo: String? = null,
val style: SpanStyle,
val onClick: (String) -> Unit
)

Building the Annotated String

Now we are ready to build our AnnotatedString that holds the text and links within it. Since we can specify each link's style and click handler separately, it will be flexible.

val linkData = LinkData(
fullText = "Some sample text with a link. Click here to visit profile screen.",
linksList = listOf(
Link(
linkText = "link",
linkInfo = "https://example.com/",
style = SpanStyle(color = Color.Blue),
onClick = { }
),
Link(
linkText = "Click here",
linkInfo = "",
style = SpanStyle(color = Color.Blue),
onClick = { }
)
)
)

val annotatedString = buildAnnotatedString {
append(linkData.fullText)
linkData.linksList.forEach { link ->
var startIndex = linkData.fullText.indexOf(link.linkText)
while (startIndex >= 0) {
val endIndex = startIndex + link.linkText.length
addStyle(
style = link.style,
start = startIndex,
end = endIndex
)
addStringAnnotation(
tag = link.linkText,
annotation = link.linkInfo ?: "",
start = startIndex,
end = endIndex
)
startIndex = linkData.fullText.indexOf(link.linkText, endIndex)
}
}
}

Displaying Clickable Text

Now we can use this annotated string with the ClickableText composable provided by the Compose framework.

ClickableText(
text = annotatedString,
onClick = { position ->
linkData.linksList.forEach { link ->
annotatedString
.getStringAnnotations(link.linkText, position, position)
.firstOrNull()?.let {
link.onClick.invoke(it.item)
return@forEach
}
}
}
)

Complete Example

To give you a complete picture, here’s how you can use LinkData and Link within a reusable composable function:

@Composable
fun TextWithLinks(
modifier: Modifier = Modifier,
linkData: LinkData,
textStyle: TextStyle = MaterialTheme.typography.bodyLarge,
) {
val annotatedString = buildAnnotatedString {
append(linkData.fullText)
linkData.linksList.forEach { link ->
var startIndex = linkData.fullText.indexOf(link.linkText)
while (startIndex >= 0) {
val endIndex = startIndex + link.linkText.length
addStyle(
style = link.style,
start = startIndex,
end = endIndex
)
addStringAnnotation(
tag = link.linkText,
annotation = link.linkInfo ?: "",
start = startIndex,
end = endIndex
)
startIndex = linkData.fullText.indexOf(link.linkText, endIndex)
}
}
}

ClickableText(
modifier = modifier
.padding(16.dp)
.fillMaxWidth(),
text = annotatedString,
style = textStyle,
onClick = { position ->
linkData.linksList.forEach { link ->
annotatedString
.getStringAnnotations(link.linkText, position, position)
.firstOrNull()?.let {
link.onClick.invoke(it.item)
return@forEach
}
}
}
)
}

/**
* Data class representing a link with its full text and a list of links.
*
* @property fullText The full text containing the link.
* @property linksList A list of [Link] objects representing the links found in the full text.
*/
@Immutable
data class LinkData(
val fullText: String,
val linksList: List<Link>
)

/**
* Represents a link within the text, with a display text, optional link information, a style, and an
* action to perform when the link is clicked. Links can be used to navigate to other parts of the
* application, open external URLs, or trigger other actions.
*
* @property linkText The text to display for the link. This is the text that users will see and interact with.
* @property linkInfo Optional additional information about the link. This can be used to provide more context
* or details about the link destination.
* @property style The style to apply to the link text. This can be used to customize the appearance of the link,
* such as the font, color, or underline.
* @property onClick The action to perform when the link is clicked. This can be used to navigate to another
* screen, open a URL, or trigger any other desired action.
*/
@Immutable
data class Link(
val linkText: String,
val linkInfo: String? = null,
val style: SpanStyle,
val onClick: (String) -> Unit
)

And finally, we can use the composable as follows.

Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val context = LocalContext.current
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
Spacer(Modifier.height(32.dp))
TextWithLinks(
textStyle = TextStyle(
fontSize = 24.sp,
textAlign = TextAlign.Justify
),
linkData = LinkData(
fullText = "Welcome to our app! For more information about our Privacy Policy, " +
"please review it carefully. If you're having trouble, check out " +
"our Help Center for FAQs and troubleshooting guides. Ready to get started? " +
"Head over to the Account Settings page to personalize your experience.",
linksList = listOf(
Link(
linkText = "Account Settings",
linkInfo = "https://www.example.com/account-settings",
style = SpanStyle(color = Color.Blue)
) { str -> // This str will return the linkInfo of the link.
Toast.makeText(
context,
"Clicked on $str", //Toast will show -> Clicked on https://www.example.com/account-settings
Toast.LENGTH_LONG
).show()
},
Link(
linkText = "Help Center",
style = SpanStyle(color = Color.Red),
linkInfo = "refund"
) { str -> // This str will return the linkInfo of the link.
// navigateToHelpCenterScreen(helpTopic = str) will pass helpTopic = "refund"
},
Link(
linkText = "Privacy Policy",
style = SpanStyle(color = Color.Magenta),
) {
Toast.makeText(
context,
"Clicked on Privacy Policy",
Toast.LENGTH_LONG
).show()
}
)
)
)
}
}

And it looks like this:

A mobile screenshot showing a paragraph with multiple clickable links within the paragraph.

Conclusion

Handling links in text using AnnotatedString in Jetpack Compose provides a flexible way to style and manage interactions within your app's text content. By leveraging the composability and declarative nature of Jetpack Compose, you can create rich, interactive text experiences with minimal effort. Try experimenting with different styles and actions to see what works best for your app!

Happy Annotating!!!

--

--

Sushil Kafle
0 Followers

Android developer, passionate about creating intuitive apps. Love exploring new tech, continuous learning, and sharing knowledge. Let's connect and talk tech!