/AutoLinkText

A simple library that makes links, emails, and phone numbers clickable in text in Jetpack Compose and Kotlin Compose Multiplatform.

Primary LanguageKotlinApache License 2.0Apache-2.0

AutoLinkText

AutoLinkText is a simple library that makes links, emails, and phone numbers clickable in text in Jetpack Compose and Kotlin Compose Multiplatform.

Motivation

TextView has autoLink and Linkify but Compose doesn't have an equivalent. This library aims to fill that gap.

Android iOS
A screenshot of the demo app in Android A screenshot of the demo app in iOS
Desktop Web
A screenshot of the demo app in Desktop A screenshot of the demo app in Web

Features

  • Make links, emails, and phone numbers clickable in your text out of the box
  • Create custom matchers for your own patterns (e.g. hashtags, mentions, etc.)
  • Customizable styling for links
  • Customizable click listeners for links
  • Supports Compose Multiplatform (Android, iOS, Desktop/JVM, Wasm)

Usage

Version Catalog

If you're using Version Catalog, add the following to your libs.versions.toml file:

[versions]
#...
autolinktext = "1.1.1"

[libraries]
#...
autolinktext = { module = "sh.calvin.autolinktext:autolinktext", version.ref = "autolinktext" }

or

[libraries]
#...
autolinktext = { module = "sh.calvin.autolinktext:autolinktext", version = "1.1.1" }

Gradle

If you're using Gradle instead, add the following to your build.gradle file:

Kotlin DSL

dependencies {
    implementation("sh.calvin.autolinktext:autolinktext:1.1.1")
}

Groovy DSL

dependencies {
    implementation 'sh.calvin.autolinktext:autolinktext:1.1.1'
}

Examples

See demo app code for more examples.

Basic Usage

By default AutoLinkText turns URLs, emails, and phone numbers into clickable links and underlines them.

AutoLinkText(
    text = """
        |Visit https://www.google.com
        |Visit www.google.com
        |Email test@example.com
        |Call 6045557890
        |Call +1 (604) 555-7890
        |Call 604-555-7890
    """.trimMargin(),
    style = LocalTextStyle.current.copy(
        color = LocalContentColor.current,
    ),
)

Customize Link Color

You can override the default styling by mapping over the default list of rules and changing the style in each default rule.

AutoLinkText(
    text = "...",
    style = LocalTextStyle.current.copy(
        color = LocalContentColor.current,
    ),
    textRules = TextRuleDefaults.defaultList().map {
        it.copy(
            style = SpanStyle(
                color = MaterialTheme.colorScheme.primary,
                textDecoration = TextDecoration.Underline
            )
        )
    }
)

Make Your Own Rules

Create your own rules by providing TextRules with a TextMatcher, an optional MatchStyle for the matched text and an optional onClick lambda.

AutoLinkText(
    text = "Make your own rules like #hashtag and @mention",
    style = LocalTextStyle.current.copy(
        color = LocalContentColor.current,
    ),
    textRules = listOf(
        TextRule(
            textMatcher = TextMatcher.RegexMatcher(Regex("#\\w+")),
            style = SpanStyle(
                color = MaterialTheme.colorScheme.primary,
                textDecoration = TextDecoration.Underline
            ),
            onClick = {
                println("Hashtag ${it.matchedText} clicked")
            },
            annotationProvider = { "https://link.to.hashtag" },
        ),
        TextRule(
            textMatcher = TextMatcher.RegexMatcher(Regex("@\\w+")),
            style = SpanStyle(
                color = MaterialTheme.colorScheme.secondary,
                textDecoration = TextDecoration.Underline
            ),
            onClick = {
                println("Mention ${it.matchedText} clicked")
            },
            annotationProvider = { "https://link.to.mentions" },
        )
    )
)

Match Dependent Styling

Use styleProvider to provide a SpanStyle based on the matched text.

AutoLinkText(
    text = "Style the same rule differently like #hashtag1 and #hashtag2",
    style = LocalTextStyle.current.copy(
        color = LocalContentColor.current,
    ),
    textRules = listOf(
        TextRule(
            textMatcher = TextMatcher.RegexMatcher(Regex("#\\w+")),
            styleProvider = {
                val hashtag = it.matchedText
                if (hashtag == "#hashtag1") {
                    SpanStyle(
                        color = Color.Red,
                        textDecoration = TextDecoration.Underline
                    )
                } else {
                    SpanStyle(
                        color = Color.Blue,
                        textDecoration = TextDecoration.Underline
                    )
                }
            },
            onClick = {
                println("Hashtag ${it.matchedText} clicked")
            },
            annotationProvider = { "https://link.to.hashtag" },
        ),
    )
)

TextRules don't have to be Clickable

You can create TextRules that are not clickable by not providing an onClick lambda.

AutoLinkText(
    text = "This is very important",
    style = LocalTextStyle.current.copy(
        color = LocalContentColor.current,
    ),
    textRules = listOf(
        TextRule(
            textMatcher = TextMatcher.StringMatcher("important"),
            style = SpanStyle(color = Color.Red),
            annotationProvider = { null },
        ),
    )
)

Make Your Own Matcher

Create your own matchers with TextMatcher.FunctionMatcher that takes the given text and returns a list of SimpleTextMatchResults.

AutoLinkText(
    text = "Make every  other  word blue",
    style = LocalTextStyle.current.copy(
        color = LocalContentColor.current,
    ),
    textRules = listOf(
        TextRule(
            textMatcher = TextMatcher.FunctionMatcher {
                val matches = mutableListOf<SimpleTextMatchResult<Nothing?>>()
                var currentWordStart = 0
                "$it ".forEachIndexed { index, char ->
                    if (char.isWhitespace()) {
                        val match = SimpleTextMatchResult(
                            start = currentWordStart,
                            end = index,
                        )
                        if (it.slice(match).isNotBlank()) {
                            matches.add(match)
                        }
                        currentWordStart = index + 1
                    }
                }
                matches.filterIndexed { index, _ ->  index % 2 == 0 }
            },
            style = SpanStyle(color = Color.Blue),
            annotationProvider = { null },
        ),
    )
)

Caveats

  • Google seems to be working on replacing ClickableText API with LinkAnnotation but it is not functional yet. I will try my best to keep this library up to date with the latest Compose APIs.
  • The default list of TextRules are accessible. But at the moment accessibility is not great if you add TextRules with onClick function that does something other than opening a link because of a bug with ClickableText. Hopefully this gets fixed in the future (See Google IssueTracker issue 274486643).

API

Running the demo app

To run the Android demo app, open the project in Android Studio and run the app.

To run the iOS demo app, open the iosApp project in Xcode and run the app or add the following Configuration to the Android Studio project, you may need to install the Kotlin Multiplatform Mobile plugin first.

Android Studio Debug Configuration Page

To run the web demo app, run ./gradlew :composeApp:wasmJsBrowserDevelopmentRun.

To run the desktop demo app, run ./gradlew :demoApp:ComposeApp:run.

Contributing

Open this project with Android Studio Preview.

You'll want to install the Kotlin Multiplatform Mobile plugin in Android Studio before you open this project.

License

Copyright 2023 Calvin Liang

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.