ConnectivityManager / Muselee / NetworkCallback

Muselee 16: Q Connectivity – Part 1

Muselee is a demo app which allows the user to browse popular music artists. It is not intended to be a fully-featured user app, but a vehicle to explore good app architecture, how to implement current best-practice, and explore how the two often go hand in hand. Moreover it will be used to explore how implementing some specific patterns can help to keep our app both maintainable, and easy to extend.

There are some changes in the recently released (at the time of writing) Android Q developer preview which affect Muselee somewhat. There are changes to how we should be determining network connectivity which means that the ConnectivityChecker class (which we declared in the core module) now uses some methods that have been deprecated in Android Q. In this post we’ll explore how we can overcome this, and look at how we should detect connectivity status.

The changes in Android Q are nothing unexpected, and the new way of doing determining connectivity status first appeared in API 21 (Lollipop), although there have been some additions since then. Moreover, in API 24 (Nougat) some restrictions were imposed on the use of which apps would receive CONNECTIVITY_ACTION broadcasts. Moreover CONNECTIVITY_ACTION itself was deprecated altogether in API 28 (Pie). So we can no longer use this mechanism to determine the network status. Furthermore, in Android Q NetworkInfo has been deprecated, and it is this which affects ConnectivityChecker:

class ConnectivityChecker @Inject constructor(private val connectivityManager: ConnectivityManager) {
 
    val isConnected: Boolean
        get() = connectivityManager.activeNetworkInfo?.isConnected ?: false
 
}

Both the getActivieNetworkInfo() method, and the NetworkInfo class that it returns have been deprecated. While this will still work, it is really not a good idea to rely on deprecated methods and classes in production code, although there are strict exceptions, which we’ll consider later. Instead of relying on these mechanisms, it is far better to switch the the recommended techniques which, in this case, is to use NetworkCallback instead:

class ConnectivityMonitor(private val connectivityManager: ConnectivityManager) {

    private var callbackFunction: ((Boolean) -> Unit) = {}

    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            super.onAvailable(network)
            callbackFunction(true)
        }

        override fun onLost(network: Network) {
            super.onLost(network)
            callbackFunction(false)
        }
    }

    fun startListening(callback: (Boolean) -> Unit) {
        callbackFunction = callback
        callbackFunction(false)
        connectivityManager.registerDefaultNetworkCallback(networkCallback)
    }

    fun stopListening() {
        connectivityManager.unregisterNetworkCallback(networkCallback)
        callbackFunction = {}
    }
}

This class demonstrates how we can register a NetworkCallback instance which will monitor the status of the default network connection. Whenever it changes we will get either an onAvailable() or onLost()callback notifying us of the new state. The startListening() and stopListening() methods are called by a consumer of the network state to register a callback function. These perform the registration and un-registration of the NetworkCallback instance.

One important thing to understand about this behaviour is that if there is network connectivity when we register the NetworkCallback then we will immediately receive an onAvailable() callback indicating the current network that is connected. However, if there is not connectivity, then we will not receive any callback. For this reason, it is necessary to make an initial callback to the consumer in startListening() to indicate a disconnected state. If we are in fact connected, then the onAvailable() callback will be triggered and we will make a second callback to the consumer to indicate a connected state; However, if we are not connected, then no further callback will be made until the connectivity changes. Hence we always properly inform the consumer.

So this may appear to be pretty straightforward, but there is one small problem with this because the registerDefaultNetworkCallback() method was introduced in API 24, and Muselee has minSdkVersion = 21. While it would be possible to re-engineer it to only use API 21 APIs, instead I have elected to provide a backwardly compatible version that would work if we had to support earlier API levels than 21. My reasoning for this is purely to explain the techniques for doing this in a blog post, and this approach offers little tangibly benefit to Muselee specifically.

The trick to use here is a Kotlin sealed class which contains two concrete implementation classes, one for the Nougat and later version (which we just saw an example of, and the second which uses the more traditional BoradcastReceiver pattern to handle CONNECTIVITY_ACTION events:

internal sealed class ConnectivityMonitor(
    protected val connectivityManager: ConnectivityManager
) {

    protected var callbackFunction: ((Boolean) -> Unit) = {}

    abstract fun startListening(callback: (Boolean) -> Unit)
    abstract fun stopListening()

    @TargetApi(Build.VERSION_CODES.N)
    private class NougatConnectivityMonitor(connectivityManager: ConnectivityManager) : 
        ConnectivityMonitor(connectivityManager) {

        private val networkCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                super.onAvailable(network)
                callbackFunction(true)
            }

            override fun onLost(network: Network) {
                super.onLost(network)
                callbackFunction(false)
            }
        }

        override fun startListening(callback: (Boolean) -> Unit) {
            callbackFunction = callback
            callbackFunction(false)
            connectivityManager.registerDefaultNetworkCallback(networkCallback)
        }

        override fun stopListening() {
            connectivityManager.unregisterNetworkCallback(networkCallback)
            callbackFunction = {}
        }
    }

    @Suppress("Deprecation")
    private class LegacyConnectivityMonitor(
        private val context: Context,
        connectivityManager: ConnectivityManager
    ) : ConnectivityMonitor(connectivityManager) {

        private val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)

        private val isNetworkConnected: Boolean
            get() = connectivityManager.activeNetworkInfo?.isConnected == true

        override fun startListening(callback: (Boolean) -> Unit) {
            callbackFunction = callback
            callbackFunction(isNetworkConnected)
            context.registerReceiver(receiver, filter)
        }

        override fun stopListening() {
            context.unregisterReceiver(receiver)
            callbackFunction = {}
        }

        private val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                callbackFunction(isNetworkConnected)
            }
        }
    }

    companion object {
        fun getInstance(context: Context): ConnectivityMonitor {
            val connectivityManager =
                context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                NougatConnectivityMonitor(connectivityManager)
            } else {
                LegacyConnectivityMonitor(context, connectivityManager)
            }
        }
    }
}

Important things to note are that both concrete implementations are private classes meaning that they cannot be instantiated outside of the sealed class itself. The only way to create an instance of wither of these classes is using the getInstance() static method which is declared in companion object of the sealed class ConnectivityMonitor.

The other thing worthy of explanation is that this new API differs from ConnectivityChecker in one fundamental way: ConnectivityChecker has a single method which is invoked synchronously whereas ConnectivityMonitor is used asynchronously. That is dictated by how the new NetworkCallback-based approach operates, and means that we’ll have to change the way that Muselee deals with connectivity changes. We’ll look at that in the next article.

Although this isn’t yet wired in, the source code for this article can be found here.

© 2019, Mark Allison. All rights reserved.

Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

3 Comments

  1. Nice article as always.
    One comment: there are some formatting issues. Instead of -> in lambdas you have ->
    For example: override fun startListening(callback: (Boolean) -> Unit) {

Leave a Reply to Oleg Osipenko Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.