Kotlin Idioms

It has been a while since I start using Kotlin as my first class language when developing Android app. It's efficiency and easy to use, though it might have something we all should pay attention to. I think it's time to origanize some useful idioms that we should apply when writing Kotlin :)

Type-safe builder

Consider the class written in C#:

public class Image
{
    public double Width { get; set; }
    public double Height { get; set; }
}

You must miss initializing the properties using this pattern:

var image = new Image()
{
    Width = 50,
    Height = 50
};

It's a language level syntax. In Java, you are not able to to like this. But in Kotlin though, you can use function type with receiver to do the same thing.

Consider the class written in Kotlin:

class Image(apply: (Image.() -> Unit)? = null) {
    var width = 0.0
    var height = 0.0

    init {
        apply?.invoke(this)
    }
}

Due to this fact:

In Kotlin, there is a convention that if the last parameter to a function is a function, and you're passing a lambda expression as the corresponding argument, you can specify it outside of parentheses

you can use the same syntax in C# to initialize the properties after constructing a object.

val image = Image {
    width = 1.0
    height = 1.0
}

Cooperate well with ButterKnife

You must aready know ButterKnife if you are not a findViewById lover. In java, you use ButterKnife to bind view like this:

@BindView(R.id.user_name)
EditText mEditText;

However, since Kotlin is a null-safe language, you must decide a property to be nullable or not when you declare it. Thus the code below is not allowed:

@BindView(R.id.user_name)
val editText : EditText

even you know the ButterKnife will help us inject the property later.

So you may write the code like this:

@BindView(R.id.user_name)
var editText : EditText? = null

However, every time you access its methods or properties you must use ? or !!, due to its a nullable type even you have already known this property won't be null at all.

But you may have forget the lateinit key word: it means that I guarantee that this property will be initialized later. Thus you can write the code like this:

@BindView(R.id.user_name)
lateinit var editText : EditText

Use higher-order functions instead of interface in some cases

Remember that you can pass a lambda to the setOnClickListener() method?

view.setOnClickListener { v-> Log.d("Main", "View ${v::class.java} clicked." }

You don't have to pass a anonymous inner class instance to the method since it has only a method to override, and Kotlin does the convert for us.

In fact, if you have a async work to do in a function and should send callback to the caller, it's a good way to use higher-order function. Look at the sample below:

fun doSomethingHeavy(callback: (result:String) -> Unit) {
    // do heavy work
    callback.invoke("ok")
}

doSomethingHeavy { result ->
    // handle the result
}

Invent your own DSL

Writing code just like writing an article:

val user = getUser()
user isDead { sendToHospital(this) } otherwise { talkTo(this) }

It's a native kotlin code, and it's not hard to implement. If you are familar with infix function, you already know how it works:

inline infix fun isDead(apply: User.() -> Unit): User {
    if (dead) {
        apply.invoke(this)
    }
    return this
}

inline infix fun otherwise(apply: User.() -> Unit): User {
    if (!dead) {
        apply.invoke(this)
    }
    return this
}

With inline infix, you can call a function with using a .(dot) and accept another parameter.

Target of a annotation

Be careful of the @Target while writing a anotation class. Let's say we have a annotation class named Exclude:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
annotation class Exclude

It tells the compiler that the annotation is stored in binary output and visible for reflection, and it targets to fields or properties, which means that you can attach to the fields or properties, like this in Java:

@Exclude
private String name;

or in Kotlin:

@Exclude
var name: String? = null

Only a question: are the code blocks the same after being compiled? The answer is NO. The property in Kotlin equals the below things in Java: a private field, a public setter of the field and a public getter of the field. The bytecode of the property is like this:

  // access flags 0x2
  private Ljava/lang/String; name
  @Lorg/jetbrains/annotations/Nullable;() // invisible

  // access flags 0x11
  public final getName()Ljava/lang/String;
  @Lorg/jetbrains/annotations/Nullable;() // invisible
   L0
    LINENUMBER 4 L0
    ALOAD 0
    GETFIELD com/by/butter/camera/adapter/User.name : Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Lcom/by/butter/camera/adapter/User; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x11
  public final setName(Ljava/lang/String;)V
    @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0
   L0
    LINENUMBER 4 L0
    ALOAD 0
    ALOAD 1
    PUTFIELD com/by/butter/camera/adapter/User.name : Ljava/lang/String;
    RETURN
   L1
    LOCALVARIABLE this Lcom/by/butter/camera/adapter/User; L0 L1 0
    LOCALVARIABLE <set-?> Ljava/lang/String; L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2

which tells us there is a private field named name with String type, a final method called getName() and a final method called setName(). Let's attach the Exclude annotation and see what's going on:

// access flags 0x2
  private Ljava/lang/String; name
  @Lorg/jetbrains/annotations/Nullable;() // invisible

  // DEPRECATED
  // access flags 0x21009
  public static synthetic name$annotations()V
  @Lcom/by/butter/camera/api/v4/Exclude;()
    RETURN
    MAXSTACK = 0
    MAXLOCALS = 0

  // access flags 0x11
  public final getName()Ljava/lang/String;
  @Lorg/jetbrains/annotations/Nullable;() // invisible
   L0
    LINENUMBER 7 L0
    ALOAD 0
    GETFIELD com/by/butter/camera/adapter/User.name : Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Lcom/by/butter/camera/adapter/User; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x11
  public final setName(Ljava/lang/String;)V
    @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0
   L0
    LINENUMBER 7 L0
    ALOAD 0
    ALOAD 1
    PUTFIELD com/by/butter/camera/adapter/User.name : Ljava/lang/String;
    RETURN
   L1
    LOCALVARIABLE this Lcom/by/butter/camera/adapter/User; L0 L1 0
    LOCALVARIABLE <set-?> Ljava/lang/String; L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2

The private field name is still no infomation about Exclude annotation, which means that when you get annotation of the name field in runtime, you won't get the annotation.