Kotlin – First Impressions

With the announcement at Google I/O this year that Kotlin will be an officially supported language for Android it seemed like a good idea to dive in and write a small sample app to get a feel for the language.

I’ve re-written the MVP app which I’ve used in other blog posts entirely in Kotlin – the full source is here on github.  This post is a quick summary of some of the language features which I find noteworthy

Gradle Changes

This point is not actually anything to do with Kotlin at all but I thought it worth mentioning as it gave me a few minutes of head scratching.  The latest version of the gradle plugin (which ship with Android Studio 3.0 beta) have deprecated the compile statement and instead use implementation

implementation 'com.google.code.gson:gson:2.8.0'

View Binding

I’ve always been a big fan of using ButterKnife for binding views.  Whilst I have read that you can get it working with Kotlin I decided to try the alternative available as part of the Kotlin Android Extensions.

To use this you firstly have to apply the plugin in your app level build.gradle file:

apply plugin: 'kotlin-android-extensions'

It’s included as part of the standard library.  Now at the top of my MainActivity class there is a special import statement:

import kotlinx.android.synthetic.main.activity_main.*

Any control in my layout xml file which I give an id to such as this RecyclerView:

<android.support.v7.widget.RecyclerView
    android:id="@+id/cats_recycler"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

Can now be referenced directly in the Activity using that id:

private fun initRecycler() {

    adapter = CatsAdapter(mutableListOf<Cat>())
    cats_recycler.layoutManager = LinearLayoutManager(this)
    cats_recycler.adapter = adapter
}

You can do the same thing in ViewHolders too but it is slightly different. My layout file is called view_holder_cat.xml.  To access this I need to add the following import statement to the top of the ViewHolder:

import kotlinx.android.synthetic.main.view_holder_cat.view.*

Now instead of accessing the values directly like you can in an Activity you have to access them as an extension of your View field:

class CatsViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {

    fun bind(cat : Cat) {

        Picasso.with(view.cat_image.context)
                .load(cat.imageUrl)
                .into(view.cat_image)

        view.cat_name.text = cat.name
        view.cat_description.text = cat.description
    }
}

Data Classes

Data classes are a great way of defining the entities which  carry data around your app in as little as one line.  Let’s take the Cat class from Java as an example:

public class Cat {

    @SerializedName("name")
    private String name;

    @SerializedName("desc")
    private String description;

    @SerializedName("image")
    private String imageUrl;

    public String getName() {
        return name;
    }
    public String getDescription() {
        return description;
    }

    public String getImageUrl() {
        return imageUrl;
    }
}

Then compare that with the same class in Kotlin:

data class Cat (
        
        @SerializedName("name")
        val name: String,

        @SerializedName("desc")
        val description: String,

        @SerializedName("image")
        val imageUrl: String
)

You can see that the Kotlin is already much shorter.  In fact it could be even more concise if I wasn’t using the GSON annotations (which still work as expected):

data class Cat(val name: String, val description: String, val imageUrl:String)

Kotlin will automatically generate getters for this class.  You also get hashCode(), equals() & toString() for free.  You can control the generation of getters/setters by changing the access modifiers of the constructor variables:

private val val var
getters n y y
setters n n y

Sealed Classes

This is where Kotlin gets really exciting.  In my Java projects I use various Data<T> classes to transfer data between various layers of the application.  Each class contains all the fields required to represent all the possible states of the data.  Here is a Java example for getting results from a model (http/db):

public class Data<T> {

    public static final int LOADING = 0;
    public static final int SUCCESS = 1;
    public static final int ERROR = 2;

    public final int status;
    public final T result;
    public final DataError error;

    public Data(int status, T result, DataError error) {
        this.status = status;
        this.result = result;
        this.error = error;
    }
}

This works well enough but Kotlin offers some big improvements.  There are a couple of problems:

  • In different states some of the fields can be null.  For example where there is an error the result field would be null.
  • There is no compiler enforcement that you check all the different states of the class.  This could be especially problematical if you later added a new state to the class and forget to implement the check in all places.

Here is a version of the same construct using a Kotlin ‘sealed class’:

sealed class Data<out T> {

