Flutter 正規表現を利用してリンクテキストを作る

APIから文字列を取得してウィジェットへ表示したときにその文字列の中に**https://**が含まれていた場合、マッチしたテキスト部分のみタップイベント発火させてブラウザアプリケーションを起動してリンクさせたい:frowning2:


そこで！その処理をどこでも利用できるようなStatefulWidgetを作成してみました。

![スクリーンショット 2020-09-15 20.09.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478758/e588a1ca-65bc-4691-629f-0a0933381f52.png)


## 利用したパッケージ
- url_launcher


```dart:link_text_atoms.dart
import 'package:flutter/gestures.dart';
import 'package:url_launcher/url_launcher.dart';

class LinkTextAtoms extends StatefulWidget {
  LinkTextAtoms(this.text, {this.linkStyle, this.textStyle});

  final String text;
  final TextStyle linkStyle;
  final TextStyle textStyle;

  @override
  createState() => LinkTextAtomsState();
}

class LinkTextAtomsState extends State<LinkTextAtoms> {
  List<GestureRecognizer> recList = List<GestureRecognizer>();

  @override
  Widget build(BuildContext context) {
    // 通常のテキストデフォルトスタイル
    var text = Theme.of(context).textTheme.bodyText1.merge(widget.textStyle);
    // リンク時のテキストデフォルトスタイル
    var link = Theme.of(context)
        .textTheme
        .bodyText1
        .merge(TextStyle(
            inherit: true,
            color: Colors.blue,
            decoration: TextDecoration.underline))
        .merge(widget.linkStyle);

    List children = List<TextSpan>();
    List matches = RegExp(r'https?://[a-zA-Z0-9\-%_/=&?.]+')
        .allMatches(widget.text)
        .toList();
    var links = matches.map<TextSpan>((m) {
      var rec = TapGestureRecognizer()
        ..onTap = () async {
          // URL を処理できるアプリケーションが存在しない可能性があるためcanLaunchメソッドで処理可能かどうかチェックし、OK の場合のみlaunch()メソッドを呼ぶようにする
          if (await canLaunch(m.group(0))) {
            await launch(m.group(0), forceSafariVC: false);
          }
        };
      recList.add(rec);
      return TextSpan(text: m.group(0), style: link, recognizer: rec);
    }).toList();
    if (matches.length > 0) {
      for (var i = 0; i < matches.length; i++) {
        if (i == 0) {
          if (matches[i].start != 0) {
            children.add(TextSpan(
                text: widget.text.substring(0, matches[i].start), style: text));
          }
          children.add(links[i]);
          continue;
        }
        children.add(TextSpan(
            text: widget.text.substring(matches[i - 1].end, matches[i].start),
            style: text));
        children.add(links[i]);
      }
      if (matches.last.end != widget.text.length) {
        children.add(TextSpan(
            text: widget.text.substring(matches.last.end), style: text));
      }
    } else {
      children.add(TextSpan(text: widget.text, style: text));
    }
    return RichText(text: TextSpan(children: children));
  }

  @override
  dispose() {
    for (var i = recList.length - 1; i >= 0; i--) {
      recList.removeAt(i);
    }
    super.dispose();
  }
}
```

テキストの一部の**色を変えたい、リンクにしたい、イベント発火させたい、**ので、TextSpanウィジェットを利用し、通常テキストのデフォルトスタイルとリンクテキストだった場合のテキストスタイルを振り分けています。
リンクテキストだった場合はテキストの文字を青くし、アンダーラインを引いています。ここのスタイルはお好みに合わせて変更してしまってよいです。

TextSpanのrecognizerプロパティでタップを検出できるよう設定し、url_launcherパッケージで外部ブラウザを起動します。

...コード量長いし力技感いなめない。:frowning2:

## 利用方法
```dart:
// ↑上で作成したファイルをimportする
import 'package:<プロジェクト名>/widgets/components/atoms/link_text_atoms.dart';

class SampleClass extends StatelessWidget {
  SampleClass({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
        child: Center(
            child: LinkTextAtoms(
                'このテキストはリンクですhttps://www.google.com/このテキストはリンクです')));
  }
}
```


関係ないけどproviderの作者がhooksパッケージ出してたの最近気が付きました。。
https://github.com/rrousselGit/flutter_hooks

新しい状態管理パッケージのRiverpodと併用してうまく使えていければと思います:v:
早くこのあたりのベストプラクティスが決まってくれると嬉しいなと思う今日この頃でございます:kissing_closed_eyes:
たまにFlutter（Dart）関係のことつぶやいていたりするので、[@amitatantan](https://twitter.com/amitatantan)をフォローしてくれると嬉しいです。


## 参考にしたdocument

- [RegExp（正規表現クラス）](https://api.flutter.dev/flutter/dart-core/RegExp-class.html)
- [URL でプログラムを起動する方法](https://flutter.keicode.com/basics/urllauncher.php)