38
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

AndroidAdvent Calendar 2020

Day 10

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

Last updated at Posted at 2020-12-09

はじめに

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

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

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

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

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

実装

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

ConnectivityWatcher

役割

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

ConnectivityStatus

役割

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

コード

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)
                }
            }
    }
}
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のメソッドはいくつかありますが、ここではonAvailableonLostだけオーバーライドしています。
この2つのメソッドだけでも、ネットワークが有効になった・切断されたというイベントは通知できます。
(全てのメソッドはコチラ

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

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

2. NetworkRequest.Builder()

その通りNetworkRequestをビルダーを使って生成していきます。
ここでaddCapability(...)メソッドを使って、何のネットワークの状態をコールバックで通知するのか指定していきます。
ここではインターネット接続に関するネットワークの状態を通知して欲しいので、NetworkCapabilities.NET_CAPABILITY_INTERNETを追加しました。
API LEVEL 23 以上の時はNET_CAPABILITY_VALIDATEDも追加しておきます。
(より詳しい説明はコチラ

3. awaitClose

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

4. shareIn(...)

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

5. getNetworkCapabilities()

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

公式ドキュメントだと、connectivityManager.activeNetworkInfoを使っていますが、ここではconnectivityManager.allNetworksを使っています。
そのようにした理由は、切断したはずのネットワークが返ってきてしまうことがあるからです。
参考:Android 10 時代の Connectivity Monitoring

ConnectivityStatus

1. isEnabled()

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

2. isWiFiEnabled()

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

3. isCellularEnabled()

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

利用例

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

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

まとめ

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

コード全文

参考リンク

  1. Android 10 時代の Connectivity Monitoring
38
17
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?