【Flutter】Androidでhttp通信を許可する方法

## はじめに

開発の段階ではdev環境のサーバーを用意して、そこに対してAPIを投げて通信するのが一般的だと思います。
しかし今の現場でバックエンドとフロントエンドで繋ぎこみを行う際に、
まずはフロントエンドのローカル環境で試して成功を確認→サーバーにデプロイすることとなりました。
この時にhttp通信となるのですが、Androidでは次のエラーが出て上手くいきませんでした。

```
Error: SocketException: Connection refused (OS Error: Connection refused, 
errno = 111), address = localhost, port = 52604
```
結果的にこのエラーを解消して無事にhttp通信を許可させることができましたので、
その方法をまとめてみたいと思います。

### 筆者の経歴

- UIKitでのiOSアプリケーション開発の学習9ヶ月
- SwiftUIでのiOSアプリケーション開発学習４ヶ月
- Flutterでのモバイルアプリケーション開発３ヶ月

### 記事の対象者

- Androidでhttp通信ができずに悩まれている方

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

- macOS 14.3.1
- Xcode 15.2
- Swift 5.9
- iPhone11 pro ⇒ iOS 17.2.1
- Flutter 3.19.0
- Dart 3.3.0
- Pixel 7a ⇒ Android 14

## 1. 実装の概要

ローカル環境でhttp通信を行いAPIの接続テストを行う場合には設定が必要でした。
iOSは設定しないでも繋がりました。（もしかしたら現場の先輩が設定しているのかも？）
Androidでは対応が必要です。

以下がその対応内容です。

1. エミュレーターのwifi設定からゲートウェイの値を確認
2. Androidマニフェストに追記
3. `network_security_config.xml` の作成
4. httpクライアントの接続先をOSごとに分岐

## 2. エミュレーターのwifi設定からゲートウェイの値を確認

エミュレーターの設定アプリ >> インターネット >> ネットワークとインターネット

![Androidでhttp通信を許可する方法 .001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/c52bc055-33c0-13fb-4970-dc1f2d95a3e8.png)


 AndroidWifi >> 画面を下にスクロールして**ゲートウェイ**の値を控えておきます。
おそらくここの値はどのエミュレーターでも`10.0.2.2`で固定の数値だと思います。
違かった場合でもその値を使えばいいので大丈夫です。

![Androidでhttp通信を許可する方法 .002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/bfadee77-fe17-c1f6-c179-567763ba5b57.png)

## 3. Androidマニフェストに追記

今回はFlavorなどで環境を分けている前提で書きます。
あくまで開発する時だけhttp通信を許可したいだけなので、
mainの方のマニフェストではなく**devのAndroidマニフェスト**に書きます。

![スクリーンショット 2024-04-30 22.48.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/97d9e309-21b6-0b30-816f-c75c92eb741b.png)

追記するのは`<application android:networkSecurityConfig="@xml/network_security_config">`の部分です。
```AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 以下を追加 -->
	<application android:networkSecurityConfig="@xml/network_security_config">
  </application>
</manifest>
```

## 4. `network_security_config.xml` の作成
resディレクトリの配下にxmlディレクトリを作ります。
そしてその中に`network_security_config.xml`ファイル を作ります。
![スクリーンショット 2024-04-30 22.59.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/ade804d8-e926-ee90-956b-9e07d92760d7.png)

```network_security_config.xml
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>
```

## 5. httpクライアントの接続先を分岐させる
flavorのdevで開発していてもさらにサーバーはdevサーバーなのか、ローカル環境のサーバーなのかで切り替えなければいけません。
ここがちょっとややこしいですね💦

flavorでは以下の３つの環境を作ります。
- dev => デベロップ、開発環境
- stg => ステージング、テストで配布する時
- prot => プロダクト、製品版、本番環境

なので、通信してデータを保存したりとってきたりするサーバーも上記のように分けておいきます。
そして各環境によって接続する先を別々に紐づけておくことで、開発中に誤ってユーザーが使っている正規のサーバーのデータを書き換えるリスクを防止したりします。

ただ今回はさらにローカル環境に繋げなければいけません。

ローカル環境をざっくりいうと、dockerを使って自分のPCにサーバーを作ることです。
大抵はバックエンドの方が作ったソースコードをGithubなどから自分のPCに落としてきて
それをdockerで実行して構築する流れです。

まとめると、flavorのdevモードの時にサーバー通信とローカル通信で分けておきたいということです。