    object Loading : Data<Nothing>()
    data class Success<out T>(val result : T) : Data<T>()
    data class Error(val error: DataError) : Data<Nothing>()
}

Basically a sealed classes is a wrapper around a group of other classes which confines the possible data types.  You could think of it as an extra flexible enum. They can be combined with Kotlin’s when expression:

when (data) {
    is Data.Loading -> view?.setLoadingView()
    is Data.Success -> view?.setContentView(data.result)
    is Data.Error -> view?.setErrorView(data.error)
}

There’s a few things to note here:

  • We don’t have to cast the values.  The compiler automatically does the ‘smart casting’ for you so you
  • We don’t have to null check data.result or data.error because for the Success & Error subclasses they are guaranteed to be non-null
  • In a when statement you have to account for all possible variations (or provide an else statement).  If you do not the code will not compile.

Any time where you know all the expected return types then sealed classes are perfect.

Immutable Collections

Collections in Kotlin are built right on top of the Java collections framework.  This is important for minimum friction when it comes to interop.  Kotlin however adds nice features on top of these.

The first thing to know is that by default all the standard collection interfaces (Set<>, List<>, Map<>, etc) are immutable – you can’t add or remove items from them.  The advantage that Kotlin gives you over Java is that where in Java if you try and call add/remove on an immutable collection it would throw an exception, in Kotlin the methods just don’t exist on the interface:

If you want a collection that you can add and remove items from then you have to specifically specify a mutable one – MutableList<>, MutableSet<>, MutableMap<>, etc.

As you can see in the image above Kotlin also provides a number of helper functions for quickly creating collections such as listOf().

Kotlin also provides stream methods directly on collections but I didn’t get to use them in this project.  I plan to do a full review of these in a separate post.

Static Fields & Singleton Application

In small projects where dependency injection would be overkill it is common to use an extension of the Application class as a singleton to provide global references for things such as Retrofit or Analytics libraries (the merits of this are a different issue – don’t tell me you haven’t done it).  In Java it looks something like this:

public class App extends Application {

    private static App instance;
 
    public static App getInstance() {return instance;}

    @Override
    public void onCreate() {

        super.onCreate();
        instance = this;
    }
}

Kotlin does not have static fields.  Instead there is what is called a companion object.  The Kotlin equivalent of the above would be:

class App : Application() {

    companion object {
        lateinit var instance : App
    }

    override fun onCreate() {

        super.onCreate()
        instance = this
    }
}

Now anywhere that you’d previously use App.getInstance() you can instead refer to App.instance

Another common use of static fields in Android is for holding constant values for RESULT_CODE or for EXTRAS.  The quickest replacement I found for this was to again use the companion object like this:

companion object {

    @JvmField
    val EXTRA_CODE : String = "extra_code"

    @JvmField
    val RESULT_SIGN_IN : Int = 1001
}

The @JvmField annotation tells Kotlin that you would like this field to appear as a static field when accessing it from Java classes.  This is ideal for when you are interOpping (verb?) between Kotlin and Java.

Frustrations

With anything new you are bound to have some little frustrations and Kotlin is no exception.  In truth I have only one real complaint and it’s more of an IDE issue than a language one.

I’m used to defining variables in Java declaring the type first.  After this 95% of the time the IDE then suggests a sensible variable name which I can tab accept and be on my way.  Because in Kotlin you have to type the variable name first it’s twice the typing.

I have found in constructor parameters the IDE does already suggest the type and variable name as a single selection whilst you are typing.  This does not happen in other places though.

The solution I have found for now is to use IntelliJ’s postfix completion.  You specify the type you want and follow it with .val or .var:

When you accept the postfix then IntelliJ will suggest the variable names for you and give some extra options:

Conclusion

I’ve never been particular bothered by the verbosity of Java.  Most of the time I have tools or libraries which do most of the heavy lifting for me anyway.  So what excites me about Kotlin is not so much its conciseness but rather the language features which make it harder to make foolish mistakes.

The null safety, immutability by default and the compiler checks offered by sealed classes all mean that the opportunities to make ‘unforced errors’ are drastically reduced.  I will definitely be using it more.