Skip to content

Kotlin Primer

Functions

Functions in Kotlin are similar to methods in Java, with some big exceptions. Three of the biggest ones are

  • Top-level functions that are not inside a class
  • Nested functions
  • Extension functions

Top-level functions

Kotlin can run on top of the Java Virtual Machine (JVM), which requires methods to exist inside classes. So how does Kotlin allow top-level functions?

By generating a dummy class for each file.

You write your Kotlin code in ".kt" files, such as "HiZev.kt" or "CrunchyFrog.kt". Kotlin generates a class for these files by appending "Kt" to the file name, such as HiZevKt or CrunchyFrogKt.

If the file contains a top-level main function, you can call it at runtime using the class name. For example, if we define

// file KotlinMain.kt
package com.androidbyexample.kotlin

fun main() {
    println("Hello")
}

we can run class com.androidbyexample.kotlin.KotlinMainKt to run that main. We can do this in a Gradle script:

plugins {
    kotlin("jvm") version "2.4.10"
    application
}

application {
    mainClass.set("com.androidbyexample.kotlin.KotlinMainKt")
}

The application plugin (which we won't be using after this module) provides a run task, so we can call

./gradlew run

and the main function will run.

Nested functions

You can nest functions as well, which can be useful for capturing common logic that's reused inside a function but not outside of it, or wrapping recursive logic. We'll be using nested functions to access parameters of the surrounding function as well. When we're creating a graph example, we'll be drawing several shapes. Drawing these shapes requires access to many parameters from surrounding function.

Note

We'll be using Jetpack Compose to create our user interface, and things will look a wee but different, but the nesting and parameter passing is the same.

fun drawTriangle(
    // need triangle color, shape size, border width, and location to draw
) {
    // do drawing
}

// similar for drawSquare and drawCircle

fun drawEverything(
    // triangle color, square color, circle color, shape size, border width
) {
    drawTriangle( /* pass all parameters */)
    drawSquare( /* pass all parameters */)
    drawCircle( /* pass all parameters */)
}

That's a lot of parameter passing, and we're only ever using the shape-drawing functions inside drawEverything(). If we nest the functions, we have direct access to the parameters passed to drawEverthing():

fun drawEverything(
    // triangle color, square color, circle color, shape size, border width
) {
    fun drawTriangle(
        // location to draw
    ) {
        // do drawing
    }

    // similar for drawSquare and drawCircle

    drawTriangle( /* pass just location */)
    drawSquare( /* pass just location */)
    drawCircle( /* pass just location */)
}

This can be very useful, but the tradeoff is that drawEverything() is larger.

Single-expression syntax

Kotlin loves expressions, and they can often greatly reduce the size of your code.

fun factorial(n: Int): Int {
    if (n == 1) {
        return 1
    } else {
        return n * factorial(n-1)
    }
}

fun main() {
    println(factorial(4))
}

As a first step in reducing the code, if is an expression in Kotlin, so we can

fun factorial(n: Int): Int {
    return if (n == 1) {
        1
    } else {
        n * factorial(n-1)
    }
}

fun main() {
    println(factorial(4))
}

Wow - we're returning the results of the if!

Note

Because Kotlin doesn't require semicolons, the return must be on the same line as the if in this case. Otherwise, it's treated as ending the function without returning a value, which will cause a compiler error here.

Note that the function body is just a single expression, the if expression. We can further reduce the function by replacing the {...} with a = and removing the return:

fun factorial(n: Int): Int =
    if (n == 1) {
        1
    } else {
        n * factorial(n-1)
    }

fun main() {
    println(factorial(4))
}

And because Kotlin can infer types of an expression, let's remove the return type from the function declaration:

fun factorial(n: Int) =
    if (n == 1) {
        1
    } else {
        n * factorial(n-1)
    }

fun main() {
    println(factorial(4))
}

Ooops! Recursion can cause some issues when inferring the type. We'll get

Type checking has run into a recursive problem. Easiest workaround: specify the types of your
declarations explicitly.

So in this case, we must keep the return type. For most single-expression functions, we won't need the return type to be explicitly specified.

Extension functions

There are often times when you really wish you could add functionality to a class. Maybe the class cannot be extended, but it would feel really great if you could add a few functions anyway...

Kotlin allows you to create "extension functions", which make it feel like you've really added a function to a type. Let's say we have a Square shape that we'd like to add a getCenter() function:

data class Point(
    val x: Int,
    val y: Int,
)

data class Square(
    val offset: Point,
    val size: Int,
)

// extension function!
fun Square.getCenter() =
    Point(
        offset.x + size / 2,
        offset.y + size / 2,
    )

fun main() {
    val square = Square(
        Point(100, 100),
        size = 20,
    )
    val center = square.getCenter()
    println(center)
}

Extension functions are defined using the name of the class to extend, followed by . and the name of the function. They can access any public data inside the class. When called:

val center = square.getCenter()

the receiver (the expression before the .) is passed as this to the body of the function.

Note

Under the covers, this is all syntactic sugar. The function gets an implicit first parameter of the receiver type, and the calling receiver is passed as the first parameter to the function.

You can also define extension properties:

data class Point(
    val x: Int,
    val y: Int,
)

data class Square(
    val offset: Point,
    val size: Int,
)

val Square.center: Point
    get() =
        Point(
            offset.x + size / 2,
            offset.y + size / 2,
        )

fun main() {
    val square = Square(
        Point(100, 100),
        size = 20,
    )
    val center = square.center
    println(center)
}

This becomes incredibly useful when chaining functions. For example:

data class Point(
    val x: Int,
    val y: Int,
)

data class Square(
    val offset: Point,
    val size: Int,
)

fun Point.niceString() = "($x, $y)"

fun String.indent(spaces: Int) =
    "${" ".repeat(spaces)}$this"

fun Square.getCenter(): Point =
    Point(
        offset.x + size / 2,
        offset.y + size / 2,
    )

fun main() {
    val square = Square(
        Point(100, 100),
        size = 20,
    )
    println(
        square
            .getCenter()
            .niceString()
            .indent(4)
    )
}

If we didn't have extension functions, we'd need to assign results to intermediate properties or pass more parameters.

There are some limitations to extension functions/properties, however...

  • Cannot be overridden (polymorphism doesn't apply because they're statically resolved at compile time)
  • Cannot access non-public data inside the class
  • Cannot have backing fields (for extension properties - so no adding data to a class)
  • Member functions with the same signature win (see the note below)

Note

If the signatures of an extension function and member function are the same, the member functions will silently win. Personally, I think this was a horrible language-design choice, and it should have been reported to the programmer for explicit resolution.

This recently caused a rather nasty issue in Android, where the Kotlin standard library defined some extension functions on java.util.Collection, and a later version of Android used a newer version of Java that included the same functions as member functions.

The newer version of Java was not available on previous versions of Android...

When apps were compiled against the older Android/Java combination, the functions resolved to the Kotlin versions at compile time. Regardless of which Android version was used, the code explicitly used the Kotlin versions.

When apps were compiled against the newer Android/Java combination, the functions resolved to the Java versions at compile time. If the app were run on the newer Android version, everything was fine. If the app were run on an older version of Android, the Java functions were not present and the app would crash.

Lint checks have been added that check that functions are present in the min Android SDK (which includes the associated Java API).