Skip to content

2.) Basic Kotlin Features

Gabor Varadi edited this page Dec 19, 2018 · 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: List<Item> = Collections.emptyList()

    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!
}

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!
}

properties, backing fields

As mention along the syntax quirks, any field is defined as either val (final field with getter) or var (field with getter/setter).

It is however also possible to execute custom getter/setter logic, and manipulate the visibility of the getter/setter.

var name: String = ""
     private set(value) {
         field = value
         println("Name was set to $value")
     }

In this case, the setter is private, the getter is public, and we also execute custom logic.

An important to thing to note is that there is a difference between the following two:

val hello = "Hello!"

val hello: String
     get() = "Hello!"

Because name is initialized as a field and cannot be changed, but the second option does not "retain" the value, and can potentially change.

private var name: String = ""

val hello: String
     get() = "Hello $name!"

In this case, changing name then calling hello will yield different results.

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.

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

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.

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

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

We've already seen that interface can contain val and var and fun but we haven't seen that fun can actually have an implementation that will be provided as default.

What's more interesting is that this works even before Java 8, in fact, if we want to opt into using Java 8's default methods, we must use @JvmDefault.

interface Animal {
    fun makeSound() {
        println("Growls.")
    }
}

class Dog : Animal {
}

val dog = Dog()
dog.makeSound() // prints "Growls."

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, and that there is in/out which is sometimes needed (the compiler generally tells you that it is required).

Clone this wiki locally