Much of our responsibility as software engineers is based upon problem-solving, we are required to use our knowledge of available tools, and our experience, to find the most appropriate solution to a given problem.

That’s what I’d like to focus on here, on finding the most appropriate solution.

More often than not, the solution to a problem can be quite simple and doesn’t require over-engineering, which is perhaps where the age-old KISS and YAGNI principles come from.

But quite often, we can feel like the solution might not be “clever” enough, and this urge to show off a little, or even just to flex our creative muscles, causes us to miscalculate the problem, or even worse, write solutions to problems we didn’t even have.

Though there’s nothing wrong with this in theory, it is what drives us as developers to constantly improve and find new and inventive solutions to existing problems, but sometimes we need to make sure to keep ourselves in check.

Last year I spoke at Berlin Droidcon about Data Binding with Kotlin, concerning the possibility of unnecessarily complex binding declarations, containing business logic or domain behaviour, making it very difficult to test; whilst the Data Binding framework allows us to use many different and powerful binding expressions, we probably don’t always need to use them all.

With Kotlin having been adopted so quickly, and with new features constantly becoming available to a community whose experience with it may not be mature enough to be cautious, means we can be prone to the same issues.

Here’s an example of commonly used code with Kotlin scope functions, as a demonstration of how code that may seem intuitive could actually be damaging.

val extra = intent?.getStringExtra("extra.string")
extra?.let {
  doSomeWork(it)
}

This bit of code may seem pretty trivial and doesn’t seem to include much complexity, but the issue often overlooked is that this code doesn’t handle the failure case when the extra parameter is not provided to the activity intent.

Looking at the behaviour of getStringExtra, you can see that it does indeed return a null in the absence of the parameter, and uses a framework return type, which doesn’t infer the correct nullability.

Consider this compounded by further use of context functions, perhaps used to fetch a cached value to perform some work, and it seems reminiscent of the callback hell encountered when trying to build asynchronicity in JavaScript.

intent?.also {
  it.getStringExtra("extra.string")?.let{
    val cached = cache.getOrNull(it)
    cached?.let {
      doSomeWork(it)
    }
  }
}

By repeatedly nesting scope functions, its can be hard to determine the current scope you’re operating on, and whilst there isn’t an “official” guideline to how scope functions should be used, there are many recommendations from community members what work quite well.

You can read more about how to use scope functions with a deeper look into Kotlin where Sebastiano Gottardo details the behaviour of each of the standard library scope functions and where they might be appropriate to use.

However, when it’s so easy to shield ourselves from the presence of null, it can also be very easy to forget it even exists, thereby forgetting to handle anything other than the happy path.

Handling failures often depends on the cause of the failure, and whether or not it can be recoverable, or if it is terminal. In this circumstance, the provided parameter could have been optional, and the app will continue to function without it, so we could default with null or an empty string.

An example of handling a failure case can be done with a try…catch statement, where the operation is often fetching an asynchronous resource, this is wonderfully demonstrated with the Coroutines function runCatching, where the use of an inline class to represent a result with a mutually exclusive value or exception.

Forgetting to handle negative cases means that our code can become error-prone, and we no longer care for handling unexpected behaviour, which can manifest in many different ways from errors not being reported to unpredictable offline behaviour.

It’s important to remember, that Kotlin gives us a wide array of tools to use that allow us to create more concise, idiomatic code, and I’m not advocating against the use of these functions. Quite often the use of scope functions like .?let { ... } can allow us to write more concise code, but it doesn’t mean we can treat it trivially.

Put simply, Kotlin has been revitalising to the Android community as a whole, it’s been widely accepted and makes programming joyful, but as software engineers, it’s our responsibility to make sure the code we’re writing is sensible, and practical.