Kotlin Features You Should Use Now

Andrew Grosner
Fuzz
Published in
9 min readApr 30, 2018

A little less than a year ago at Google IO 2017, Google announced that Kotlin was a first-class supported language for Android development. It was one of the top highlights of the conference among many other important announcements. I was there and I can tell you that it received the loudest applause.

Finally, a language on par with Swift, C#, etc.

Almost one year later, now that we’ve seen, tried, and used Kotlin in some fashion. The language adds a lot of new features when compared with Java, that will improve your code readability and maintainability.

In this post, we will skip the basics of the language and dive right into some interesting features. Read the reference later if you want even more context.

In this post, we use Kotlin 1.2.31.

Breakdown

Sealed Classes

Inline Methods

Extensions

Sealed Classes

Sealed Classes are a special kind of class such that the Kotlin compiler can verify all subtypes during compilation, adding some unique benefits.

Take this enum State :

enum class State {
Loading,
Done,
None
}

Enums are exhaustive in when expressions (switch in Java). In Kotlin, we can return on expressions or assign a value to one, forcing each branch of the expression to supply a valid value to fulfill the expression return type.

fun checkState(state: State): Unit = when (state) { // Unit implied with no return
State.Loading -> {

}
State.Done -> {

}
State.None -> {

}
}

Say we want to add more State values to our app (ex. Error). When using expressions, if we don’t use an else branch, compilation will fail with a proper message:

'when' expression must be exhaustive, add necessary 'Error' branch or 'else' branch instead.

For best practices (and prevent undefined behavior) do not supply an else branch. (You’ll thank me later!). Add the State.Error branch and we satisfy the condition.

Now, we use our enum and are initially happy with it. It covers all of our initial cases and we’re coding along. We discover that in some situations, we need the result that the loading State comes back with, instead of utilizing a separate variable. With JVM enums, this is not quite possible. In Java, enum are static singleton objects that cannot have multiple instances. Furthermore, if you want mutable values on those objects (such as to hold state), you globally change it for all consumers of the enum. This is not ideal for a multithreaded (heck single-threaded) app that can mutate this value in many different places.

To allow our enumto hold state, but have the compiler verify we covered all cases of an instance, we change the declaration to asealed class:

sealed class State

Next, we must explicitly declare each case of the old enum as subtypes of the sealed class:

sealed class State {
object Loading : State()
object Error : State()
object Done : State()
object None : State()
}

Pro-tip: Option-Enter on the State class and select the “Convert To Sealed Class” option. This will take care of preserving backwards compatibility.

Observe how an object subclass of a sealed class preserves call site compatibility:

