SECRET OF CSS

Build Subscription-aware Flows in Kotlin | by Andrei Buneyeu | Sep, 2022


Subscription-aware Flows may allow developers to seamlessly use the legacy APIs with registered callbacks in a modern reactive programming

1*c94GRTwBkU71PLsHSodEKA

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 registerListener / 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 register/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.

Luckily, there is a field of MutableSharedFlow named subscriptionCount. It represents a number of subscribers and is itself a StateFlow that we can subscribe to.

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 register/unregister callbacks:

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!



News Credit

%d bloggers like this: