callbackFlowでインターネット接続の有無を通知する

# はじめに
アプリ開発をしていると、インターネット接続のイベントを取得したい場合があります。

##### イベントの例
- インターネット接続が切断されたら、`Toast`でユーザに通知する
-  WiFi通信からモバイル通信に切り替わったら、ダウンロードを中止する

Androidでは`ConnectivityManager.NetworkCallback`を使えば、ネットワーク状態をコールバックで通知してくれます。
しかし、コールバックのまま使うよりも`callbackFlow`でラップするように実装した方が便利です。

便利な点は以下の通りです。

- `callbackFlow`の利用側で、`ConnectivityManager.NetworkCallback`を削除する処理を書かなくて良い。
- `lifecycleScope.launchWhenResumed` のようなスコープを指定できる

# 実装

2つのクラスを作りました

### ConnectivityWatcher

役割

-  `ConnectivityManager.NetworkCallback` を登録
-  コールバックを`callbackFlow(SharedFlow)`でラップ

### ConnectivityStatus

役割

- インターネット接続の有無をチェック
- WiFiのインターネット接続の有無をチェック
- モバイルデータのインターネット接続の有無をチェック

# コード

```kotlin
class ConnectivityWatcher(private val context: Context) {

    private val connectivityManager: ConnectivityManager
        get() = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    val status = callbackFlow<ConnectivityStatus> {
        // 1.
        val networkCallback = object : ConnectivityManager.NetworkCallback() {

            override fun onAvailable(network: Network) {
                offer(ConnectivityStatus(getNetworkCapabilities()))
            }

            override fun onLost(network: Network) {
                offer(ConnectivityStatus(getNetworkCapabilities()))
            }
        }

        // 2.
        val builder = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // API LEVEL 23以上ならNetworkCapabilities.NET_CAPABILITY_VALIDATEDも指定
            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
        }
        connectivityManager.registerNetworkCallback(builder.build(), networkCallback)

        // 3.
        awaitClose {
            connectivityManager.unregisterNetworkCallback(networkCallback)
        }
    }.shareIn( // 4.
        ProcessLifecycleOwner.get().lifecycleScope,
        SharingStarted.WhileSubscribed(),
        1
    )

    // 5.
    private fun getNetworkCapabilities(): List<NetworkCapabilities> {
        return connectivityManager.allNetworks
            .mapNotNull { network ->
                connectivityManager.getNetworkCapabilities(network)
            }
            .filter {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    it.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
                            it.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
                } else {
                    it.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                }
            }
    }
}
```

```kotlin
class ConnectivityStatus(private val networkCapabilities: List<NetworkCapabilities>) {

    /**
     * @return インターネット接続があるか
     */
    // 1.
    fun isEnabled(): Boolean {
        return networkCapabilities.isNotEmpty()
    }

    /**
     * @return WiFiのインターネット接続があるか
     */
    // 2.
    fun isWiFiEnabled(): Boolean {
        return networkCapabilities.any {
            it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
        }
    }

    /**
     * @return モバイルデータのインターネット接続があるか
     */
    // 3.
    fun isCellularEnabled(): Boolean {
        return networkCapabilities.any {
            it.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
        }
    }
}
```

# 説明

## ConnectivityWatcher

### 1. ConnectivityManager.NetworkCallback のオブジェクト式

`ConnectivityManager.NetworkCallback`のメソッドはいくつかありますが、ここでは`onAvailable`と`onLost`だけオーバーライドしています。
この2つのメソッドだけでも、ネットワークが有効になった・切断されたというイベントは通知できます。
（全てのメソッドは[コチラ](https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback)）

`onAvailable`と`onLost`では同じ処理をしていて、`offer(ConnectivityStatus(getNetworkCapabilities()))`が実行されています。

つまりは、ネットワークが有効になった・切断された時に、受信元に`ConnectivityStatus`を送るという処理だけやっています。

### 2. NetworkRequest.Builder()

その通り`NetworkRequest`をビルダーを使って生成していきます。
ここで`addCapability(...)`メソッドを使って、何のネットワークの状態をコールバックで通知するのか指定していきます。
ここではインターネット接続に関するネットワークの状態を通知して欲しいので、`NetworkCapabilities.NET_CAPABILITY_INTERNET`を追加しました。
API LEVEL 23 以上の時は`NET_CAPABILITY_VALIDATED`も追加しておきます。
（より詳しい説明は[コチラ](https://developer.android.com/training/basics/network-ops/reading-network-state#introducing-net-capabilities
)）

### 3. awaitClose

コールバックの後処理をします。
`awaitClose`内に後処理を書いておくと、利用元で受信されなくなった時に後処理が実行されます。
（🚀利用元でコールバックを削除することを意識しなくていい）

### 4. shareIn(...)

この`callbackFlow`をただの`Flow`ではなく、`SharedFlow`として扱います。
いくつかの場所で同じ`callbackFlow`を受信している場合に、コールバックが複数登録されないようにしておきます。

### 5. getNetworkCapabilities()

全てのネットワークを取得して、その中からインターネット接続に関するネットワークだけ取り出します。

[公式ドキュメント](https://developer.android.com/training/basics/network-ops/connecting#intro-net-ops)だと、`connectivityManager.activeNetworkInfo`を使っていますが、ここでは`connectivityManager.allNetworks`を使っています。
そのようにした理由は、切断したはずのネットワークが返ってきてしまうことがあるからです。
参考：[Android 10 時代の Connectivity Monitoring](https://takasfz.hatenablog.com/entry/2019/07/04/191744)


## ConnectivityStatus

### 1. isEnabled()

コンストラクタ引数の`networkCapabilities`にはインターネット接続に関するネットワークのリストが入っています。
このリストが空の場合、インターネット接続がないと判断しています。

### 2. isWiFiEnabled()

コンストラクタ引数の`networkCapabilities`の中に、WiFIのネットワークがあるかチェックしています。

### 3. isCellularEnabled()

コンストラクタ引数の`networkCapabilities`の中に、モバイルデータのネットワークがあるかチェックしています。

# 利用例

インターネット接続が切れたら`Toast`などを表示する場合は、`lifecycleScope.launchWhenResumed`などのライフサイクルスコープで`collect`するといいと思います。

```kotlin
    lifecycleScope.launchWhenResumed {
        watcher.status.collect { status ->
            if (!status.isEnabled()) {
                showToast("No Internet connection")
            }
        }
    }
```


# まとめ

- callbackFlow(SharedFlow)でインターネット接続の有無を通知することができた
- `ConnectivityManager.NetworkCallback`を登録・削除する処理を利用元が意識しなくていい
- 任意のコルーチンスコープで通知を受け取ることできる

# コード全文
- https://github.com/Takahana/AndroidConnectivityWatcher


# 参考リンク
1. [Android 10 時代の Connectivity Monitoring](https://takasfz.hatenablog.com/entry/2019/07/04/191744)