Categories
Architecture Java Kotlin

Callback styles for async tasks

For asynchronous tasks, the actions on completion need to be handled via a callback. There are different patterns to achieve this, with each having their own benefits and shortcomings.

Interfaces

One of the oldest callback styles are interfaces or anonymous classes. They are used to great effect in Android. As an example, with okhttp library, a network request could be sent:

okHttp.newCall(request).enqueue(new Callback() {
  @Override public void onFailure(Call call, Exception e) {

  }

  @Override
  public void onResponse(Call call, Response response) {

  }
});

Interface use is very convenient, because the request and callback can be written in one line and all of the outer class properties are available in the nested function.

However, shortcomings arise when handling multiple tasks. Consider if we needed to wait for all of the requests to finish. Then, a response counting logic is required:

Response[] successfulResponses = Response[requests.size()]
final int[] responseCount = {0};

for (Request request in requests) {
  okHttp.newCall(request).enqueue(new Callback() {
    @Override
    public void onResponse(Call call, Response response) {
      successfulResponses.add(response);
      responseCount[0]++;
    }
  });
}

// wait for all of the responses

while (responseCount[1] != requests.size()) {
  Thread.sleep(1);
}

// all responses are here
println("All tasks finished:");

There is a better way to handle this kind of scenario.

CompletableFuture

Since Java8 and Android 24(or with a support lib), CompletableFuture is available. It proposes to fix the interface shortcomings like scattered callback locations, deeply nested callbacks or sequential tasks management.

With this new API, waiting for all of the answers can be done with the allOf() method:

CompletableFuture<Response>[] requests;

CompletableFuture<Void> tasks = CompletableFuture.allOf(requests);

CompletableFuture cf = tasks.thenRun(() ->
  print("All requests finished"));

// start a blocking thread to run the tasks
cf.get();

Single completions can also be observed:

for (CompletableFuture<Response> request : requests) {
  request.thenAcceptAsync(response -> {
    print("request response: " + response);
  });
}

CompletableFuture API is expansive, having different methods for creating, combining and executing tasks. Some extra benefits are:

  • Sequential task management
  • Integration with Kotlin coroutines, Streams API, RxJava

LiveData and RxJava

Recent paradigm shift in programming has been the introduction of Observable pattern. It is now even the Android’s recommended app architecture style.

What differentiates it from the previous styles, is that a single callback is used for all of the updates of a property. A common scenario is a view state, which advertises its value changes. Only the new value is advertised, irrelevant from the source of the change.

In our case it would mean that we wouldn’t get the response from the web request directly, but from an field in the ViewState object:

class ViewState {
  String response;
  String error;
}

MutableLiveData<ViewState> viewState = repository.getViewState();

// observe the view state. Observer count is unlimited
viewState.observe(this, viewState -> {
    view.setText(viewState.response)
});

// repository makes the requests internally and updates the // viewState object
repository.getNewState();

RxJava possibilities are even greater than the ones of CompletableFuture, including a rich set of chaining operators. Before jumping in, one has to consider the learning curve of a new programming paradigm.

Conclusion

There are different use cases for all of the aforementioned Async task callback styles.

Interfaces can be used for simple tasks or callbacks that are only run once and no combination is needed.

Chained tasks or more complicated process management is handled better with the CompletableFuture.

Observable pattern can be used for even greater flexibility and added benefits. It is a programming paradigm shift though, and weighing the benefits over the skill acquisition time is recommended.