Skip to content

2.) Basic Kotlin Features

Jean-Michel Fayard edited this page Jan 6, 2019 · 19 revisions

typed nullability, and null-safety operators (?., ?:)

If you've heard about Kotlin, you've probably heard litanies about "null safety".

While it's still possible to get NPEs if you aren't paying attention:

  • specifying nullable platform-type as non-nullable

  • invoking anything on an uninitialized lateinit variable

  • using !! on a nullable and actually null value

It's definitely true that you can reduce the number of necessary null checks just by restricting input arguments to be non-null, and you can also ditch some nested conditions by using safe-call operator (?.) and the Elvis-operator (?:, think if-null-then).

For example, one could write the following Java code:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
    private List<Item> items = null;

    public void updateItems(List<Item> items) {
        this.items = items;
        notifyDataSetChanged();
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.my_item, parent, false));
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.bind(items.get(position));
    } 

    @Override
    public int getItemCount() {
        return items == null ? 0 : items.size(); 
    }
}

You could first write the following Kotlin code:

class MyAdapter: RecyclerView.Adapter<MyAdapter.ViewHolder>() {
    private var items: List<Item>? = null

    fun updateItems(items: List<Item>?) {
        this.items = items
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
        return ViewHolder(LayoutInflater.from(parent!!.getContext()).inflate(R.layout.my_item, parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(items!!.get(position))
    }

    override fun getItemCount(): Int {
        return items?.size ?: 0
    }
}

But do we reeeeeally want to enable setting a null into this adapter? I think not.

class MyAdapter: RecyclerView.Adapter<MyAdapter.ViewHolder>() {
    private var items = listOf<Item>()

    fun updateItems(items: List<Item>) {
        this.items = items
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
        ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.my_item, parent, false))
    
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(items[position])
    }

    override fun getItemCount(): Int = items.size
}

smart casting (and mutable vars gotcha)

In Kotlin, if we do a check against the type of a class, we can invoke functions on it without a need to cast it again with as T.

However, we should also be aware that this only works if the class is not a nullable mutable variable.

private var realm: Realm? = null
private var realmResults: RealmResults<T>? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    realm = Realm.getDefaultInstance()

    realmResults = realm.where().findAllAsync() // ERROR

    // realmResults = realm?.where()?.findAllAsync() // works but it's ugly
}

Because then we'll get an error: smart-casting is impossible, this value could have changed over time.

This means that calling realm.where is not possible, because realm could have potentially been changed to null by another thread. Even if we know this is not the case, Kotlin won't permit this. We'll have to keep a reference to the non-null instance to use it as a non-null value. Or we can specify the object as lateinit.

private var realm: Realm? = null
private var realmResults: RealmResults<T>? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    val realm = Realm.getDefaultInstance()
    this.realm = realm

    val realmResults = realm.where().findAllAsync() // works!
    this.realmResults = realmResults

    realmResults.addChangeListener(RealmChangeListener { ... }) // also works!
    // note: I need to specify the Java interface explicitly, because 
    // this method actually has multiple overloads. 
}

lateinit vars

If we know that a property will be initialized only once (but not by the constructor), then we can set it to be a lateinit var which means "we guarantee that this will be non-null upon any actual access to it".

Please note that incorrect access results in KotlinUninitializedPropertyAccessException.

private lateinit var realm: Realm
private lateinit var realmResults: RealmResults<T>

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    realm = Realm.getDefaultInstance()
    realmResults = realm.where().findAllAsync() // works!
    realmResults.addChangeListener(RealmChangeListener { ... }) // also works!
}

string interpolation and """multiline escaped ${strings}"""

While it's been used in previous examples, string interpolation is preferred in Kotlin against string concatenation. We can use $ for this.

val hello: String
    get() = "Hello $name, your overlord ${overlord.name} has been expecting you."

What's also really nice is that you can use multi-line escaped strings, which lets you easily add a JSON to your project without littering it with escape characters.

    val jsonString = """
        {
        	"hello": "world",
	        "another": {
                    "field": "field",
                    "boom": "boom"
	        }
        }
    """.trimIndent()

data classes

Have you heard about data classes? They're the most talked about feature in Kotlin for its "easy-to-demonstrate boilerplate reduction".

Technically it's true, although it's also a pain that you need to have at least 1 constructor argument to use it.

Either way, the way it works is that if you specify a class as data class, then it will generate an equals, hashCode and toString function automatically (along with copy()).

data class Dog(
    val name: String,
    val owner: Person
)

