Our previous entry in the series ended with a new implementation of a composable API for your library. If you remember, early in the refactor, we decided not to touch an interface OnDragCallback because callbacks are a lengthy, evolving subject. In this entry, we'll dissect the way your library code can provide asynchronous responses to other parts of a program, and how that's handled in the Android environment.
Asynchrony in Android is a touchy subject. One that even Google has been known to avoid whenever possible even when making framework decisions. In our case, the problem is the lifecycle of the Android apps. The architectural assumption is that there is a set of classes where your code happens, namely Activities, and said classes should never be retained, as they come with large memory overhead and are also tightly bound to the Operative System flow.
Designing a callback system in this environment is hard, as callbacks imply blocks of code where the holding class is retained.
The Thread will never terminate, and our activity will never be destroyed. This is a simple example, but sometimes the threads or statics are hidden from us, and their implementations can be equally as wrong.
// the callback reference never gets cleared!
We need to find a way of killing those callbacks.
The following sections go through the different approaches to handling these connection/disconnection behaviours. Some of them are fairly common, some may be new to you. They all have value and have been widely used through the industry for decades.
This is the military, strict way of handling your callbacks. As the implementor, you provide an interface to add and remove callbacks into your api, and any clients using it are in charge of remembering to remove themselves from the list of results.
For the api implementor it is the most repetitive, boilerplate-filled approach, but can also be difficult to assure that it won't be affected by race conditions when removing from the list when the operation happens on multiple threads.
In this approach, the user is in charge of collecting tokens (i.e. a string or number) representing the operation, which can be sent to a cancellation method.
This approach is fairly simple to implement, simply with an AtomicLong inside the API where the IDs go up incrementally, and a Map from Long to any callback where the entries get removed on completion/cancellation.
It suffers from the same problems as the callback list, and can definitely become an annoyance to implement for every new api.
WeakReferences are a JVM construct to handle memory. They tell the garbage collector that if they're retained in only one place, they can be collected and deleted. They basically clean after themselves.
This approach has a couple of pitfalls: the user cannot use the callback inlined, as inlined anonymous classes are not counted towards garbage collection, which means the library user has to be told to use a nullable reference to work around the lifecycle.
Google is vocal about Java-centric libraries. In some way, the idea they promote is that Java libs are nice and you can use them if they work for you, but you must remember that they're not Android specific and might not be the optimal choice in terms of integration with the system. Instead, they lure you to use their libraries, as they are well presented as aligned with memory, CPU, and being lifecycle-aware. Those libs go as far as hiding the whole connection/disconnection system from you but come at an interesting complexity tradeoff.
Think about Activities for a second. They don't look like most classes you've ever written, as they don't start with a constructor, and definitely, none requires you to clean after themselves. Which kind of classes do that? callbacks! An Activity is (among many other things) a callback of one screen manager, getting called on lifecycle events. The system hides the addition/removal of the lifecycle callback from you but requires that it's never retained externally to avoid leaks.
But there is more. An Activity is also a callback for configuration changes. An Activity is also a callback for the Permissions system. An Activity is a callback from Intents coming from system signals coming from other activities. Whenever Google creates a new async api, they use their already existing callback system on the activity manager. They either consider this a quality of life improvement or don't trust users to be able to clean after themselves.
Because it's based on inheritance rather than composition, via overriding methods that normally no-op, it makes our Activities that big ball of mud where following execution becomes increasingly difficult over time, with extra complexity from any kind of vendor bugs. Some apps (and libraries too!) bypass this problem by creating small short-lived Activities to be used as callbacks for cases like Permissions.
Once that is understood, I won't dig deep into Loaders, Services, or ContentProviders. The approach is the same used for Activities, they get tied to their lifecycle internally, and cause the same pitfalls plus some extra implementation bugs when mixed with Fragments and Support Fragments.
Let's make a quick stop in our way to reassess our current situation. I would guess that at this point you were already familiar with most of the subjects we've gone through. They don't need an introduction, were it not for the fact that they all have the same approach: the library implementor is responsible for providing the user with a way of disconnecting their elements. The logic for those callbacks lives inside the implementation of the library, as do their references. They look suspicious enough, but surely there must be other ways of approaching callbacks, and most definitely you've come to this blog to talk about functional, composable, approaches.
We need to go back into the field to interrogate our final two suspects.