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.

Categories
Android Architecture Kotlin testing

Logging in a Java library

It can be useful to emit logs in a library. When doing so, one needs to consider when to emit, how to filter and who is responsible for printing/handling the logs. Correct logging should also be tested.

When to log

There are different reasons to emit a message, for instance on important events, undefined behaviour or different levels of debug events.

Any potentially useful message should be emitted. However, in order to not clutter the terminal, output should be refined.

Filtering the logs

A library logging level should be configurable according to user preference:

/**  Possible logging levels. */
public enum Level {
    /**  No log messages */
    OFF(0),
    /** Informational messages and errors only */
    INFO(1),
    /* Debug messages */
    DEBUG(2),
    /* All messages, including fine traces */
    ALL(3);
}

It is expected that important and error messages are emitted by default, so Level.INFO should be the default setting.

However, if finer traces are required, the filter could be set to DEBUG:

Library.loggingLevel = Level.DEBUG

The library should then filter the messages according to level:

static void logDebug(String message) {
    if (loggingLevel >= Level.DEBUG) {
        loggingFramework.logDebug(message);
    }
}

Example of filtering implementation in hmkit-android library.

Choosing a logging framework

The user could be using any java subsystem, or maybe emitting the messages to a web service. For this reason, the library should never output to System.out.println or android.util.Log. Instead, it should be an interface from where the logs are emitted, and it should be up to the user to choose where the messages are output in the end.

A popular logging facade is slf4j. api part of it should be included in our library:

implementation 'org.slf4j:slf4j-api:1.7.25'

Then logs can be emitted through a Logger instance:

// sample informational log
logger = LoggerFactory.getLogger(Library.class);
...
logger.info("Library initialised")

Our library user could then continue using his favourite logging framework and add an slf4j binding to see the messages. For instance, a Timber binding:

implementation 'at.favre.lib:slf4j-timber:1.0.0'

Testing the emitted logs

To verify the emitted logs, a test setup is necessary. Mockk can be used to mock the slf4j and verify calls to it’s Logger.

For this to work, each test could inherit from BaseTest, which initialises the mock:

lateinit var mockLogger:org.slf4j.Logger

@BeforeEach
fun before() {
  mockLogger = mockk()
  mockkStatic(MyLoggerFactory::class)
  every { MyLoggerFactory.getLogger() } returns mockLogger

  every { mockLogger.warn(allAny()) } just Runs
  every { mockLogger.debug(allAny()) } just Runs
  every { mockLogger.error(allAny()) } just Runs
}

This class could also contain convenience lambda methods to assert the emitted logs:

fun debugLogExpected(runnable: Runnable, count: Int = 1) {
    runnable.run()
    verify(exactly = count) { mockLogger.debug(allAny()) }
}

From the derived class’s test method, an assertion can be then written about the log message:

@Test fun invalidStartControlControlModeThrows() {
    debugLogExpected { 
        val action = Library.resolve()
    }
}

This test will succeed if one debug message is emitted to the slf4j interface.

Please see auto-api-java as an example of using this pattern.

Conclusion

Logging in a library can be very beneficial. The maintainer should however be aware of the library user’s perspective, and filter too precise logs by default. Log printing should also be left to the user, and correct log emittance should be tested with unit tests.

Categories
Kotlin

Kotlin: Concatenating nullable strings

A null String is concatenated as “null” in Kotlin:

val first:String? = "first"
val second:String? = null

println("First is $first and second is $second")
> First is first and second is null

What if the goal is to concatenate only if the second string is not null? There are a few options:

if-else

One of the solutions would be to check the null value with elvis(?:) operator:

var result = "First is ${(first ?: "")} and second is ${(second ?: "")}"
println("$result")
> First is first and second is 

Now there is no “null”, but the whole “and second is” part is superfluous because the second string has no value. With an if case this segment can be omitted completely:

result = 
(if (first != null) "First is $first" else "") +
(if (second != null) " and second is $second" else "")
> First is first

This is the desired result, but the code is not easy to read with double if checks and concatenation of empty string “”. What if we could hide this logic with an extension function?

Extension function with lambda expression

Using an extension function with a function parameter enables us to separate the if case logic:

fun String.concatIfNotNull(fromObject: Any?, transform: (String) -> String) =
  if (fromObject != null) this + transform(this) else this

result = "".concatIfNotNull(first) {
  "First is $first".concatIfNotNull(second) { " and second is $second" }
}
> First is first

This code doesn’t have the if cases, but one might argue it is even harder to read than the first solution because of the extension’s dot notation and empty string “” concatenation in the beginning.

Top-level function with lambda expression

By defining a top-level function we don’t have to use the dot notation and extra string concatenation:

inline fun <T> ifNotNull(fromObject: T?, transform: (T) -> String): String =
 if (fromObject != null) "" + transform(fromObject) else ""

result = ifNotNull(first) {
  "First is $first" + ifNotNull(second) { " and second is $second" }
}

This solution could seem like the most human-readable way of conveying the purpose of our code: appending a sentence only if its contents are not null.

Conclusion

Kotlin string literals can become difficult to read if multiple if cases are used to construct them.

The developer can be aware of this and use alternatives like top-level functions or extension functions. This can make the code more self-documenting and easier to read.