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.