【Flutter】テキスト中のURLに自動でリンクをはる方法

# はじめに
テキスト中のURL部分を自動でハイパーリンク化する方法をご紹介します。

 # 目標
 テキスト中のURL部分に自動でリンクをはる。
 URL部分をタップすると、URLが示すWebページに遷移する。

 # 実装

 Flutterにおける`Text`ウィジェットのソースコードは次のようになっており、`TextSpan`ウィジェットから構成されていることがわかります。

 https://github.com/flutter/flutter/blob/2ad6cd72c0/packages/flutter/lib/src/widgets/text.dart#L618


```dart:
Widget result = RichText(
  ...
  text: TextSpan(
  style: effectiveTextStyle,
  text: data,
  children: textSpan != null ? <InlineSpan>[textSpan!] : null,
  ),
);
```

ここで、`TextSpan`にはユーザによる操作に対する処理を登録する`recognizer`というプロパティがあります。

https://github.com/flutter/flutter/blob/2ad6cd72c0/packages/flutter/lib/src/painting/text_span.dart#L65

したがって、今回の目標である「URL部分をタップしたらリンク先に遷移する」という機能は、`TextSpan`にタップ時の処理を設定することで実現できます。


## URLをタップするとリンク先に遷移する処理の実装
　タップ時の処理については
`TextSpan`
の`recognizer`プロパティを設定することで実現できます。
指定したURLに遷移する機能は`url_launcher`というパッケージを使用して実現します。


仮にURL文字列を`urlText`とすると、リンク先への遷移処理は次のようになります（コードサンプルでは外部ブラウザを使用するよう指定しています）。

```dart:
await launchUrlString(_encadedUrl,mode: LaunchMode.externalApplication);
```

また`TextSpan`の
`recognizer`にタップされた時の処理を実装するには、`TapGestureRecognizer`を使用します。

したがって、テキストをタップした際、そのテキストの文字列が示すWebページに遷移するには、次のような実装を行う必要があります。

```dart:
final _recognizer = TapGestureRecognizer()
  ..onTap = () async {
  await launchUrlString(_encadedUrl,mode: LaunchMode.externalApplication);
  };
final _textSpan = TextSpan(
  text:  'urlText',
  recognizer:  _recognizer,
);
```

URLのエンコード処理を追加した結果は次のようになります。

```dart:
final _encadedUrl = Uri.encodeFull('urlText');
final _recognizer = TapGestureRecognizer()
  ..onTap = () async {
  await launchUrlString(_encadedUrl,mode: LaunchMode.externalApplication);
  };
final _textSpan = TextSpan(
  text:  'urlText',
  recognizer:  _recognizer,
);
```

これで `urlText`という文字列をタップしたときに、 `urlText`というURLが表すリンク先に飛ぶ処理が実装できました。

## テキストからURL部分を自動で抽出する処理の実装
任意のテキスト中のURL部分を自動でハイパーリンク化するためには、テキストからURL部分を抽出する必要があります。

URLの正規表現を仮に次の様に設定します（実装の目的によって自由に変えて構いません）。

```dart:
static RegExp get _urlRegExp => RegExp(
  r"(http(s)?:\/\/[a-zA-Z0-9-.!'()*;/?:@&=+$,%_#]+)",
  caseSensitive: false,
);
```

この正規表現を使って、正規表現にマッチする部分はリンクとして、マッチしない部分はノーマルなテキストとして`TextSpan`を生成し、最終的には`Text`ウィジェットの形にすることを目指します。

文字列が与えられた時に前述の`recognizer`をセットした`TextSpan`を返却するメソッド`generateLinkTextSpan`を以下の様に定義します。

```dart:
TextSpan generateLinkTextSpan(String url){
  final _encadedUrl = Uri.encodeFull(url);
  final _recognizer = TapGestureRecognizer()
    ..onTap = () async {
    await launchUrlString(_encadedUrl,mode: LaunchMode.externalApplication);
  };
  final _textSpan = TextSpan(
    text:  url,
    recognizer:  _recognizer,
    //style: リンク部分のテキストスタイル
  );
  return _textSpan;
}
```

また、URLでない文字列が与えられたときにノーマルなテキストの`TextSpan`を返却するメソッド`generateCommonTextSpan`を次のように定義します。

```dart:
TextSpan generateLinkTextSpan(String url){
  final _textSpan = TextSpan(
    text:  url,
  );
  return _textSpan;
}
```

任意のテキストを正規表現にマッチする部分と、マッチしない部分に分割する処理は`splitMapJoin`を利用することで、次のようにかけます。

```dart:
this._text_.splitMapJoin(RegExp('<正規表現パターン>'),

  //マッチした部分
  onMatch: (Match match) {
    //match.group[0]でパターン一致全体の文字列を取得できる
    return ''; //String型をreturnする必要があるため
  },

  //マッチしなかった部分
  onNonMatch: (String text) {
    //マッチしなかった文字列に対する処理
    return '';
  }
);
```

よって任意のテキストを正規表現にしたがってURL部分とそれ以外の部分に分け、URLにはタップしたらWeb遷移するような機能を持ったTextSpanを返却するメソッド`generate`は次のようになります。

```dart:
//実際には引数_rawTextはコンストラクタで与えるが、簡単のため以下とする
TextSpan generate(Text _rawText) {
  final List<TextSpan> _textSpans = [];
  _rawText.splitMapJoin(
    ThisIsAutoLinkTextSpan._urlRegExp,
    onMatch: (Match match) {
      final _urlSpan = this._generateUrlTextSpan(match.group(0) ?? '');
      _textSpans.add(_urlSpan);
      return '';
    },
    onNonMatch: (String text) {
      final _commonSpan = this._generateCommonTextSpan(text);
      _textSpans.add(_commonSpan);
      return '';
    },
  );
  return TextSpan(children: _textSpans);
}
```

こちらのメソッドを以下のように`Text.rich`ウィジェット内で利用すると、任意の文字列のURL部分に自動でハイパーリンク機能を持たせることができます。
（ここでは`generate`が定義されるクラスを仮に`AutoLinkText`とします。）

```dart:
Text.rich(
  AutoLinkText.generate(rawText),
)
```


# おわりに
テキスト中のURLを自動でハイパーリンク化する方法を紹介しました。
`Text`ウィジェットだけでは対応できないUI表現は、このように`TextSpan`をうまく利用することで可能となる場合があります。