【kotlin】自作TwitterクライアントAndroidアプリでハッシュタグをリンク化する

# 自作TwitterクライアントAndroidアプリでハッシュタグをリンク化する

最近、qiitaに寄稿していなかったので久々に寄稿。
Kotlin系の記事です。

## やりたかったこと

![https://wandering-engineer.tech/wp-content/uploads/Screenshot_1580788577.png](https://wandering-engineer.tech/wp-content/uploads/Screenshot_1580788577.png)

自作Twitterクライアントアプリ「[TwitMorse 〜モールス信号でつぶやこう、あなたのSOSに誰かが答えてくれる〜](https://play.google.com/store/apps/details?id=jp.sub.takelab.twitmorus)」のAndroid版での挙動で、ハッシュタグに反応するLinkMovementMethodを実装して、ハッシュタグ検索結果画面へ遷移させよう、というものです。

- ハッシュタグの色付け
- ハッシュタグをタップしたらハッシュタグ専用画面への遷移

この時、kotlinのMatcherの挙動で苦労したので、後学のために残します。

## 修正前の実装（クラッシュ発生します）

以下、修正前のコード

```kotlin

        /**
         * ハッシュタグを抽出してタップできるようにする
         * タップするとHashTagSearchResultActivityに遷移する
         * いずれStringUtilなどのUtilクラスにまとめられるようにtextViewも渡す。
         */
        fun onTapHashTag(text: String, textView: TextView) {
            textView.movementMethod = LinkMovementMethod.getInstance() //ClickableSpan#onClickを動かすのに必要
            val spannableString = SpannableStringBuilder(text)
            val hashTagRegex = Regex("(?:^|\\s)([＃#]([^\\s]+))[^\\s]?")
            val matcher = Pattern.compile(hashTagRegex.toString()).matcher(text)
            // 参考 : https://qiita.com/droibit/items/75416c0955b797931bb8#kotlintext
            hashTagRegex.findAll(text)
                    .map { it.value }
                    .forEach {
                        val clickableSpan = object : ClickableSpan() {

                            override fun onClick(widget: View) {
                                val hashTagSearchResultActivity = Intent(widget.context, HashTagSearchResultActivity::class.java)
                                hashTagSearchResultActivity.putExtra(IntentKeyUtil.HASH_TAG_SEARCH_QUERY, it)
                                hashTagSearchResultActivity.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                                widget.context.startActivity(hashTagSearchResultActivity)
                            }

                            override fun updateDrawState(ds: TextPaint) {
                                super.updateDrawState(ds)
                                ds.isUnderlineText = false
                            }
                        }
                        // ここが問題の箇所
                        spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
                        textView.text = spannableString
                    }
        }
```

ちょいと長めのコードで申し訳ないですが、
ポイントは2つ

- ```val hashTagRegex = Regex("(?:^|\\s)([＃#]([^\\s]+))[^\\s]?")```
  - 参考: [Android リアルタイム入力でハッシュタグ形式に文字装飾するTIPS](https://qiita.com/nosaka/items/aeb3ae8c0a3a187dd7a4#s%E3%81%81-%E3%82%93%E3%82%A1-%E3%83%B3%E3%83%BCa-za-z%E4%B8%80-%E9%BE%A0-rs%E3%81%A7%E3%81%AF%E4%B8%96%E7%95%8C%E3%82%92%E5%88%B6%E3%81%9B%E3%81%AA%E3%81%84)
- 以下苦労した箇所

```kotlin

// ここが問題の箇所
spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannableString
```

### Matcher.find()を呼ばないとMatcher.start()もMatcher.end()も正常に動作しない

ハッシュタグの色付けには成功したものの、そのハッシュタグをタップすると下記例外でクラッシュします。
この修正に1日半かけて悩みました・・・。

```
java.lang.IllegalStateException: No successful match so far
```

```kotlin:修正前

// ここが問題の箇所
spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannableString
```
そんなMatchはおきねぇよ！！と。

そこで下記のように修正したら、正常に動作しました。

```kotlin:修正後
if (matcher.find()) {
    spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    textView.text = spannableString
}
```

上記を見れば分かるように```matcher.find()```を呼んであげるとクラッシュがなくなりました。

### 修正後の実装

```kotlin
        /**
         * ハッシュタグを抽出してタップできるようにする
         * タップするとHashTagSearchResultActivityに遷移する
         * いずれStringUtilなどのUtilクラスにまとめられるようにtextViewも渡す。
         */
        fun onTapHashTag(text: String, textView: TextView) {
            textView.movementMethod = LinkMovementMethod.getInstance() //ClickableSpan#onClickを動かすのに必要
            val spannableString = SpannableStringBuilder(text)
            val hashTagRegex = Regex("(?:^|\\s)([＃#]([^\\s]+))[^\\s]?")
            val matcher = Pattern.compile(hashTagRegex.toString()).matcher(text)
            // 参考 : https://qiita.com/droibit/items/75416c0955b797931bb8#kotlintext
            hashTagRegex.findAll(text)
                    .map { it.value }
                    .forEach {
                        val clickableSpan = object : ClickableSpan() {

                            override fun onClick(widget: View) {
                                val hashTagSearchResultActivity = Intent(widget.context, HashTagSearchResultActivity::class.java)
                                hashTagSearchResultActivity.putExtra(IntentKeyUtil.HASH_TAG_SEARCH_QUERY, it)
                                hashTagSearchResultActivity.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                                widget.context.startActivity(hashTagSearchResultActivity)
                            }

                            override fun updateDrawState(ds: TextPaint) {
                                super.updateDrawState(ds)
                                ds.isUnderlineText = false
                            }
                        }
                        if (matcher.find()) {
                            spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
                            textView.text = spannableString
                        }
                    }
        }
```

これでmatcherの参照が動くためか、```java.lang.IllegalStateException: No successful match so far```のクラッシュは発生しなくなりまして、思い通りの挙動をしてくれるようになりました。

#### 複数ハッシュタグのリンク化

一つのTwitter投稿にハッシュタグが一つなわけがありません。下記やっておかないと、ひとつめのハッシュタグにしか反応しなくなるので、注意点として一応あげておきます

```kotlin

hashTagRegex.findAll(text).map { it.value }.forEach {}
```




## 参考資料

- [Kotlinのコーディングが捗る標準ライブラリ | qiita](https://qiita.com/droibit/items/75416c0955b797931bb8#kotlintext)
- [Android リアルタイム入力でハッシュタグ形式に文字装飾するTIPS | qiita](https://qiita.com/nosaka/items/aeb3ae8c0a3a187dd7a4#s%E3%81%81-%E3%82%93%E3%82%A1-%E3%83%B3%E3%83%BCa-za-z%E4%B8%80-%E9%BE%A0-rs%E3%81%A7%E3%81%AF%E4%B8%96%E7%95%8C%E3%82%92%E5%88%B6%E3%81%9B%E3%81%AA%E3%81%84)
- [【kotlin】Matcher.find()を呼ばないとMatcher.start()もMatcher.end()も正常に動作しない | takelab.note](https://wandering-engineer.tech/2020/02/04/kotlin-matcher-find-does-not-work/)

## 宣伝


- [TwitMorse 〜モールス信号でつぶやこう、あなたのSOSに誰かが答えてくれる〜 | Google play](https://play.google.com/store/apps/details?id=jp.sub.takelab.twitmorus)
  - Android版
  - ![https://wandering-engineer.tech/wp-content/uploads/chart1.png](https://wandering-engineer.tech/wp-content/uploads/chart1.png)
  - お陰様で5周年、DAU 90
- [TwitMorse 〜モールス信号でつぶやこう、あなたのSOSに誰かが答えてくれる〜 | App Store](https://apps.apple.com/jp/app/id1485828762)
  - iOS版
  - ![https://wandering-engineer.tech/wp-content/uploads/twitmorse_iOS_qr.png](https://wandering-engineer.tech/wp-content/uploads/twitmorse_iOS_qr.png)
  - 昨年12月にリリースしたてホヤホヤ、まだ最低限機能しかない、絶賛開発苦労中（誰か助けて）