【Flutter】enumとurl_launcherでスマートなブラウザ起動方法を実現する

## はじめに

モバイルアプリにおいてブラウザを使ってWebページを開かせたい時って結構あると思います。
つい最近の現場であったのが、設定画面で以下のウェブサイトに飛ばしたい時でした。

- そのサービスのホームページ
- プライバシーポリシーをのせたウェブページ
- 利用規約をのせたウェブページ

上記の例以外にもヘルプページやお問い合わせフォームなどウェブページを開かせたいことはよくあります。
そんな時に便利なパッケージが**url_launcher**です。

https://pub.dev/packages/url_launcher

今回はそのurl_launcherに渡すurlと、どんな方法でブラウザを開くかを決める`LaunchMode`を`enum`を使って管理しやすくする方法を解説します。

### 記事の対象者

- url_launcherを使う際に便利に扱いたい方
- enumで動的な値をフィールドに持たせたいと悩んでいる方

### 記事を執筆時点での筆者の環境

```
[✓] Flutter (Channel stable, 3.22.1, on macOS 14.3.1 23D60 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.3)
[✓] VS Code (version 1.90.1)
```
## 今回のサンプルプロジェクト

ブラウザを開くボタンが三つあり、それぞれに違ったウェブサイトへ遷移するようになっています。
Gifだとわかりづらいですが、三つとも違った方法でブラウザを開いています。
1. アプリ内ブラウザ
1. アプリ外ブラウザ
1. WebView

また、一番下の**Change Flavor**ボタンを押すとボトムシートが立ち上がります。
そこで選択した結果によってFlavorが切り替わります。
**changeLinkを開く**ボタンを押した場合、現在の`Flavor`によって開くサイトが変わるようになっています。
この`Flavor`によって開くサイトが変えるというのは、例えば開発環境と本番環境では接続するサーバー、APIを変える時によく使います。


![url_launcher_sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/029ebbb7-18fe-697e-6dd0-92acde1185f6.gif)


ブラウザの開き方の違いについてはこちらの方が丁寧に解説されていましたので、興味がある方はご覧になってみてください。

https://zenn.dev/tsuruo/articles/56f3abbb132f90



### ソースコード

https://github.com/HaruhikoMotokawa/url_launcher_sample

### 補足
**補足１**
今回の`Flavor`はサンプルのため、擬似的に定義しただけのものです。
プロジェクトのデバック実行はmain関数のrunを押して行っています。

**補足２**
iOSとAndroidのどちらでも実行できます。

**補足３**
url_launcherの導入方法の解説は省略しています。
恐らく初学者の方がつまりやすい、OSごとのブラウザ設定については
こちらの方が解説されていますので参考にしてみてください。

https://zenn.dev/joo_hashi/articles/ee377ae7f1fe05

## 1. `enum`にブラウザ起動モードをフィールドとして持たせる
以前書いたこの記事でも紹介していますが`enum`にはフィールド持たせることができます。

https://qiita.com/Harx02663971/items/ab19d396656f0e90309d

まずは`final LaunchMode mode;`というフィールドを作り、それぞれの列挙子にどのモードでブラウザを起動するかをコンストラクタで設定しています。

```lib/external_links.dart

/// 外部リンク
enum ExternalLinks {
  /// Qiitaへの接続で、[mode]はアプリ **外** ブラウザで開く
  qiita._(LaunchMode.externalApplication),

  /// Zennへの接続で、[mode]はアプリ **内** ブラウザで開く
  zenn._(LaunchMode.inAppBrowserView),

  /// Flavorによって接続先が変わり、[mode]はWebViewで開く
  changeLink._(LaunchMode.inAppWebView);

  const ExternalLinks._(this.mode);

  /// [LaunchMode]はurl_luncherに定義されたenumで、URLを起動するモードを指定する
  /// enumのフィールドはコンストな値（変更不可能な値）のみを持つことができる。
  final LaunchMode mode;
}

```

## 2. urlを動的な値で設定する

肝心なurlについて上記と同じように`final String url;`としたいところです。
enumのフィールドは複数持つことも可能です。
しかし、今回の場合そうはいきません。

`ExternalLinks.qiita`と`ExternalLinks.zenn`の場合は大丈夫です。
`ExternalLinks.qiita`と`ExternalLinks.zenn`のurlは固定の値なので設定できます。

しかし`ExternalLinks.changeLink`はFlavorによって接続先が変わる仕様です。
つまり、**固定値ではない**のです。

