As these conversations arise, there is potential to feel as if we’ve made a “mistake”; that by not knowing the “ideal” pattern or trying something different we’ve somehow wasted time by writing a sub-optimal solution.

Ultimately, we’ve all been in this position.

We learn how to do something, write the code, continue learning and eventually discover a new/different/maybe-better way of solving the same problem. We call the previous iterations a “mistake” and discount what we learned during the process.

“Mistakes” are learning opportunities

We’ve worked to encourage these “mistakes” and embrace the exploration and experimentation that may lead to these conversations. It’s all too easy to simply apply our familiar patterns & conventions from Java to our Kotlin codebase, but in doing so we lose out on some of the opportunities Kotlin provides.

If we find that our previously held understanding was mistaken, it means we’ve increased our collective understanding and most likely learned a good deal along the way.

Learn from our “mistakes”

As we continue with Kotlin, we’ve made a number of “mistakes” that have lead us to a deeper understanding of the language, and ultimately to better code. The following are a few instances where our previously held understanding of the language was challenged and developed over time.

Chain everything into a single statement

Kotlin provides a number of functional constructs that allow the concise chaining of operations into a single statement or expression.

By removing startActivity(this) out of the apply block, it more clearly separates the configuration of the Intent and its usage. Unfortunately, moving the statement into run adds a bit of verbosity that Kotlin is generally good at avoiding.

For this type of situation, I’ve now moved away from trying to overuse these Kotlin functions, and simply storing the Intent in a val and referencing it in a separate statement. It’s 1 extra line of code, but much easier to quickly understand (in my opinion).

I may have gone through several iterations of solving a similar problem, and abused some functions along the way, but I now have a much better understanding of the differences between let, apply, run, and with as well as an understanding of how those functions are actually implemented.

Ultimately this is a small example of a larger problem:

It’s very easy, tempting even, to make Kotlin code as concise as possible but in doing so, it may become less understandable. Finding a balance between brevity and readability is an ongoing and important part of diving into Kotlin.

We can define our constants as top-level properties using const val. There is then no need for a CompanionObject and any of the pitfalls they may entail.

Learning to embrace this less object-oriented approach was a very freeing experience and led to other experimentation and learning with top-level declarations that we will explore shortly.

The Java -> Kotlin conversion tool is enough

Kotlin has a pretty great interop story with Java. Additionally, Android Studio has an extremely useful Java to Kotlin conversion tool that makes it trivial to incorporate Kotlin into your codebase by converting an existing Java file with the click of a button.

All of that said, we’ve learned from experience (sometimes the hard way) that simply clicking `Convert Java File to Kotlin File`is not enough. There are a number of things to consider beyond what the conversion tool provides.

Think about nullability and avoiding !! whenever possible.

Prefer val to var. The conversion tool isn’t always able to convert your existing code as-is to make full use of val over var.

Does your converted class now have a CompanionObject? Does it need one?

Do you have overloads or builders that could be replaced with default/named parameters?

Converted Java doesn’t take into account the functional operators available or idiomatic Kotlin conventions such as higher-order functions or extension functions.

Not every converted file will require you to address all of these areas. However, sometimes the conversion tool is just the first step and to get the full benefit of Kotlin considerable time may be needed to manually convert your code.

When using CompanionObjects you may want to consider whether using the @JvmStatic and @JvmField annotations are beneficial to your code. These annotations can allow you to have truly Java-static versions of your CompanionOjbect properties and functions.

If the idea of default & named parameters is appealing, keep in mind that, by default, when calling a method from Java that has default parameter values only 1 version of the method (with all parameters required) is available to you. The @JvmOverloads annotation can be applied to generate multiple overloads for the Java side, but this can be cumbersome to use so it’s utility will likely depend on your use case.

lateinit var foo:SomeType is the best we can do for lifecycle dependent initializations

This was a common misconception for our team for quite a while.

We would often come across places where we would only initialize an object once, but that initialization needed to be deferred until onCreate or some other point beyond the initial object creation.

Ideally, we wanted to define these properties as val to properly enforce immutability, but with the lifecycle dependency this didn’t seem feasible.

Using lateinit let’s us defer the initialization until a later point, and will throw an exception if accessed before initialization has occurred. This may be closer, semantically, to our ideal usage but leaves the following questions:

Has this property been initialized when I want to access it?

Has this property changed? Since it’s now a var there’s no insurance that it hasn’t been reassigned.

Ideally, we could express the property as a val and still defer until to the appropriate point in the Android lifecycle

By creating a custom delegate, we can defer the initialization until the appropriate time and define the property as a read-only val.

Once such delegate is lazy which will initialize the property on first access

This allows us to move from

lateinit var foo:SomeType

to

val foo:SomeType by lazy { <creation & initialization here> }

We now have the desired read-only semantics, and know that our object will be available when needed.

You can write custom delegates as well. This is useful if you want to provide a more readable delegate name, provide a delegate for generic types, or have initialization logic that is dependent on the Android Activity lifecycle.

The following is a custom delegate we’ve been using to simplify how we handle binding objects when using Android’s databinding:

One area where we’ve really experimented with this is in our usage of helper functions.

For example, it’s a common Android convention in Java to define a static startActivity method for our activities. When converted to Kotlin, this approach would leverage a CompanionObject with a function. However, in a Kotlin code base we don’t necessarily need to tie ourselves to a companion, and instead can choose instead to just define a top-level function to handle the same functionality.

This eliminates the need for a CompanionObject, but also adds to the global namespace since top-level functions are available to the entire package unless marked private.

This is somewhat subjective, but it might not be preferable to have everything publicly available; particularly if you’re writing a library and want to maintain an isolated, self-contained api.

In this case, scoping to the class in question can be a useful convention. Your team may also prefer the convention of accessing the function in more of the traditional Java “static” manner in which case again scoping the function to a CompanionOjbect might be preferable.

The approach here may also depend on whether your code base is mixed Java & Kotlin, or maybe 100% Kotlin.

We’ve found that the more functional approach feels a bit nicer in our fully Kotlin project than it does in our mixed code base. This is primarily because when calling a Kotlin function from Java, whether from a CompanionObject or as a top-level function, there are hoops to jump through such as needing to mark functions with @JvmStatic or needing to import the generated Kotlin file that contains the top-level function.

We are still experimenting with different patterns and conventions for leveraging pure functions versus “static” ones, but, to date, our trial and error has resulted in some useful results and a lot on lessons learned.

Go forth, and make “mistakes”

Hopefully you’ll find some of our previous “mistakes” enlightening and will be encouraged to continue pushing the bounds of your understanding of Kotlin.

Experimenting with a new pattern, discovering a new function, learning the pros/cons of different implementations; these drive the learning process.

As the Android community learns to leverage Kotlin together, we should be embracing the opportunity to discover new skills/patterns/conventions and happily discovering that our previous understanding was “mistaken”.