One of the best approaches to making your application more streamlined is to make your code event driven. To do that, you usually need to subscribe to certain sources for changes — like your DB and SharedPreferences , or to other services like an event bus or a BroadcastReceiver . When you do use subscriptions, one of the most important things is to remember to unsubscribe. Forgetting to do so would most probably result in memory leaks or weird and unwanted behaviour.

Luckily, Android provides specific locations in which to do so. For example:

When I load my activity, I’d like to get and subscribe to changes in our SharedPreferences since they contain various properties which are displayed in the UI, and since the screen has a method which changes those properties, the UI needs to be updated on the fly.

Image by Couleur from Pixabay

To do so, I create a listener, subscribe it to the matching key, and sequentially get the latest value, all in the onCreate method, while I clear the subscription in the onDestroy method:

class SubscribingActivity : AppCompatActivity() {

private lateinit var listener: SharedPreferences.OnSharedPreferenceChangeListener



override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {

super.onCreate(savedInstanceState, persistentState)

setContentView(R.layout.activity_subscribing)



listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->

if ("theKeyImListeningTo" == key) {

doTheThing(sharedPreferences.getString("theKeyImListeningTo", null))

}

}



val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)

sharedPreferences.registerOnSharedPreferenceChangeListener(listener)

doTheThing(sharedPreferences.getString("theKeyImListeningTo", null))

}



override fun onDestroy() {

super.onDestroy()

val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)

sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)

}



private fun doTheThing(string: String?) {

// Update the UI or do something else

}

}

While this code works just fine, it feels a bit clunky. So much code for just one subscription! Had we needed another one, it would require another field, and another callback for that specific property. In addition, if we needed to subscribe again in another activity or component, this giant block would follow us there, and we’d probably forget eventually to add the cleanup in the onDestroy method. Let’s shrink it shall we?

Extraction

First of all, in order to make the subscription reusable, we need to extract it to a separate class — PreferencesSubscriber . Regarding its design, we’d like to work with lambda expressions which will be triggered when the property changes instead of the really really long SharedPreferences.OnSharedPreferencesChangeListener callback interface, that one is just too nasty-looking:

open class SubscriptionsContainer(val context: Context) {

private val listeners = mutableListOf<SharedPreferences.OnSharedPreferenceChangeListener>()



fun registerPreference(preferenceKey: String, action: () -> Unit) {

val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->

if (preferenceKey == key) {

action()

}

}



listeners.add(listener)

PreferenceManager.getDefaultSharedPreferences(context)

.registerOnSharedPreferenceChangeListener(listener)

action()

}



open fun clearSubscriptions() {

listeners.forEach {

PreferenceManager.getDefaultSharedPreferences(context)

.unregisterOnSharedPreferenceChangeListener(it)

}

}

}

The SubscriptionsContainer accepts a key and a lambda expression for registration. It then creates the required callback listener which will check if the changed property was the one it was initialised with (it is invoked whenever any property was changed), and if so, will execute the lambda expression. The container then adds the listener to a list, subscribes it to the SharedPreferences , and executes the action immediately to deliver the initial value of the property.

When it needs to unsubscribe all the listeners, it iterates over the list and removes them one by one from the SharedPreferences .

So far so good, let’s use it in our activity:

class SubscribingActivity : AppCompatActivity() {

private val subscriptionsContainer = SubscriptionsContainer(this)



override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {

super.onCreate(savedInstanceState, persistentState)

setContentView(R.layout.activity_subscribing)



subscriptionsContainer.registerPreference("theKeyImListeningTo") {

val value = PreferenceManager.getDefaultSharedPreferences(this).getString("theKeyImListeningTo", null)

doTheThing(value)

}

}



override fun onDestroy() {

super.onDestroy()

subscriptionsContainer.clearSubscriptions()

}



private fun doTheThing(string: String?) {

// Update the UI or do something else

}

}

Much better! The subscription code is now reusable, and it makes the code much cleaner due to the lambda expressions.

But still, though the code is easier to work with, and is it’s pretty straightforward, I’d like to achieve 2 more things:

The onDestroy method is still a potential issue, another developer might forget to implement the cleanup there, so I’d like to have the subscription cleanup to be called without having the line there. Since we’re adding functionality to the activity, I’d like it to be executed by the activity itself, with an interface. However, doing that would require us to implement the logic by each activity which will beat the purpose of the class. Luckily, there’s a solution!

So let’s tackle those:

Lifecycle

First of all, let’s add the Jetpack Lifecycle library to our gradle*:

dependencies {

implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"

}

With this library we can subscribe to the lifecycle events of components like activities, fragments and services.

Now we can create a “lifecycle-aware” version of our subscription manager:

open class LifecycleSubscriptionsContainer(context: Context,

lifecycle: Lifecycle)

: SubscriptionsContainer(context),

LifecycleObserver {



init {

lifecycle.addObserver(this)

}



@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)

override fun clearSubscriptions() {

super.clearSubscriptions()

}

}

This version — the LifecycleSubscriptionsContainer , operates exactly like the original container, except it subscribes itself to a lifecycle it receives, and will execute the clearSubscriptions method after the onDestroy phase of the given lifecycle is completed (in our case, after the onDestroy method is called in the activity).

We’ll implement it like this:

class SubscribingActivity : AppCompatActivity() {

private val subscriptionsContainer = LifecycleSubscriptionsContainer(this, lifecycle)



override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {

super.onCreate(savedInstanceState, persistentState)

setContentView(R.layout.activity_subscribing)



subscriptionsContainer.registerPreference("theKeyImListeningTo") {

val value = PreferenceManager.getDefaultSharedPreferences(this).getString("theKeyImListeningTo", null)

doTheThing(value)

}

}



private fun doTheThing(string: String?) {

// Update the UI or do something else

}

}

Note that the onDestroy method is now gone, as it’s not needed anymore! The container’s clearSubscriptions method is called by the lifecycle owner — the activity, when it reaches its onDestroy phase.

Default Implementation

With the lifecycle-based invocation done, we’ll now wrap this functionality as an interface with default implementation, to simplify the usage of the subscriptions. However, we wouldn’t want every single inheritor of the interface to simply copy-paste an implementation for this functionality, as that will achieve nothing and won’t improve the code. For that — We’ll use Kotlin’s feature of default implementations!

When we declare an interface in Kotlin, unlike Java, we can add a body to each method as a default implementation of it. Using that, we can grant each inheritor a complete set of additional functionality with very little additional code required, like so:

interface PreferencesSubscriber {

val subscriptionContainer: SubscriptionsContainer



fun registerPreference(preferenceKey: String, action: () -> Unit) {

subscriptionContainer.registerPreference(preferenceKey, action)

}



fun clearSubscriptions() {

subscriptionContainer.clearSubscriptions()

}

}

Note that the PreferencesSubscriber has a val member which nothing set to it: Interfaces cannot hold variables on their own, but ones can be declared for them, due to the fact that declaring members in Kotlin is parallel to declaring getters and setter in java - which are methods.

The default implementation we’ve declared for this interface is to simply call the container it owns.

With this interface, we’ve wrapped the functionality of the SubscriptionsContainer and granted them to whoever implements the interface. The only piece of code they’ll need to add is the type of container they’ll wish to use: the regular one, the lifecycle-based one, or another custom one you’ll implement on your own. So returning to our activity, it’ll look like this:

class SubscribingActivity : AppCompatActivity(), PreferencesSubscriber {

private val subscriptionsContainer = LifecycleSubscriptionsContainer(this, lifecycle)



override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {

super.onCreate(savedInstanceState, persistentState)

setContentView(R.layout.activity_subscribing)



registerPreference("theKeyImListeningTo") {

val value = PreferenceManager.getDefaultSharedPreferences(this).getString("theKeyImListeningTo", null)

doTheThing(value)

}

}



private fun doTheThing(string: String?) {

// Update the UI or do something else

}

}

Note that now, since we’ve added the PreferencesSubscriber , declaring the subscriptionsContainer isn’t voluntary, but mandatory: as it is a part of the interface.

Nice and clean to use, and can be easily implemented in other components, with or without lifecycle.

In Conclusion

This architecture can be used for many purposes and can be triggered on different events : I use it in my project to register and unregister my activity to an EventBus on every onStart and onStop , and for a none-subscription example: to close DB connection instances when the activity is destroyed.

One note before we’re done:

Note that the methods you register to lifecycle events are invoked after their respective methods are executed. So let’s say you need to query data from the DB during your onCreate method and you need an initialised DB connection for that. You won’t be able to use the connection if you initialise it via lifecycle registration as the initialisation method will be invoked after the onCreate method was called. As such, the interface should expose some sort of initialiseConnection method which will be connected to the respective container for execution.

Using these components helps us further clean up our code and make it easier to expand, support and develop. I hope you’ve found this article helpful, good luck in your projects!