:::note info
今回はflavorの各種設定が終わってる前提で話しています。
flavorによる開発環境と本番環境を分けるための設定方法は割愛します。
:::

### 5-1.DartDefineを使って環境変数を作る
まずflavorは一旦おいておいて、ローカル通信のフラグを設定します。
```define.dart
class Define {
  const Define({required this.isLocal});

  static Define instance = const Define(
    isLocal: bool.fromEnvironment('isLocal'),
  );

  final bool isLocal;
}
```

### 5-2. launch.jsonに設定を追加
先に述べたようにflavorのdevモードの時にローカル通信を行うモードを
`debug-dev-isLocal`として追記します。

```launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "debug-dev",
            "request": "launch",
            "type": "dart",
            "program": "lib/main_dev.dart",
            "env": {
                "FLAVOR": "dev"
            },
            "args": [
                "--debug",
                "--flavor",
                "dev",
                "--dart-define=FLAVOR=dev",
            ],
        },
        ///
        /// ここから追加
        ///
        {
            "name": "debug-dev-isLocal",
            "request": "launch",
            "type": "dart",
            "program": "lib/main_dev.dart",
            "env": {
                "FLAVOR": "dev"
            },
            "args": [
                "--debug",
                "--flavor",
                "dev",
                "--dart-define=FLAVOR=dev",
                "--dart-define=isLocal=true",
            ],
        },
        ///
        /// ここまで
        ///
        {
            "name": "debug-stg",
            "request": "launch",
            "type": "dart",
            "program": "lib/main_stg.dart",
            "env": {
                "FLAVOR": "stg"
            },
            "args": [
                "--debug",
                "--flavor",
                "stg",
                "--dart-define=FLAVOR=stg",
            ],
        },
        {
            "name": "debug-prod",
            "request": "launch",
            "type": "dart",
            "program": "lib/main_prod.dart",
            "env": {
                "FLAVOR": "prod"
            },
            "args": [
                "--debug",
                "--flavor",
                "prod",
                "--dart-define=FLAVOR=prod",
            ],
        },
    ]
}
```


### 5-3. httpクライアントで通信する際の接続先を分ける
今回は`riverpod`を使ってhttpクライアントのパッケージである`dio`のインスタンスを渡す設計になってます。

以下のコード内のコメントのようにAPIリクエストを送信する接続先を３つのパターンで分けています。
1. ローカル通信でiOSの場合
2. ローカル通信のAndroidの場合
3. それ以外、すなわちオンラインのサーバーに接続する場合でOSは問わない

ここで気になるのはローカル通信でOSごとに接続先を変えていることです。
本来はローカルで一つのパターンにしたいところです。
しかしAndroidに限ってはwifi接続の出入り口を指定しないとローカルにつながってくれません。

```http.dart
@Riverpod(keepAlive: true)
Dio http(HttpRef ref) {
    // switchの条件はサーバーがローカルか否かとOSがiOSか否か（否はAndroidを想定） 
  final baseUrl = switch ((Define.instance.isLocal, Platform.isIOS)) {
	  // ローカル通信のiOS
    (true, true) => 'http://localhost',
    // ローカル通信のAndroid
    // エミュレーターのゲートウェイを xx.x.x.x に入力
    (true, false) => 'http://xx.x.x.x',
    // 通常の接続先
    _ => 'https://hogehoge-api',
  };
  return Dio(
    BaseOptions(
      baseUrl: baseUrl,
      listFormat: ListFormat.csv,
    ),
  ),
}
```
ここでAndroidの場合の接続先`xx.x.x.x`の部分は
「2. エミュレーターのwifi設定からゲートウェイの値を確認」で控えておいた**ゲートウェイ**の値を指定します。

## 6. 完了

お疲れ様でした。
あとはデバック実行する際にモードを`debug-dev-isLocal`にすればAndroidのエミュレータでhttp通信を行うことが可能です。

## 終わりに

いかがだったでしょうか。
今回の実装を行う上でさまざまな記事を見たのですがなかなか上手くできませんでした。
最終的には先輩に教えて頂いた記事を参考になんとか実装することができました。
初学者にとってはなかなか内容が小難しいのですが、理解してしまえば割と簡単に実装できます。

この記事が誰かのお役に立てれば幸いです。

## 参考記事

https://backport.net/blog/2018/12/27/how_to_allow_http_on_android_9/