zsmb.coBeta 3



Top 10 Kotlin Stack Overflow questions, pt 3 - nulls and such

This is the written version of a talk I gave recently, and since it was quite long, this is the third of three articles about it. If you've missed it, here's the first and second articles.


Nullability

6. Smart casts on mutable properties

Why doesn't smart cast work on mutable properties?

Here's what the problematic code looks like:

class Dog(var toy: Toy? = null) {
    fun play() {
        if (toy != null) {
            toy.chew()
        }
    }
}

toy is expected to be smart cast inside the if block from Toy? to Toy, as it has been checked for nullability. However, this error appears:

Kotlin: Smart cast to 'Toy' is impossible, because 'toy' is a mutable property that could have been changed by this time

The issue is that this Dog instance could be modified from another thread between the time the toy != null check is made, and the toy.chew() method is called. This could potentially cause a NullPointerException.

What can I do to make it work?

The issue doesn't exist if the property is not mutable. It's a good idea to make everything (both properties and variables) a val by default, and only make them mutable if you must.

If they really have to be mutable, using a local immutable copy solves the problem - smart cast works as expected on an immutable variable:

class Dog(var toy: Toy? = null) {
    fun play() {
        val _toy = toy
        if (_toy != null) {
            _toy.chew()
        }
    }
}

But of course, there's a nicer way to make a temporary copy:

class Dog(var toy: Toy? = null) {
    fun play() {
        toy?.let { 
            it.chew()
        }
    }
}

At this point, you should probably have noticed the obvious solution for this simple case of calling just a single function on the property in question:

class Dog(var toy: Toy? = null) {
    fun play() {
        toy?.chew()
    }
}

But the solution using let works for the cases where you do more complex things after your null check.

7. null!!

Why exactly does null!! crash?

Here's some example code of a property which is meant to be non-null, but for some reason can't be initialized when the class is constructed. Putting !! after null lets the code compile, but at what cost? Any time an Episode is constructed, it will crash with a KotlinNullPointerException.

class Episode {
    var airdate: Date = null!!
}

The problem here is that the !! operator is often perceived to be similar to the safe call operator ?., and therefore thought to not do anything in the code above, since there's nothing on its right hand side anyway. It's also sometimes thought to be a compile time signal to the compiler to "just let your code compile", which it is not.

Let's take a look at another code sample:

fun scheduleEpisode(airdate: Date?) {
    airdate!!.getWeekday()
}

The most important point here is that the !! operator can exist and does its job without anything following it. While airdate is the first expression of the line, the next expression is not airdate!!.getWeekday(), but instead just airdate!!. The way this is evaluated is the following:

  • If airdate is non-null, it returns airdate, but now as a non-nullable type, in this case, Date, which is why the getWeekday method can be then called without a safe call operator.
  • If airdate is null, it throws a KotlinNullPointerException.

You can think of !! as having the same functionality as an extension function like this would:

fun <T> T?.forceNonNull(): T {
    if (this == null) {
        throw KotlinNullPointerException("Oops, found null")
    } else {
        return this as T
    }
}

So the previous usage of !! would be equivalent to this:

fun scheduleEpisode(airdate: Date?) {
    airdate.forceNonNull().getWeekday()
}

Finally, let's get back to our original problem:

class Episode {
    var airdate: Date = null!!
}

The issue should now be obvious. Before the value of the expression null!! is assigned to the property, it has to be evaluated, with throws the exception.

8. Platform types

How do I decide the nullability of a parameter when overriding a Java function? What are the consequences of each choice?

Let's return to our OnClickListener class defined in Java, with a single callback method.

public interface OnClickListener {
    void onClick(Button button);
}

We'll assume that sometimes a valid Button instance isn't provided for the click event (perhaps the Button is disabled by the time the listener is called). We'll also forget to annotate these parameters in the Java code accordingly.

Let's override this interface in Kotlin - we'll end up with the following method signature if we let IDEA generate it:

class KtListener: OnClickListener {
    override fun onClick(button: Button?): Unit {
        val name = button?.name ?: "Unknown button"
        println("Clicked $name")
    }
}

Since the Java method has no nullability annotations, the parameters of the Kotlin function will have a platform type, in this case, Button!. This means that it's up to us to decide (based on experience, or hopefully, documentation) whether the parameter received by the method can be null.

By default, it's safer to use the nullable types for all parameters, and suffer the restrictions that the compiler forces on us to handle these safely. This results in some extra code, but is always safe.

For parameters which are known never to be null, the non-nullable type can be used instead - both will compile. If the method does get called with a null value for a parameter we marked to be non-null, an injected null check will produce an IllegalArgumentException before any of the code in the method body would run, making this a slightly riskier choice. Of course, having a non-nullable parameter simplifies our code:

class KtListener: OnClickListener {
    override fun onClick(button: Button): Unit {
        val name = button.name
        println("Clicked $name")
    }
}

Small bonus tip

9. Import aliases

How can I use multiple extension functions with the same name in a file?

Let's say you implement two different indent extension functions on the String class in two different packages. If this was an ordinary function, you could call it with a fully qualified package name, however, the syntax doesn't allow for it with an extension function - there's quite simply nowhere to put the package name:

"hello world".indent()

However, you can use the as keyword at the import statement to rename one (or both) of them, like so:

import com.example.code.indent as indent4
import com.example.square.indent as indent2

Which then lets you use each of them by their new names:

"hello world".indent4()

Another use case is that you could be using two classes with the same name from different packages in the same file (e.g. java.util.Date and java.sql.Date), and you don't want to use either of them by their fully qualified name. You could do the following here:

import java.util.Date as UtilDate
import java.sql.Date as SqlDate

Within this file, you can now refer to these classes by their aliases.

Wrap-up

10. Kotlin vs Java

Which one should I learn if I'm just starting to learn Android development?

❓ ❓ ❓ ❓ ❓ ❓ ❓ ❓ ❓ ❓ ❓

I'd consider nothing other than Xamarin. Don't waste your time on native development, it's 2018. /s


Anyhow, that's a wrap, thank you for reading this series, and see you on StackOverflow :)