Dissecting callbacks: A murder mystery

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.

The scene of the crime

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.

public class MyActivity extends Activity {
    Button button;

    @Override public onCreate(Bundle bundle) {
        button = startButton();
        button.setOnClickListener(value -> {
                doThing();
            });
    }

    public void doThing(){ /* */ }
}

In that small block of code, the whole Activity class is retained inside the listener field inside the Button field, which in turn contains a reference to the Button. These cyclical references are a non-issue on most garbage collected environments. As long as none of the elements of the cycle are referenced by an external garbage collector root, they'll be collected. A root is any reference that, by design, will never be garbage collected and represents the topmost element of the memory hierarchy. The most common ones are static fields, and threads. Most forms of threading require at least one GC root to keep the thread alive, which means anything that's strongly referenced until that thread finishes will not be garbage collected. One example:

public class MyActivity extends Activity {
    @Override public onCreate(Bundle bundle) {
        new Thread(new Runnable() {
            () -> {
                while (true) { }
                doThing();
            }
        });
    }

    public void doThing(){ /* */ }
}

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.

@Override public onCreate(Bundle bundle) {
    // Network client does requests on a background thread
    networkClient()
        .requestUserProfile(new Success(){
            (userProfile) -> {
                doThing(userProfile);
            }
        }, new Error(){
            (exception) -> {
                handleError(e);
            }
        });

    // Database singleton is a static and
    // the callback reference never gets cleared!
    Database.getInstance()
        .tableUpdates(new Listener(){
                (update) -> {
                    newValues(update);
                }
            });
}

If those references aren't cleared and their operations are not timed out or otherwise handled, then we have a chance of creating a leak. The network request can be stuck on retries or waiting for a clear connection. The database never stops waiting for updates in its tables, as they're not a terminating operation. You know what this means:

We need to find a way of killing those callbacks.

The callback potential killers

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.

Callback List - Colonel Mustard

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.

interface Database {
    void tableUpdates(Listener listener);

    void removeListener(Listener listener);
}

class MyActivity extends Activity {
    final Listener dbListener = new Listener() { /* */ };

    @Override public onCreate(Bundle bundle) {
        Database.getInstance()
            .tableUpdates(dbListener);
    }

    @Override public onDestroy() {
        Database.getInstance()
            .removeListener(dbListener);
    }
}

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.

Identifier token - Ms. Peacock

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.

interface NetworkClient {
    long requestUserProfile(Success successCb, Error errorCb);

    boolean cancelRequest(long id);
}


class MyActivity extends Activity {
    final List<Long> ops = new ArrayList<>();

    @Override public onCreate(Bundle bundle) {
        ops.add(networkClient()
                .requestUserProfile(/* */));
    }

    @Override public onDestroy() {
        for (Long id: ops){
            networkClient().cancelRequest(id);
        }
        ops.clear();
    }
}

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 - Mrs. White

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.

interface Database {
    void tableUpdates(Listener listener);
}

class DatabaseImpl implements Database {
    final List<WeakReference<Listener>> listeners 
        = new ArrayList<>();

    @Override void tableUpdates(Listener listener) {
        listeners.add(new WeakReference<>(listener));
    }
}

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.

class MyActivity extends Activity {
    Listener dbListener;

    @Override public onCreate(Bundle bundle) {
        dbListener = new Listener() { /* */ };
        Database.getInstance()
            .tableUpdates(dbListener);

        // This won't work as the callback 
        // will be immediately collected
        Database.getInstance()
            .tableUpdates(new Listener() { /* */ });
    }

    @Override public onDestroy() {
        dbListener = null;
    }
}

Android constructs - Miss Scarlett

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.

Back to Scotland Yard

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.

The "Intro to FRP" Series
  1. Functional libraries for Java
  2. Advantages of using generic method references, Functions and Actions as parameters
  3. Transforming APIs based on inheritance to simpler, composable, ones by means of Functions
  4. Review of Android lifecycle issues with traditional callbacks
  5. The reactive approach to composable and cancellable callbacks with observable sequences
  6. The Observable in RxJava as a composable sequence for your existing code