when(state) {
State.Loading ->

We don’t need the is operator for State.Loading since it is a singleton and will be the same instance.

Since these are just now classes, we can change our Done state to hold a result object:

data class Done<T>(val result T? = null) : State()

Notice that since these are classes, we can also make them object or data class.

Now in our when statement:

fun checkState(state: State) = when (state) {
is State.Done(null) -> {
// can pattern match!
}
is State.Done(true) -> {
// can match on result cases
}
is State.Done<*> -> {
// other values
// access the result directly via smart casting,
// we lose type arguments in java still.
(state.result as? List<User>)
?.let { userList -> displayUserList(userList) }
}
//...excluded for brevity
}

We can pattern match on the case and only execute code for which value the State contains.

Best Practices

Keep Compact

Do not overuse to the point where you have a sealed class for everything.If you have UserLoadingState , ProfileLoadingState , etc. and they follow all similar state pattern, it might be time to consider combining them into a single State class. After all, each declaration is another class which comes with it’s overhead. Strike a healthy balance between generic and specific to keep your codebase clean and lean.

Use Only When Necessary

As with any class, do you really need a ScreenAction() sealed class where it’s only parameter is a String ? Given:

sealed class ScreenAction(val name: String) { 
object Profile("Profile)
object Account("Account")
}

As it grows, you add many more classes for each String. If not necessary, just define them as:

object ScreenAction {
const val Profile = "Profile"
const val Account = "Account"
}

Keep The Declarations Coupled

Declaring them as inside the outer sealed class will “namespace” them to the type, so you are more aware when using it, where it belongs from.

sealed class State {
object Loading : State()
}
Loading.State

Inline Methods

Inline Methods instruct the compiler to copy + paste code into the call site during compilation. This contains some obvious benefits in that we can reduce overhead of certain methods by providing no-cost helper functions. As we all know on Android, we look for ways to minimize method count impact to keep our precious apps under the 65K method limit.

Take this innocent looking method here:

fun log(tag: String, priority: Int, logMessage: () -> String) {
if (BuildConfig.DEBUG) {
Timber.log(priority, tag, logMessage())
}
}

We want to log a message to Timber only on debug builds, and lazily specify the message so that it only calculates itself (a potentially expensive operation) when it is logged. Note: the Android DEX compiler will strip out code executed in BuildConfig.DEBUG checks in release builds, so this code won’t appear in release.

Let’s dig into the generated JVM byte-code decompiled into Java:

public final class LogKt {
public static final void log(@NotNull String tag, int priority, @NotNull Function0 logMessage) {
Intrinsics.checkParameterIsNotNull(tag, "tag");
Intrinsics.checkParameterIsNotNull(logMessage, "logMessage");
if (BuildConfig.DEBUG) {
Timber.log(priority, tag, new Object[]{logMessage.invoke()});
}
}
}

Ok, from first glance this looks great! The function parameter is passed into the method and it only gets invoked when build is on debug. I can live with new Object[]{logMessage.invoke} because it it’ll only affect debug builds.

Let us check out the call site of this method:

fun call() {
var name = getGeneratedName()
log("Tracking Name Calls", priority = Log.DEBUG) { "This $name is my name." }
useMyName(name);
}

So far, so good. Let’s dig into what gets generated.

public static final void call() {
final ObjectRef name = new ObjectRef();
name.element = "MyName";
log("Yes", 3, (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}
@NotNull
public final String invoke() {
return "This failed " + (String)name.element;
}
}));
}

Woah, what is this? It appears that when I call log , it is constructing an ObjectRef and Function0 object at the call site. Add these logs everywhere and you are creating objects that the Android Dex Compiler cannot remove.

The Power of Inline Functions

This method, in particular, should be inlined. Adding inline to the definition:

inline fun log(tag: String, priority: Int, logMessage: () -> String) {
if (BuildConfig.DEBUG) {
Timber.log(priority, tag, logMessage())
}
}

Ok we expect the method declaration to change:

public static final void log(@NotNull String tag, int priority, @NotNull Function0 logMessage) {
Intrinsics.checkParameterIsNotNull(tag, "tag");
Intrinsics.checkParameterIsNotNull(logMessage, "logMessage");
if (BuildConfig.DEBUG) {
Timber.log(priority, tag, new Object[]{logMessage.invoke()});
}
}

What? No Change? Also why does it exist in bytecode? This method still gets generated for Java consumers and inter-project usage. If this method signature no longer existed, then these consumers would not be able to reference it publicly.

The real magic is at the call site of this method:

public static final void call() {
Object name = "MyName";
String tag$iv = "Yes";
int priority$iv = 3;
if (BuildConfig.DEBUG) {
Object[] var10002 = new Object[1];
byte var9 = 0;
Object[] var8 = var10002;
Object[] var7 = var10002;
String var10 = "This failed " + name;
var8[var9] = var10;
Timber.log(priority$iv, tag$iv, var7);
}
}

As you can see, no unnecessary objects are created here. No function definition and no multiple object instances created outside the BuildConfig.DEBUG .

This is pretty much what we wanted.

You might ask, why not do this for more methods? I would say its up to your discretion, however, inlining occurs some minor cost to compilation speed.

Best Practices

Use with Lambda Arguments

It works best on lambdas because the compiler can directly inline the body (in most instances). As from earlier, without inlining, there could be serious efficiency costs.

Don’t Use For Everything

