Dagger2 / Lifecycle / LiveData

Muselee 16: Q Connectivity – Part 2

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.

Previously we looked at the deprecations in Android Q which are necessitating a change in how we detect network connectivity, and we create a nice compat class that would offer a compatible solution across different versions of Android. As a result of this the way that changes in network connectivity are detected is different. Previously we would periodically poll ConnectivityChecker whereas the new ConnectivityMonitor class that we created in the last article no longer supports synchronous checking, but utilises an asynchronous publish / subscribe pattern where interested parties register with ConnectivityMonitor to receive notifications of connectivity changes. This poses something of a problem in LastFmTopArtistsProvider where we check that we have network connectivity before attempting a network transaction:

class LastFmTopArtistsProvider(
    private val topArtistsApi: LastFmTopArtistsApi,
    private val connectivityChecker: ConnectivityChecker,
    private val mapper: DataMapper, List>
) : DataProvider {

    override fun requestData(): TopArtistsState {
        return if (!connectivityChecker.isConnected) {
            TopArtistsState.Error("No network connectivity")
        } else {
            val response = topArtistsApi.getTopArtists().execute()
            response.takeIf { it.isSuccessful }?.body()?.let { artists ->
                TopArtistsState.Success(mapper.encode(artists to response.expiry))
            } ?: TopArtistsState.Error(response.errorBody()?.string() ?: "Network Error")
        }
    }
    .
    .
    .
}

We cannot use ConnectivityMonitor in the same way as we use ConnectivityChecker so we need to consider how best to do this. I am actually going to remove this check altogether. When we execute the Retrofit request, it will use OkHttp to attempt to make the network connection over which the transaction will take place. For OkHttp to even be able to open a network socket for this, it will need to resolve the remote host name to an IP address and this DNS lookup will fail without network connectivity. Even if it has a locally cached dns lookup, the attempt to open a socket will fail. So either way the transaction will fail fast, and it’s perfectly safe to allow this because we handle error conditions in the above code snippet.

So if we’re not going to use ConnectivityChecker for this anymore, why do we even need ConnectivityMonitor? It make for a much nicer user experience if we can inform the user of possible network connectivity issues and the problem with performing our connectivity checks within the network component itself (even with the existing ConnectivityChecker implementation) is that we do not know there’s a problem until a network transaction attempt fails. The existing UI displays a Retry button if the network transaction fails for any reason, but only following the failure. It would actually be much nicer if we could detect connectivity issues as they arise rather that we a network transaction fails. The publish / subscribe pattern that ConnectivityMonitor employs actually makes that much easier. While we could subscribe and unsubscribe from ConnectivityMonitor updates in our TopArtistFragment lifecycle methods, there is a risk that we may subscribe in onCreate() and then forget to unsubscribe in onDestroy() resulting in a resource leak. A much better approach is to have an intermediate layer that is lifecycle aware, and capable of providing updates whenever state changes – that sounds very much like a job for LiveData!

Creating a LiveData wrapper around ConnectivityMonitor is pretty easy. We first declare an enum to represent connectivity state:

enum class ConnectivityState {
    Connected,
    Disconnected
}

Now we can create a really basic wrapper around ConnectivityMonitor:

class ConnectivityLiveData @Inject constructor(context: Context) :
    MutableLiveData() {

    private val connectionMonitor = ConnectivityMonitor.getInstance(context.applicationContext)

    override fun onActive() {
        super.onActive()
        connectionMonitor.startListening(::setConnected)
    }

    override fun onInactive() {
        connectionMonitor.stopListening()
        super.onInactive()
    }

    private fun setConnected(isConnected: Boolean) =
        postValue(if (isConnected) ConnectivityState.Connected else ConnectivityState.Disconnected)
}

The onActive() method registers for connectivity updates from ConnectivityMonitor and onInactive() unregisters. However the really important thing here is the user of the Application context when we get our ConnectivityMonitor instance. This ensures that we only ever hold a reference to the Application context and therefore cannot leak a Context. The Context that is passed in in the constructor, is not the same as the LifecycleOwner that will observe this LiveData object – it is just what is used internally by ConnectivityMonitor to detect network state changes, and the Application is more than suitable for this.

It is never a bad thing to provide multiple layers of protection against leaking Activity contexts, and we can really take a belt & braces approach with our Dagger 2 injection:

@Module
object ApplicationModule {

    @Provides
    @JvmStatic
    @Singleton
    internal fun provideApplication(app: MuseleeApplication): Application = app

    @Provides
    @JvmStatic
    @Singleton
    fun providesConnectivityManager(app: Application): ConnectivityManager =
        app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    @Provides
    @JvmStatic
    @Singleton
    fun providesConnectivityLiveData(app: Application) =
        ConnectivityLiveData(app)

}

Here we ensure that an Application context is always used to create the singleton instance of ConnectivityLiveData which is injected wherever it is needed. Any Activity, Fragment, Service, or other lifecycle component can then observe the ConnectivityLiveData by getting the singleton instance injected.

In the final article where we focus on connectivity changes we’ll look at how to wire this up within TopArtistsFragment and also look at another cool new feature offered in Android Q.

Although this is not yet wired up and working, the source code for this article is available 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.

Leave a 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.