Architecture / Architecture Components / Lifecycle / LiveData

Maintainable Architecture – Lifecycle

Creating a maintainable, flexible codebase is not easy but is an essential part of software engineering. In this series we’ll take a look at a simple, functional weather app and look at some of the issues in its design. We shall then refactor and re-design it to create a codebase which will be easier to maintain, less prone to bugs, and easier to add features to. This series is not going to be a deep dive in to the techniques and technologies that we’re going to use, but will be more an exploration of what benefits they give us. In this article we’ll look at making Android lifecycles easier to manage.

We’ve tidied our app up quite a lot, but there are still some issues. Firstly the handling of configuration changes, such as device rotation, is not great; and secondly we are responsible for unsubscribing from location updates within our CurrentWeatherFragment.

The unsubscribe from the LocationProvider currently happens in the onPause() method of CurrentWeatherFragment and this is ultimately the cause of the issue.

While the implementation of OpenWeatherMapProvider goes some way to mitigating configuration changes because of the use of an OkHttp cache to reduce the cost of repeated calls to the same API URL, we can still improve things. There is the possibility that a configuration change may occur while a network request is in flight. When this happens, the Retrofit transaction will be cancelled as the current CurrentWeatherFragment is paused prior to it being destroyed, and a new one will be created and run when the new CurrentWeatherFragment instance is created then resumed. This is clearly inefficient.

Another issue is that it is the responsibility of CurrentWeatherFragment to unsubscribe from LocationProvider once it is finished with it. while in a small app such as WeatherStation, there is only a single consumer of LocationProvider. However, is a much larger codebase with many more consumers, there is always the risk that one consumer may not unsubscribe which may result in either a crash (as the provider makes a callback to a consumer which has been GC’d), or we may continue getting location updates after we no longer require them, which may be bad for battery life.

Further to this: If we actually think about it CurrentWeatherFragment actually does not need the location information at all. All it consumes is the CurrentWeather data from CurrentWeatherProvider, and it is actually CurrentWeatherProvider which requires the location updates. So we should actually remove any knowledge of LocationProvider from CurrentWeatherFragment, which is difficult because we need to tie it to the lifecycle of the Fragment to properly unsubscribe at the correct lifecycle event.

While these don’t seem like major issues, they can quickly become so as our codebase grows so it makes sense to get this right. Android Architecture Components to the rescue. Those already familiar with them will be aware that we can use LiveData to respond to appropriate lifecycle events, and ViewModel to retain LiveData (and other data) across configuration changes. So in this case we can use a LiveData instance to be the consumer of both our LocationProvider and CurrentWeatherProvider:

class CurrentWeatherLiveData @Inject constructor(
        private val locationProvider: LocationProvider,
        private val currentWeatherProvider: CurrentWeatherProvider
) : LiveData() {

    override fun onActive() {
        super.onActive()
        locationProvider.requestUpdates(::updateLocation)
    }

    private fun updateLocation(latitude: Double, longitude: Double) {
        currentWeatherProvider.request(latitude, longitude) { current ->
            postValue(current)
        }
    }

    override fun onInactive() {
        currentWeatherProvider.cancel()
        locationProvider.cancelUpdates(::updateLocation)
        super.onInactive()
    }
}

This subscribes and unsubscribes to location updates at appropriate lifecycle events, and will retrieve the current weather data whenever the location is updated. If our CurrentWeatherFragment observes this, it no longer has a direct dependency on the LocationProvider, so we have removed the responsibility for unsubscribing from it, and that responsibility is now handled, and much more easily managed by CurrentWeatherLiveData – any LifecycleOwner can observe it, and we can be confident that we’ll unsubscribe from location updates at the appropriate time. CurrentWeatherFragment will also now completely agnostic of LocationProvider, which gives us further separation of concerns.

We now need a ViewModel to properly manage this and enable a CurrentWeatherLiveData instance to persist across a configuration change:

class CurrentWeatherViewModel @Inject constructor(val currentWeather: LiveData) : ViewModel()

The ViewModel can’t get an awful lot simpler than that, but now comes the trickier bit: Normally we would create this by using ViewModelProviders.of(this).get(CurrentWeatherViewModel.class) within our Fragment, but this requires us to have a no-arg constructor for CurrentWeatherViewModel. However, the @Inject annotation on the constructor of CurrentWeatherViewModel hints at how we can get around this – by using Dagger to instantiate our CurrentWeatherViewModel instance for us. But this is a little more complex than anything we’ve looked at before.

The problem that we have here is that we do not necessarily want a Singleton as we have with the location provider, because it would be wasteful to have our CurrentWeatherViewModel hanging around forever. Neither do we want to create it on-demand because it is actually the responsibility of the architecture components library (specifically the ViewModelProvider) to manage the lifespan of a ViewModel instance to ensure that it properly straddles a configuration change, but gets GC’d once it’s no longer needed. What we have to do is provide a mechanism whereby the architecture components library can request an instance of CurrentWeatherViewModel when it’s needed, and we do this by creating a ViewModelProvider.Factory which is responsible for creating these instances:

