はじめに
アプリ開発をしていると、インターネット接続のイベントを取得したい場合があります。
イベントの例
- インターネット接続が切断されたら、
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
のメソッドはいくつかありますが、ここではonAvailable
とonLost
だけオーバーライドしています。
この2つのメソッドだけでも、ネットワークが有効になった・切断されたというイベントは通知できます。
(全てのメソッドはコチラ)
onAvailable
とonLost
では同じ処理をしていて、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
を登録・削除する処理を利用元が意識しなくていい - 任意のコルーチンスコープで通知を受け取ることできる