Subscription-aware Flows may allow developers to seamlessly use the legacy APIs with registered callbacks in a modern reactive programming
Some time ago Android developers started promoting a pattern of Lifecycle-aware components — components that automatically adjust their behavior based on the current lifecycle state of an activity or a fragment. Instead of explicitly telling a component that it’s in this or that state, we want the component to be already aware of this.
In this article, I’d like to demonstrate a similar pattern of how you can build Subscription-aware components — components that similarly to the Lifecycle-aware components can take decisions based on the status of their subscriptions.
The traditional imperative approach to get updates on some components is something like the following:
I’m using Android SharedPreferences here as an example but nothing here in principle is Android-specific: there are similar
unregisterListener APIs in vanilla JVM world too.
They give a lot of headache to developers as the code evolves. We have to be careful about memory leaks: maybe
registerListener(listener) will store a reference to the listener and therefore to the class where it’s defined. Maybe we’ll forget unregister it somewhere, or if not maybe our colleague will forget to do that.
Some issues may be naturally solved by introducing a Flow:
So now, we can tell all our colleagues to only use
sharedPreferencesFlow and forget about
unregister callbacks. We inverted the control and we can stop caring about who subscribes to
sharedPreferencesFlow and how the subscriptions are handled: it’ll pretty difficult to make it leak memory.
But, now we have a problem with scopes: instead of letting each developer decide when to start and stop updating data (which can be computationally somewhat expensive), we now took the decision for everybody, and we do update the flow in the biggest scope possible so that everybody gets guaranteed updates.
This field is used under the hoods of the
shareIn method that converts a Cold Flow into a Hot Flow, to define the start behaviour policy: whether the
SharedFlow should be working
Eagerly (from the very moment of declaration and never stopping),
Lazily (when somebody starts subscription and forever), or
WhileSubscribed. But in our case, we have to use it manually, since we’re calling legacy
Voilà! We have a Flow of changing SharedPreferences (presented in a form of a Map). Any component in the app can simply subscribe to it, and while there is anything subscribing to it, the updates are going to be delivered., and after that, the resources are going to be released automatically.
Back to the Android world, we can subscribe to it in a Composable:
And as soon as
SomeComposable will stop being rendered,
unregisterOnSharedPreferenceChangeListenerwill happen automatically. Easy!
A couple of remarks about the code:
applicationScope— this is a Scope that outlives Fragments, Activities or ViewModels. Even though using GlobalScope is discouraged, there is nothing wrong with creating your own CoroutineScope that will live through the whole application lifecycle. You can easily override or control that scope in your tests.
- CoroutineName(TAG) — this is just for good manners so that when you want to know what Coroutines are being executed at any given moment, or what’s the Coroutine Context of a given suspending function, this name will greatly help you in debugging process.
- Dagger/Hilt is implicitly used here. I’ll leave it to the reader to implement the missing parts 🙂
It’s often a challenge to combine legacy code with new patterns and approaches, but the Subscription-aware Flow allows us to bridge the gap between the legacy APIs registering callbacks and Kotlin Flows. Such a pattern may help not only with this particular example of legacy
SharedPreferences code, but pretty much any code that uses the approach of registering callbacks.
Thank you for reading!