@Singleton
class ViewModelFactory @Inject constructor(
        private val viewModelProviders: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val provider = viewModelProviders[modelClass]
                ?: viewModelProviders.entries.first { modelClass.isAssignableFrom(it.key) }.value

        return provider.get() as T
    }
}

This will be constructed by Dagger with a map of Class to Provider instance. The Class represents the object class that will be created, and the Provider is a Dagger Provider instance which will create an instance of the required object. The create() method will find a Provider for the requested class, and then call its get() method to obtain an instance of the class.

We need to tell Dagger how to create both the Class -> Provider Map, and the Provider itself, and we do this in a Dagger module:

@Target(
        AnnotationTarget.FUNCTION,
        AnnotationTarget.PROPERTY_GETTER,
        AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

@Module
abstract class ViewModelModule {

    @Binds
    abstract fun bindLiveData(currentWeatherLiveData: CurrentWeatherLiveData): LiveData<CurrentWeather>

    @Binds
    @IntoMap
    @ViewModelKey(CurrentWeatherViewModel::class)
    abstract fun bindCurrentWeatherViewModel(viewModel: CurrentWeatherViewModel) : ViewModel

    @Binds
    abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}

The providesLiveData() method is required for the constructor injection of CurrentWeatherViewModel. In the earlier declaration of CurrentWeatherLiveData we also had constructor injection of LocationProvider and CurrentWeatherProvider instances. We have already configured Dagger to provide these, so we have the necessary dependencies to create an instance of CurrentWeatherViewModel. Where it gets a little clever is how the ViewModelFactory instance gets created. The bindCurrentWeatherViewModel() will create an instance of CurrentWeatherViewModel and the @IntoMap and @ViewModelKey annotations are used by Dagger to create the Map which is injected in to the constructor of the ViewModelFactory instance. So when the create() method of the ViewModelFactory instance is called, it will lookup the Provider which Dagger wraps around from bindCurrentWeatherViewModel(), and calling get() on the Provider will cause Dagger to create a new instance of CurrentWeatherViewModel with the necessary constructor injection.

While that may sound complicated, Dagger is doing all of the heavy lifting, and we just need to define those few abstract methods in order to reap the rewards.

So with those bits in place, we need to hook it up in our Fragment:

class CurrentWeatherFragment : Fragment() {

    @Inject lateinit var viewModelFactory: ViewModelProvider.Factory

    private lateinit var currentWeatherViewModel: CurrentWeatherViewModel

    private lateinit var converter: Converter

    override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
        inflater?.inflate(R.menu.main_menu, menu)
        super.onCreateOptionsMenu(menu, inflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidSupportInjection.inject(this)
        super.onCreate(savedInstanceState)

        currentWeatherViewModel = ViewModelProviders.of(this, viewModelFactory)
                .get(CurrentWeatherViewModel::class.java)

        currentWeatherViewModel.currentWeather.observe(this, Observer<CurrentWeather> { current ->
            current?.apply {
                bind(current)
            }
        })
    }
    ...
}

All that is now getting injected is the ViewModelProvider.Factory instance that we just looked at. In onCreate, we perform the injection, and then obtain a ViewModelProvider instance by calling ViewModelProviders.of(this, viewModelFactory) (the second argument is the Factory provided by Dagger which will use Dagger to construct instances of CurrentWeatherViewModel as needed. The ViewModelProvider itself will handle the lifecycle of those instances – so we’ll get the desired handling of configuration changes. We then obtain an instance of CurrentWeatherViewModel, and from that we begin observing currentWeather (which is our CurrentWeatherLiveData instance). We’ll now get callbacks whenever the weather data is updated.

The net result of all of this is that CurrentWeatherFragment is now down to 110 lines (from over 200) including imports. It no longer is responsible for subscribing and unsubscribing from the LocationProvider, and actually is now completely agnostic of the LocationProvider.

As we have been going along, I have been adding unit tests to the components that are testable. The work that we have done in decoupling the different components has increased the testability enormously, and we have quite a rich set of unit tests. CurrentWeatherFragment is now much more focused on UI, all the better for it, and it now look likes it may be possible to create some tests. Testing our UI cannot easily be done in unit tests, and Espresso is the logical choice for UI testing. However getting location may not be possible if we are running our tests on an emulator, and making network calls in tests is a really bad idea because network outages / fluctuations make make our tests unreliable. Our tests should not be testing external components outside of our control. Fortunately, the architecture that we now have makes it pretty easy to remove them from the equation, but it may not be immediately obvious how to go about that.

In the next article in this series, we’ll look at how we can write Espresso tests to test our UI in isolation, and also look at a really neat trick that leverages the dependency injection that we already have in place.

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 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.