Kotlin Frist Impressions

Kotlin was picked by Google as the default solution for building Android apps, has amazing support on the server through its Spring integration, and now, thanks to Kotlin Multi Platform you can write your code once and then run it on the server, mobile devices, desktop and the web!

Knowing Kotlin is a great asset these days, and learning it is easier than you might think. So let’s not waste any more time and look at some Kotlin code. There are a couple of details and conventions you should be aware of from the get-go.

First, Kotlin is built on top of the Java Virtual Machine. The JVM operates on a platform independent binary format called bytecode. This is the glue allowing your code to run on any device equipped with a JVM.

So, let’s go ahead and install both Java and Kotlin locally using SDK Man.

$ sdk install java
$ sdk install kotlin

Then, in a new file I’m defining a basic hello world function.

fun main() {
    println("Awesome!");
}

Back in the terminal, we’ll use the Kotlin compiler command to pack our code and the Kotlin runtime in a JAR file, which can then be executed on the JVM.

$ kotlinc Main.kt -include-runtime -d Main.jar
$ java -jar Main.jar

Of course, since both Java and Kotlin share the same platform, Kotlin code can easily interoperate with Java code, so you’ll get all the benefits of both a modern language and a mature, battle tested ecosystem.

Ok, now that we know how to compile and run Kotlin apps, let’s go back to our file, and write something a bit more interesting.

The main function is the entry point into our program, and, in here, I am going to iterate over an inclusive range, initialize some Movie objects from data entered in the terminal, and add all those objects to a List.

Then, we’ll filter out movies with a score smaller than 5, map the list of movies to a list of names, and finally print each of those strings.

data class Movie (
    val name: String?,
    val score: Int
)

fun main() {
    val movies = mutableListOf<Movie>()

    for (i in 1..3) {
        val name = readLine()
        val score = readLine()?.toInt() ?: 0
        movies.add(Movie(name, score))
    }
    movies
        .filter { it.score > 5 }
        .map { it.name }
        .forEach(::println)
}

While this code might not look like much, it actually uses a lot of the features that make Kotlin such a great language so let’s look at them in more detail.

At a first glance, you should notice at least 3 of the main concepts behind Kotlin.

Immutability

First, there is a high emphasis on immutability, which is a must when developing modern scalable apps. If you really need mutable properties or variables, you have to use the “var” keyword. The same goes for data collections, where we had to explicitly use a mutable factory function - mutableListOf.

Non-nullable

Second, in Kotlin types are non-nullable by default. This means that you cannot assign null to a variable unless you explicitly declare it as nullable - ‘String?’.

This is a great mechanism to eliminate the dreaded Billion Dollar Mistake. The development experience is also enhanced thanks to the safe call operator - ‘readLine()?.toInt()’ - which allows you to safely access a property or method of a nullable object, and the Elvis operator - ?: - used to provide default values if expressions are evaluated to null.

Strong Types

Finally, Kotlin is strongly typed with a compiler that supports type inference, explicit conversions, smart casting and generics.

fun printLength(data: Any) {
    if (data is String) {
        println("String length: ${data.length}")
    }
}

While smart casting is a really great feature that allows you to remove some of the boilerplate, we should probably briefly discuss generics in more detail, since you’ll definitely run into them whenever working with Kotlin collections.

Generics

This might not be obvious thanks to Kotlin’s type inference, but, trust me, those types are still there under the hood.

val numbers = listOf<Int>(1, 2, 3)

Generics allow you to define types that have other types parameters. When an instance of such a type is created, type parameters are substituted with specific types called type arguments.

data class Square<T>(val side: T) {
    fun area() = when (side) {
        is Double -> side * side
        is Int -> side * side
        else -> throw IllegalArgumentException("Unsupported")
    }.toDouble()
}

fun main() {
    val one = Square<Int>(5)
    val two = Square<Double>(5.5)

    println("${one.area()}, ${two.area()}")
}

Also note the use of data classes, which provide a concise way to create structures that hold data. While this doesn’t look like much, it makes a big difference especially for developers with a Java background since methods like equals(), hashCode(), toString() or copy() are auto generated.

Component Functions

On top of all these, you get access to generated component functions which allow you to destructure objects.

data class Movie(
    val name: String,
    val score: Int
)

fun main() {
    val movie = Movie("The Gentlemen", 10)
    val (name, score) = movie
    println("$name $score")
}