### エラーになってしまう書き方 

試しに以下のように書いてみるとエラーになります。

```dart

enum ExternalLinks {
  /// Qiitaへの接続で、[mode]はアプリ **外** ブラウザで開く
  qiita._(LaunchMode.externalApplication, 'https://qiita.com/'),

  /// Zennへの接続で、[mode]はアプリ **内** ブラウザで開く
  zenn._(LaunchMode.inAppBrowserView, 'https://zenn.dev/'),

  /// Flavorによって接続先が変わり、[mode]はWebViewで開く
  changeLink._(
    LaunchMode.inAppWebView,
    _changeLinksUrl, // <= 🔥 ここでエラーになる　🔥
  );

  const ExternalLinks._(this.mode, this.url);

  final LaunchMode mode;
  final String url;

  /// Flavorによって変わるurlを返却する
  String get _changeLinksUrl {
    switch (FlavorConfig.appFlavor) {
      case Flavor.dev:
        return 'https://www.apple.com/jp/';
      case Flavor.stg:
        return 'https://developer.android.com/?hl=ja';
      case Flavor.prod:
        return 'https://www.microsoft.com/ja-jp/';
    }
  }
}

```

エラー文をみると
```
// エラー原文
Arguments of a constant creation must be constant expressions.
Try making the argument a valid constant, or use 'new' to call the constructor.

// 和訳
定数生成の引数は定数式でなければならない。
引数を有効な定数にするか、コンストラクタの呼び出しに 'new' を使用してください。
```

つまり、変動する値を入れることはできないのです。そこで出てくるのが`extension`です。


### `extension`で`getter`を定義する
```lib/external_links.dart

/// 動的な値を持たせたい場合は、extensionを使ってゲッターで受け取る。
extension ExternalLinksExtension on ExternalLinks {
  /// 接続先のurlを返す
  String get url {
    switch (this) {
      case ExternalLinks.qiita:
        return 'https://qiita.com/';
      case ExternalLinks.zenn:
        return 'https://zenn.dev/';
      // 直接分岐を書いても良いが、わかりづらいので今回はメソッドの戻り値で受け取る
      case ExternalLinks.changeLink:
        return _getChangeLinksUrl();
    }
  }

  /// Flavorによって変わるurlを返却する
  String _getChangeLinksUrl() {
    switch (FlavorConfig.appFlavor) {
      case Flavor.dev:
        return 'https://www.apple.com/jp/';
      case Flavor.stg:
        return 'https://developer.android.com/?hl=ja';
      case Flavor.prod:
        return 'https://www.microsoft.com/ja-jp/';
    }
  }
}

```

コメントにもある通り、動的な値を設定するには`extension`する必要があります。
本当は動的な値を持つことができない`enum`を**拡張**して、扱えるように改造しているのです。
少々紛らわしいですが上記の通りにすることで、`Flavor`によって返す値を変える値を受け取ることができます。

## 3. enumを活用して実装した使用例

### ブラウザを開くメソッドの定義
```lib/main.dart

  /// 引数のリンクによって開くWebサイト、ブラウザのモードを切り替える
  /// ただ、切り替えるのはあくまでExternalLinksで切り替えるようにし
  /// このメソッド内では指定されたものを使うだけにする
  Future<void> openBrowser(ExternalLinks link) async {
    final url = Uri.parse(link.url);
    try {
      if (await canLaunchUrl(url)) {
        await launchUrl(url, mode: link.mode);
      } else {
        logger.d('canLaunchUrlがfalseです');
      }
    } catch (e) {
      logger.e('エラーです', error: e);
    }
  }
  
```

### メソッドの呼び出し側
```lib/main.dart

 ElevatedButton(
              child: const Text('Qiitaを開く'),
              onPressed: () => openBrowser(ExternalLinks.qiita),
            ),

```

## 終わりに

過去のDartでは、`enum`にはフィールドも定義できなかったため、`extension`にて`getter`で定義していました。
現在のDartのバージョンではenumに直接フィールドを定義できるようになったので`extension`の必要がなくなったと思われがちです。（私はそう思っていました😇）
しかし、今回のように動的な値を扱う場合は`extension`を活用する必要があります。
言語の仕様を深く理解し適切に活用することで、より管理しやすいコードを書くことができます。

この記事に関するご指摘やご意見がございましたら、ぜひコメントにてお知らせください。
この記事が誰かのお役に立てれば幸いです。