However if you are in a pinch and need a data class but have no arguments, I tend to use the following trick:

@Parcelize
data class LoginKey(val placeholder: String = ""): Parcelable

This is because while using object for no-arguments is commonly recommended in place of empty data class, it actually has different behavior (the toString() won't be consistent across processes), see https://youtrack.jetbrains.com/issue/KT-4107

when keyword

The when keyword is a "switch-case statement on steroids".

when statements are super-powerful, because they can be combined with complex conditions, such as:

  • checking if an int is in a range of x..y

  • checking if a class is of a particular type (sealed class) or enum value

  • *Kotlin 1.3: allows creating a variable within the when statement

Here are a few examples:

val value = when(number) {
    0,1,2,3,4 -> 1.0
    in 5..9 -> 0.75
    in 10..14 -> 0.5
    in 15..19 -> 0.25
    20 -> 0.2
    else -> 0
}

or

enum class Colors {
   RED,
   GREEN,
   BLUE
}

val color = Colors.RED

when(color) {
    Colors.RED -> {
       ...
    }
    else ->
       ...
    }
}

Note/Tip: a when {} expression is forced by the compiler to be exhaustive only if it is used as part of an assignment.

For this, we can use the following trick:

fun Unit.safe() {}
fun Nothing?.safe() {}
fun Any?.safe() {}

in which case it looks as:

when(color) {
    Colors.RED -> {
       ...
    }
    else ->
       ...
    }
}.safe()

control statement as expression (assignment of when, return)

Control statements in Kotlin (such as when or return can be used as part of assignments.

We've already seen when {}, but there are also other tricks one can do.

For example, combining the ?: operator with return.

val name = tryGetName() ?: return // returns if `tryGetName()` returned `null`

named arguments, default arguments

In Kotlin, if you feel that a given set of arguments is unclear, you can specify the name at the call-site.

But if you have default arguments, then you can "skip" certain arguments while specifying other given arguments.

@JvmOverloads
fun printStrings(first: String = "Hello", second: String = "World") {
    println("$first $second")
}

We can call this in any of the following ways:

printStrings()
printStrings("Goodbye", "my dear")
printStrings(first = "Goodbye", second = "my dear")
printStrings(first = "Goodbye")
printStrings(second = "my dear")

If you have added the @JvmOverloads notation, this will work in Java too!

This also applies to constructor arguments.

vararg and the * spread operator

We already had the ability to specify a vararg method in Java as void doSomething(String... values).

Kotlin has a new syntax for this, and an operator that is worth knowing about.

fun doSomething(vararg values: String)

The values can be accessed as any array. However, what's important is that if we are passing these values one by one to another vararg function, then we must use the spread operator *.

Here's a real-world example to show what that is like.

fun animateTogether(vararg animators: Animator) = AnimatorSet().apply {
    playTogether(*animators)
}

In this case, we can see that the animators passed to animateTogether are passed one-by-one to the playTogether vararg function of AnimatorSet.

interfaces and default implementation

In Java, if you add a method to an existing interface, you will break the existing implementation of this interface.

Not so in Kotlin because you can define a default implementation of either a val/var property or even a method of the interface.

interface Animal {
    fun makeSound() { // default implementation
        println("Growls.")
    }
    val genus: String
        get() = "" // default implementation
}

class Dog : Animal {
}

fun main() {
    val dog = Dog()
    dog.makeSound() // prints "Growls."
    dog.genus // ""
}

What's more interesting is that this works even before Java 8 (though you opt-in with the annotation @JvmDefault).

Instead Kotlin Bytecode > Decompile shows that the compiler is creating a static class Animal.DefaultImpls

generics (<T: Blah>, in/out, and star projection <*>)

Generics are a bit tricky in Kotlin. In Java, we had T, ?, ? extends T and ? super T.

But in Kotlin, we have the following scenarios:

fun <T: View> findViewById(view: View, @IdRes idRes: Int) = view.findViewById(idRes) as T

val activityType: Class<out Activity> // similar to `? extends T`
    get() = when(this) {
        CAR_HEADER -> CarHeader::class.java
        CAR_VIEW -> CarView::class.java
        DATA_VIEW -> DataView::class.java
    } 
}

val clazz: Class<*> = SomeClass::class.java

Where we can see that a "raw type" can in some cases be replaced with star-projection (if a generic is needed but Any? does not work), that we can supply bounds for generic method arguments as MyClass<*>, and that there is in/out which is sometimes needed (the compiler generally tells you that it is required).