Object Expressions

Another convenient feature when working with classes is the use of object expressions. This is useful when you need a quick way to specialize a class’s behavior or to implement an interface without declaring a full class.

val movie = object {
    val name = "The Gentlemen"
    val score = 10
    override fun toString() = "$name $score"
}
print(movie)

While tempting, you should avoid overusing object expressions for complex structures or behaviors that are used across your application.

By the way, you’ll see throughout this video that Kotlin goes out of its way to provide a clean yet expressive syntax to improve code readability.

Named Parameters

For instance, we can use named parameters when calling functions and constructors, which solves the problem of long lists of function arguments which are pretty difficult to keep track of.

data class Movie(
    val name: String,
    val score: Int
)

fun main() {
    val movie = Movie(name = "The Gentlemen", score = 10)
    println(movie)
}

Functional APIs

Before moving to more advanced concepts, let’s quickly mention some of the functional APIs exposed by Kotlin collections. The functional style provides many benefits, and is one of the main mechanisms you can use to simplify your code.

data class Movie(
    val name: String?,
    val score: Int
)

fun main() {
    val movies = mutableListOf<Movie>()

    for (i in 1..3) {
        val name = readLine()
        val score = readLine()?.toInt() ?: 0
        movies.add(Movie(name, score))
    }
    movies
        .filter { it.score > 5 }
        .map { it.name }
        .forEach(::println)
}

The filter, map, and forEach functions form the basis for manipulating collections and they accept both lambda functions and function references as arguments.

Advanced Features

The easiest way to see the value in some of the advanced features employed by Kotlin (regular syntax, clean syntax, feature) is to look at a list of examples provided in the Kotlin in Action book, which, by the way, is a must if you want to get a good understanding of the fundamentals.

Extension Functions

Through extension functions you can easily extend classes without having to inherit from them.

fun String.isValidEmail(): Boolean {
    return this.contains("@") && this.contains(".")
}

val String.domain: String
    get() = this.substringAfter('@').substringBefore('.')

val email = "hi@awesome.club"
println(email.isValidEmail()) // Output: true
println(email.domain) // Output: awesome.club

Properties can also be extended, and note that extensions do not actually modify the extended classes. They provide a way to call a function on an instance of a class as if it were a method of that class.

Infix Notation

Kotlin’s infix notation is another useful little language feature, which allows your code to be more fluent and intuitive. The only constraint is that these functions must have a single parameter.

infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

val pair = "key" to 42 // Same as "key".to (42)
println(pair) // Output: (key, 42)

Overloading Operators

Overloading operators is a common feature in modern languages, and Kotlin also supports this, but with a couple of caveats that remove the complexity and increase maintainability.

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }

    operator fun minus(other: Point): Point {
        return Point(x - other.x, y - other.y)
    }
}

val p1 = Point(1, 2)
val p2 = Point(3, 4)

val p3 = p1 + p2 // Point(x=4, y=6)
val p4 = p1 - p2 // Point(x=-2, y=-2)

println(p3)
println(p4)

There is a limited set of operators you can overload, and each one corresponds to the name of the function you need to define in your class.

Higher-Order Functions

However, probably the thing I like the most about Kotlin is the great dev experience offered by its higher-order function support. Lambda expressions can be easily passed as arguments or returned as results, and your code becomes more concise thanks to small syntactic details like dropping the parenthesis when the lambda is the last parameter expected, or the implicit single parameter.

data class Movie(val name: String)

val movies = listOf(
    Movie("The Gentlemen"),
    Movie("The Boys")
)

movies.forEach {
    println("${it.name}")
}

And, since we are talking about advanced features, we should probably mention async programming via coroutines.

Coroutines

In short, Coroutines convert async callbacks for long-running tasks, such as database or network operations, into sequential code.

fun main() {
    println("Start")

    // Launching a coroutine in the GlobalScope
    GlobalScope.launch {
        delay(1000)
        println("After delay")
    }

    // Main thread continues while coroutine is delayed
    println("End")

    // Wait for the coroutine to finish (not recommended with GlobalScope)
    Thread.sleep(2000)
}

They allow for writing code that is linear and easy to read while still performing non-blocking operations.

If you found this video useful, you should watch some of my youtube videos or subscribe to the newsletter.

Until next time, thank you for reading!