Skip to content

2.) Basic Kotlin Features

Gabor Varadi edited this page Dec 17, 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 { ... } // 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 { ... } // 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 (and complex conditions, such as ranges)

control statement as expression (assignment of when, return, ...)

vararg and the * spread operator

interfaces and default implementation

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

Clone this wiki locally