Learning Kotlin by “Mistake”

Home / Developer Tools / Learning Kotlin by “Mistake”
Learning Kotlin by “Mistake”

 

As our individual and collective team understanding of Kotlin develops, we inevitably review & revisit code thinking things like:

“I’ve learned 3 new ways to write this”

“I prefer a different syntax/convention now”

“What was I thinking when I wrote this the first time?”

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.

One such construct is the apply function

When I first started using apply, I tended to throw as many related statements as I could into the apply block aiming to remove as many redundant references to the receiver object as possible.

Here’s an example of using apply that at one point seemed great, but, in my opinion, now feels like a misuse of the function.

Intent(this, MainActivity::class.java).apply {
  putExtra("extra1", "foo")
  putExtra("extra2", "goo")
  startActivity(this)
}

It avoids storing a reference to the Intent, but mixing the configuration logic with the usage of the Intent makes the overall purpose of the code a bit harder to quickly discern.

That led to me looking deeper into the other, similar functions let, with, run, and also. I tried re-writing it like this:

Intent(this, MainActivity::class.java).apply {
  putExtra("extra1", "foo")
  putExtra("extra2", "goo")
}.run {
  startActivity(this)
}

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).

val newIntent = Intent(this, MainActivity::class.java).apply {
  putExtra("extra1", "foo")
}
startActivity(newIntent)

If you still prefer everything be chained together you could try this as well

startActivity(Intent(this, MainActivity::class.java).apply {
  putExtra("extra1", "foo")
})

01.jpeg

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.

Always use a CompanionObject for constants

There is no static in Kotlin

When using the Java-to-Kotlin conversion tool, your static variables and methods will be converted to properties and functions on a CompanionObject

Since this is the default behavior provided by the tooling, for a long time I didn’t question whether using a CompanionObject for these use cases was always the best option.

After working in several different mixed, Java & Kotlin, codebases I started to see a number of situations like this:

class Foo() {
    companion object {
        private val KEY1 = "key1"
        private val KEY2 = "key2"
    }
}

A Kotlin class converted from Java would contain a CompanionObject for the sole purpose of containing the previously static values.

Eventually I came to the realization that in Kotlin, we are no longer bound to a strictly object-oriented world anymore and our constant values don’t necessarily have to be scoped to a class.

With Kotlin it’s possible to have the following within Foo.kt:

private const val KEY1 = "key1"
private const val KEY2 = "key2"

class Foo()

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.

02.jpeg

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.

“reexamining our usage of null; enforcing immutability; and applying functional operators allowed us to write safer, more concise code”

For us, this provided a huge opportunity to really sink our teeth into the language and explore what was possible.

Closely reexamining our usage of null; enforcing immutability; and applying functional operators allowed us to write safer, more concise code which is our primary reason for moving to Kotlin.

Also want to mention Java -> Kotlin interop here.

Depending on your conversion strategies and overall Kotlin adoption some of the following items might impact how you think about the above items

  • Platform Types: If your Kotlin code is interacting with Java, you may need to consider how null enforcement is handled since types coming from Java can be null or non-null. One strategy for handling this is to make thorough use of nullability annotations on your Java types.
  • 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.

For more on Java-Kotlin interop, check out the documentation:

Calling Kotlin from Java — Kotlin Programming Language
Having multiple files which have the same generated Java class name (the same package and the same name or the same…kotlinlang.org

Calling Java from Kotlin — Kotlin Programming Language
Kotlin is designed with Java Interoperability in mind. Existing Java code can be called from Kotlin in a natural way…kotlinlang.org

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:

  1. Has this property been initialized when I want to access it?
  2. 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

This is where Delegated Propertiescome in, and where our team had another “ah ha!” moment.

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:

/**
 * Provides a [ViewDataBinding] object of the declared type by
 * calling [DataBindingUtil.setContentView] with the property owning [Activity] and the specified
 * layout resource id
 */
class ActivityBindingProvider<out T : ViewDataBinding>(
        @LayoutRes private val layoutRes: Int) : ReadOnlyProperty<Activity, T> {

    private var binding : T? = null

    override operator fun getValue(thisRef: Activity, property: KProperty<*>): T {
        return binding ?: createBinding(thisRef).also{ binding = it }
    }

    private fun createBinding(activity: Activity):T {
        return DataBindingUtil.setContentView(activity, layoutRes)
    }
}
class FooActivity : Activity {
  val binding:ActivityFooBinding by ActivityBindingProvider(R.layout.activity_foo)
}

top-level functions are always preferable to using a CompanionObject function

As mentioned earlier, Kotlin enables us to break free from strict object-oriented programming and embrace pure functions.

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.

03.jpeg

Photo by Sai Kiran Anagani on Unsplash

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”.

We hope to learn from your “mistakes” soon!

Source: https://engineering.udacity.com/learning-kotlin-by-mistake-b99304b7d724

Leave a Reply

Your email address will not be published.