DroidBee
DroidBee

Reputation: 115

Where should i place the code to observe internet connection so that the user is notified if the device is online or offline?

I have the code to monitor if internet is available. It returns a LiveData and it is observed in the MainActivity . The code is given below.

  override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding=DataBindingUtil.setContentView(this,R.layout.activity_main)

        NetworkStatusHelper(this@MainActivity).observe(this, Observer {
            when(it){
                NetworkStatus.Available-> Snackbar.make(binding.root, "Back online", Snackbar.LENGTH_LONG).show()
                NetworkStatus.Unavailable-> Snackbar.make(binding.root, "No Internet connection", Snackbar.LENGTH_LONG).show()
            }
        })
    }

NetworkHelper

package com.todo.utils.networkhelper

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.util.Log
import androidx.lifecycle.LiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket

class NetworkStatusHelper(private val context: Context): LiveData<NetworkStatus>() {

    var connectivityManager: ConnectivityManager =
        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    private lateinit var connectivityManagerCallback: ConnectivityManager.NetworkCallback
    val validNetworkConnections: ArrayList<Network> = ArrayList()


    fun getConnectivityCallbacks() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        object : ConnectivityManager.NetworkCallback() {


            override fun onAvailable(network: Network) {

                super.onAvailable(network)
                val networkCapability =
                    connectivityManager.getNetworkCapabilities(network)
                val hasNetworkConnection =
                    networkCapability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                        ?: false

                if (hasNetworkConnection) {
                    determineInternetAccess(network)
                }
            }

            override fun onLost(network: Network) {
                super.onLost(network)
                validNetworkConnections.remove(network)
                announceNetworkStatus()
            }

//            override fun onCapabilitiesChanged(
//                network: Network,
//                networkCapabilities: NetworkCapabilities
//            ) {
//                super.onCapabilitiesChanged(network, networkCapabilities)
//
//                Log.d("validNetworkConnection","onCapabilitiesChanged size "+validNetworkConnections.size)
//
//
//                if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
//                   determineInternetAccess(network)
//                } else {
//                    validNetworkConnections.remove(network)
//                }
//                announceNetworkStatus()
//            }

            private fun determineInternetAccess(network: Network) {
                
                CoroutineScope(Dispatchers.IO).launch {
                    if (InternetAvailability.check()) {
                        withContext(Dispatchers.Main) {
                            validNetworkConnections.add(network)
                            
                            announceNetworkStatus()
                        }
                    }
                }
            }


            fun announceNetworkStatus() {
                
                if (validNetworkConnections.isNotEmpty()) {
                    postValue(NetworkStatus.Available)
                } else {
                    postValue(NetworkStatus.Unavailable)
                }
            }

        }
    } else {
        TODO("VERSION.SDK_INT < LOLLIPOP")
    }


    override fun onActive() {
        super.onActive()
        connectivityManagerCallback = getConnectivityCallbacks()
        val networkRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            NetworkRequest
                .Builder()
                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .build()
        } else {
            TODO("VERSION.SDK_INT < LOLLIPOP")
        }
        connectivityManager.registerNetworkCallback(networkRequest, connectivityManagerCallback)
    }


    override fun onInactive() {
        super.onInactive()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            connectivityManager.unregisterNetworkCallback(connectivityManagerCallback)
        }
    }

    object InternetAvailability {

        fun check() : Boolean {
            return try {
                val socket = Socket()
                socket.connect(InetSocketAddress("8.8.8.8",53))
                socket.close()
                true
            } catch ( e: Exception){
                e.printStackTrace()
                false
            }
        }

    }
}



The problem is here is , the Snackbar is displayed even when the app is opened for the first time .I don't want the Snackbar to be displayed when the app is opened for the first time when network is available. If network is unavailable, then the Snackbar should be displayed even when the app is opened for the first time.

Can someone help to improve the code with correct logic to implement the same.

Upvotes: 0

Views: 1726

Answers (1)

Tenfour04
Tenfour04

Reputation: 93789

If your helper class is a Flow, then you can use Flow operators to easily customize its behavior. You should keep the instance of your helper class in a ViewModel so it can maintain its state when there are configuration changes.

Here's a Flow version of your class's functionality. I actually just made it into a function, because I think that's simpler.

I removed the List<Network> but you can add it back in if you think it's necessary. I don't think it makes sense to keep a List that can only ever hold at most one item. A device cannot have multiple simultaneous network connections. If you do need it, it won't work for pre-Lollipop, so you will have to juggle differing functionality and probably do need a class instead of just a function.

I think you can probably remove the checkAvailability() function as it is redundant, but I put it in because you have it.

I added a pre-Lollipop version based on a broadcast receiver, since you seem to want to add support for that.

@get:RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
val Context.networkStatus: Flow<NetworkStatus> get() = when {
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> getNetworkStatusLollipop(this)
    else -> getNetworkStatusPreLollipop(this)
}

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
private fun getNetworkStatusLollipop(context: Context): Flow<NetworkStatus> = callbackFlow {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    val callback = object : ConnectivityManager.NetworkCallback() {
        private var availabilityCheckJob: Job? = null

        override fun onUnavailable() {
            availabilityCheckJob?.cancel()
            trySend(NetworkStatus.Unavailable)
        }

        override fun onAvailable(network: Network) {
            availabilityCheckJob = launch {
                send(if(checkAvailability()) NetworkStatus.Available else NetworkStatus.Unavailable)
            }
        }

        override fun onLost(network: Network) {
            availabilityCheckJob?.cancel()
            trySend(NetworkStatus.Unavailable)
        }
    }

    val request = NetworkRequest.Builder()
        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
        .build()
    connectivityManager.registerNetworkCallback(request, callback)

    awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}

@RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
private fun getNetworkStatusPreLollipop(context: Context): Flow<NetworkStatus> = callbackFlow {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    val receiver = object: BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            launch {
                if (connectivityManager.activeNetworkInfo?.isConnectedOrConnecting == true) {
                    send(if(checkAvailability()) NetworkStatus.Available else NetworkStatus.Unavailable)
                } else {
                    send(NetworkStatus.Unavailable)
                }
            }
        }
    }

    context.registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))

    awaitClose { context.unregisterReceiver(receiver) }
}

private suspend fun checkAvailability() : Boolean = withContext(Dispatchers.IO) {
    try {
        Socket().use {
            it.connect(InetSocketAddress("8.8.8.8", 53))
        }
        true
    } catch (e: Exception){
        e.printStackTrace()
        false
    }
}

Then in your ViewModel, you can use Flow operators to easily expose a Flow that skips initial NetworkStatus.Available values:

class MyViewModel(application: Application): AndroidViewModel(application) {

    val changedNetworkStatus = application.context.networkStatus
        .dropWhile { it == NetworkStatus.Available } // ignore initial available status
        .shareIn(viewModelScope, SharingStarted.Eagerly, 1) // or .asLiveData() if preferred
}

Upvotes: 0

Related Questions