Flutter URL/メールアドレス抽出＋外部ブラウザ/メーラー起動

# やりたいこと
文字列内のURL/メールアドレスを抽出し、リッチテキストでその部分だけ色変換
タップするとURLなら外部ブラウザを起動、メールアドレスならメーラーを起動する
下記画像のように青字の部分が該当になります。

![スクリーンショット 2023-05-28 15.15.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1077515/b1942a34-cf24-0286-3690-9554a242e976.png)

調べたところflutterにはメールアドレスやURLを抽出して表示したり、onTapをつける系のライブラリがなさそうなので、仕方なく独自で正規表現を使って実装することに。。。

# 実装概要
方法としては下記のような流れで進めていきます

1. 正規表現`RegExp`に対して`allMatches`を使って一致するテキストを抽出
2. 通常テキストと色付きテキストを棲み分け
3. その後`List<InlineSpan> spans`のリストに全部入れて一つの`widget`として表示
4. URL/メーラー起動のタップイベントの付与

前提条件： 使用テキスト（安定の坊ちゃん), 正規表現（諸説あり）
```dart
const text = '親譲りの無鉄砲で子供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間程腰を抜かした事がある。https://hogehoge.comなぜそんな無闇（むやみ）をしたと聞く人があるかもしれぬ。別段深い理由でもない。hogehoge@gmail.com新築の二階から首を出していたら，同級生の一人が冗談に，いくら威張っても，そこから飛び降りる事は出来まい。弱虫や－い。と囃（はや）し立てたからである。';

final urlRegExp = RegExp(r"https?://([\w-]+\.)+[\w-]+(/[\w-./?%&=#]*)?");
final mailRegExp = RegExp(r"[a-zA-Z0-9_.+-]+@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$");

final List<InlineSpan> spans = [];
```

# 実装詳細

### 1. URL/メールアドレス抽出
以下は　URLのRegExpの`allMatches`を使って一致した部分を抽出するコードです。
メールアドレスの場合は`urlRegExp`を`mailRegExp`に変えればいいだけ

```dart
for (Match match in urlRegExp.allMatches(text)) {
    urlText = match.group(0)!
}
```

### 2. 通常テキストと色付きテキストを棲み分け

```dart
var currentIndex = 0;
for (Match match in urlRegExp.allMatches(text)) {
    launchRichText(match.group(0)!);
    normalText(text.substring(currentIndex, match.start);
    currentIndex = match.end;
}

```
　`text.substring(currentIndex, match.start)`は0文字目からURL部分の初めの文字数目、URL部分の終わりの文字数目から次の一致部分の初めまでを指定します。

`launchRichText`はURLもしくはメールアドレス色付きとタップイベントを追加する`TextSpan`のwidget
`normalText`は上記以外の`TextSpan`のwidgetです

以下は一例
```dart
TextSpan launchRichText(String text, bool mailAddressFlag) {
    return TextSpan(
        text: text,
        style: const TextStyle(color: Colors.lightBlueAccent),
        recognizer: TapGestureRecognizer()..onTap = () {
          // タップイベント
        },
    );
}

TextSpan normalText(String text) {
    return TextSpan(
      text: text,
      style: const TextStyle(color: Colors.black),
    );
}
```

### 3. `List<InlineSpan> spans`のリストに追加して表示

spansのリストにlaunchとnormalのTextSpanを追加
作成したListを`RichText`としてまとめて表示
これで見た目の実装は完了！

```dart
// URLに該当の文字列を抽出
for (Match match in urlRegExp.allMatches(text)) {
    spans.add(normalText(text.substring(currentIndex, match.start)));
    spans.add(launchRichText(match.group(0)!, false));
    currentIndex = match.end;
}

return RichText(
    text: TextSpan(children: spans),
);
```

### 4. タップイベントの付与

urlの外部ブラウザ遷移やメーラーの起動には`launchUrl`を使用します。
URLの場合はそのまま、メーラーの場合は`mailto：`をつけてlaunchUrlにかけるとメーラーが起動します
（テストする場合は必ず端末でビルドしてください。。。シミュレーターでメーラーが出ず、時間を無駄にした経験があります）

https://pub.dev/packages/url_launcher

以下は一例です。
```dart
Future<void> openLaunch(String launchText, bool mailAddressFlag) {
    if (mailAddressFlag) {
        launchText = 'mailto:$text'
    }
    final parseAddress = Uri.parse(launchText);
    try{
        await launchUrl(parseAddress);
    } catch (e) {
        // Android端末でブラウザが入ってない場合
        Logger.e('外部機能起動時にエラー発生')
    }

}
```
とこんな感じでしょうか

# まとめ
基本的にはベタがきでこのような文章をアプリ内に載せることはないので、url/メールアドレス抽出部分はあまり使用しないかと思います。（運用管理が必要な文書/規約などはwebviewで出すのが一般的でしょう）

今回使用した`launchUrl`も特にwebviewなどで利用規約を掲載したりする際に`navigationDelegate`の設定にするのが一般的な使われ方だと思うので、かなりイレギュラーなパターンだと思いますが、そういったシチュエーションもありましたので何かのご参考まで。