Inlining requires a cost and the cost is in the compiler. For every copy-paste, the compiler has to keep track of that method body and paste it in. Also the Kotlin/JVM compiler already optimizes certain methods and may perform some inlining anyways.

Inlining May Not Preserve Line Numbers

If you get a crash in your app or program, the stacktrace may not match up completely with the code that executes. The compiler tries to add proper line number and stack traces into the program, but this does not always line up with the source file.

You Can’t Inline Everything

Lambdas that are stored as values or passed around cannot be inlined. You can declare parameters as noinline in that case. Public inline methods that attempt to access private or internal declarations are not allowed due to possible source incompatibility from the calling module.

Extensions

Extensions allow us to resolve a static method / property onto an object that already exists. Meaning, we call the object as if it has the method or property. They are a nice replacement forstatic utility methods.

StringUtils.isNullOrEmpty("someString")// vs.
"someString".isNullOrEmpty()

Under the hood, this method is just astatic method with the first parameter of the method used as the $receiver$ . When declaring it in Kotlin, we can access the class as if we’re in the body of the method.

Given a pure Kotlin method:

object StringUtils {fun isNullOrEmpty(value: CharSequence?): Boolean = 
value == null || toString().trim { it <= ' ' }.isEmpty()
}

We can use the intention “Convert Parameter to Receiver” by pressing Option-Enter on the value parameter.

Since we’re just using it as a static method, we can remove the StringUtils object declaration and instead make it top level.

@file:JvmName("StringUtils") // provide Java support.inline fun CharSequence?.isNullOrEmpty(): Boolean =
this == null || toString().trim { it <= ' ' }.isEmpty()

There are a few things to note:

The methods are not accessible via reflection on the object instance, because they are just static methods.

assert(String::class.java.declaredMethods.find { 
method -> method.name == "isNullOrEmpty"
} != null) // will fail

Since they’re static , they can only access members in visible scope of the method, meaning private , protected, and internal (outside module) members are not usable. Also, extensions cannot hold state. val and var are only allowed with no backing property.

this is the object CharSequence , but is defined as $receiver$ when debugging.

They are much easier to discover than static utils methods.

When typing in the IDE, it will suggest the methods along with actual class methods. How many devs know that a StringHelper , StringUtils , FileUtils , DBUtils , ViewUtils, etc all exist for the object they’re using? Also, if using subtypes, it may not immediately be apparent the utils method exists, causing you to make your own duplicate or implement it a different way.

They can cleanup a cluttered API without sacrificing readability.

var View.visible: Boolean
get() = visibility == View.VISIBLE
set(value) {
visibility = if (value) View.VISIBLE else View.GONE
}
recyclerView.visible = true // so much nicer
if (recyclerView.visible)

Some Best Practices

Do not overuse extension methods.

-> While they are great for cleaning up APIs, use them only for types you cannot control.

Do:

class Person(val name: String, val age: Int) {val nameAndAge: String
get() = "$name, $age"
}

Don’t:

class Person(val name: String, val age: Int)

val Person.nameAndAge: String
get() = "$name, $age"

Adding extensions means these are decoupled from the class and if you need to change implementation, extensions can no longer be extensions to access out-of-scope members anyways.

Group Them by Utility Type

Avoid the junk file. Source

Correlate them to Java utility files, but keep them grouped by logical type. StringUtils.kt , FileUtils.kt , ViewUtils.kt , etc.

Do not mix utility method into one single file with no bearing, like KotlinUtils . Think of avoiding a junk file.

To recap, sealed classes, inlining, and extensions are features that you should use right now. If you don’t use them, there are hidden performance, code readability, and code maintainability costs. These features will only make you improve as an Android/JVM/JS/Native developer.

--

--

Fuzz
Fuzz

Published in Fuzz

Fuzz is an award-winning mobile product agency. http://fuzzpro.com

Andrew Grosner
Andrew Grosner

Written by Andrew Grosner

Senior Software Engineer @FuzzPro. Android, iOS, Web, React Native, Flutter, Ionic, anything

No responses yet