Kotlin Multiplatformでテキスト内リンクを開く
はじめに
アプリケーション開発において、リンクをもとにブラウザを開くような処理は大抵出てくると思いますが、各プラットフォームごとに処理を書くことになりがちで、面倒です。
Kotlin Multiplatform(以下KMP)も苦労するだろうな…と思っていたのですが、LocalUriHandler
が用意されており、簡単に実装できます。デスクトップの場合、マウスカーソル周りの処理が追加で必要になるため、それについても本記事で紹介します。
AnnotatedStringの準備
単一テキスト内に複数のスタイルを適用したい場合は AnnotatedString
を使用する必要があります。これを用いてテキスト内リンクの色を変更・下線を引く処理を行い、加えてリンクに関する情報を withAnnotation
で追加します。
@Composable
fun App() {
val linkUri = "https://www.jetbrains.com/kotlin-multiplatform/"
val linkStyle = SpanStyle(
color = MaterialTheme.colors.primary,
textDecoration = TextDecoration.Underline
)
val annotated = buildAnnotatedString {
withAnnotation(tag = "link", annotation = linkUri) {
withStyle(linkStyle) {
append("Kotlin Multiplatform")
}
}
append(" でマルチプラットフォーム開発")
}
// ...
}
withAnnotation
は tag: String
と annotation: String
をパラメータとして取り、アノテーションを認識するためのタグと、アノテーションの中身に入れる情報を渡すことができます。後々このタグを使い、クリック時にリンク情報を引っ張り出すことになります。
表示部・URLを開く処理の実装
URLを開く処理はシンプルです。LocalUriHandler
がプラットフォームごとにハンドラを捕まえてくれるため、実装では openUri()
に対象のリンクを渡すだけです。
val handler = LocalUriHandler.current
handler.openUri(url)
LocalUriHandler.current
は @Composable
関数の中でしか呼び出せないため、その点は注意が必要です。下記サンプルでは予めハンドラを取得しておき、onClick()
内で openUri()
を呼び出しています。
クリック箇所の判別については、ClickableText
の onClick
にはクリック箇所の情報を offset
として取得する事ができるため、これを利用します。getStringAnnotation
を使ってクリックしたoffset位置に対象のアノテーションがあるかチェックします。
アノテーション箇所だった場合、AnnotatedString.Range<String>
が返ってきて、tag
でタグ、item
でアノテーションの中身を取得できるので、これを使ってリンクを開けます。
@OptIn(ExperimentalTextApi::class, ExperimentalFoundationApi::class)
@Composable
fun App() {
val linkUri = "https://www.jetbrains.com/kotlin-multiplatform/"
val linkStyle = SpanStyle(
color = MaterialTheme.colors.primary,
textDecoration = TextDecoration.Underline
)
val annotated = buildAnnotatedString {
withAnnotation(tag = "link", annotation = linkUri) {
withStyle(linkStyle) {
append("Kotlin Multiplatform")
}
}
append(" でマルチプラットフォーム開発")
}
val handler = LocalUriHandler.current
MaterialTheme {
Box(Modifier.fillMaxSize()) {
ClickableText(
text = annotated,
onClick = { offset ->
val link = annotated.getStringAnnotations(
tag = "link", start = offset, end = offset
).firstOrNull()
if (link != null) {
handler.openUri(link.item)
}
}
)
}
}
}
リンク箇所にカーソルをホバーしたとき指マークにする
一応この時点でリンクを開く処理は完成しているのですが、デスクトップで動かしてみると少し違和感があります。
カーソルを合わせても指マークになりません。色でリンクだと分かりはしますが、やはりカーソルのアイコンが変わったほうが直感的です。この点を直していきます。
これについてはModifier.pointerHoverIcon
で解決します。コンポーザブルがホバーされたときのアイコンを変更することができるため、リンク部にホバーしているかどうかの判定と組み合わせて実装していきます。
ClickableText
のonHover
はonClick
と同様にoffsetを取ることができますが、こちらはInt?
が返ってくるのでnullの時(=ホバーされていない時)は弾く必要があります。
@OptIn(ExperimentalTextApi::class, ExperimentalFoundationApi::class)
@Composable
fun App() {
val linkUri = "https://www.jetbrains.com/kotlin-multiplatform/"
val linkStyle = SpanStyle(
color = MaterialTheme.colors.primary,
textDecoration = TextDecoration.Underline
)
val annotated = buildAnnotatedString {
withAnnotation(tag = "link", annotation = linkUri) {
withStyle(linkStyle) {
append("Kotlin Multiplatform")
}
}
append(" でマルチプラットフォーム開発")
}
val handler = LocalUriHandler.current
var isLinkHovered by remember { mutableStateOf(false) }
MaterialTheme {
Box(Modifier.fillMaxSize()) {
ClickableText(
text = annotated,
onClick = { offset ->
val link = annotated.getStringAnnotations(
tag = "link", start = offset, end = offset
).firstOrNull()
if (link != null) {
handler.openUri(link.item)
}
},
onHover = { offset ->
if (offset == null) return@ClickableText
val link = annotated.getStringAnnotations(
tag = "link", start = offset, end = offset
).firstOrNull()
isLinkHovered = (link != null)
},
modifier = Modifier.pointerHoverIcon(
if (isLinkHovered) PointerIcon.Hand
else PointerIcon.Default
)
)
}
}
}
これで完成です👍
まとめ
KMPだとWindows, Android共にリンクの処理が LocalUriHandler
一つで解決するので、かなり簡単に実装することができます。ClickableText
については少々扱いがややこしいですが、色々と使い道はあるので覚えていて損はないと思います。
という記事を書いている最中に気づいたのですが、細心のパッチノートを見るとClickableText
は 1.7.0-alpha07 でDeprecatedになった ようです…。今のところ落ちてくる安定版は1.6.xなので大丈夫そうですが、近い将来別の方法に乗り換えた方がよさそうです。
1.7では LinkAnnotation
に置き換わるようなので、最新版が降ってきたらまた記事を書き直そうと思います…
参考
Discussion