Kotlin Classes, Collections, Null Safety and Lambdas

Android Development with Kotlin: 21 Real Projects · Kotlin Basics · 7 min read · Updated

A list through helpers

  1. You start with a list of Student objects.
  2. filter keeps only the students whose marks are 50 or more.
  3. map changes each remaining student into just their name.
  4. The result is a brand new list. The original list is unchanged.

This lesson covers the second half of the Kotlin you need. It is the part that makes Kotlin feel different from older languages: classes that take one line, collections you can transform with short code, and a type system that prevents the most common crash in programming, the null pointer error.

Classes and data classes

A class describes a kind of thing: a user, a product, a note. A data class is a class whose main job is to hold data. Kotlin writes toString, equals and a copy function for you. In Android you will write dozens of data classes: one for each row in a database and each item in a list. Regular classes can inherit from another class (mark the parent open) and implement interfaces, which are lists of functions a class promises to have.

// A data class gives you equals, toString and copy for free.
data class Product(val name: String, val price: Double, val stock: Int)

open class Account(val owner: String, balance: Double) {
    var balance: Double = balance
        private set                       // others can read it but not change it directly

    fun deposit(amount: Double) {
        require(amount > 0) { "Deposit must be positive" }
        balance += amount
    }

    open fun describe() = "$owner has $balance"
}

class SavingsAccount(owner: String, balance: Double, private val rate: Double) : Account(owner, balance) {
    override fun describe() = "${super.describe()} (interest rate ${rate * 100}%)"
}

interface Shape {
    fun area(): Double
}
class Square(private val side: Double) : Shape {
    override fun area() = side * side
}

class Circle(private val r: Double) : Shape {
    override fun area() = Math.PI * r * r
}

fun main() {
    val p = Product("Pen", 1.5, 20)
    val cheaper = p.copy(price = 1.2)
    println(p)
    println(cheaper)

    val acc = SavingsAccount("Meera", 100.0, 0.04)
    acc.deposit(50.0)
    println(acc.describe())

    val shapes: List<Shape> = listOf(Square(2.0), Square(3.0))
    println(shapes.sumOf { it.area() })
}
Product(name=Pen, price=1.5, stock=20)
Product(name=Pen, price=1.2, stock=20)
Meera has 150.0 (interest rate 4.0%)
13.0

Notice private set on balance. Anyone can read the balance, but only the class can change it, so the rule "a deposit must be positive" cannot be bypassed. Hiding what outsiders should not touch is the heart of good class design, and you will use it again in ViewModels.

Collections

Lists, sets and maps hold groups of values. listOf makes a list that cannot change, and mutableListOf makes one that can. The real power is in the helper functions: filter keeps some items, map turns every item into something else, sortedBy orders them, groupBy sorts them into buckets, and sumOf or average fold them into one number. Each takes a lambda, a small block of code in braces. Inside the lambda it means the current item.

data class Student(val name: String, val marks: Int)

fun main() {
    val students = listOf(Student("Asha", 82), Student("Ben", 45), Student("Chen", 91), Student("Dev", 67))

    val passed = students.filter { it.marks >= 50 }          // keep some items
    val names = passed.map { it.name }                        // change each item
    val best = students.maxByOrNull { it.marks }              // find one
    val average = students.map { it.marks }.average()         // fold into one value

    println(names)
    println(best?.name)
    println(average)
    println(students.sortedByDescending { it.marks }.map { it.name })

    val byResult = students.groupBy { if (it.marks >= 50) "pass" else "fail" }
    println(byResult.mapValues { (_, list) -> list.size })

    val mutable = mutableListOf("a", "b")
    mutable.add("c")
    println(mutable)
}
[Asha, Chen, Dev]
Chen
71.25
[Chen, Asha, Dev, Ben]
{pass=3, fail=1}
[a, b, c]

Null safety

In many languages any value may secretly be null, and using it crashes the app. In Kotlin a normal type such as String can never be null. A type that can be null is written with a question mark, String?, and the compiler forces you to deal with it. The safe-call operator ?. stops quietly when the value is null, and the Elvis operator ?: supplies a default. let runs a block only when the value is not null. Avoid !!, which means "crash if this is null".

fun lengthOf(text: String?): Int = text?.length ?: 0      // ?. stops at null, ?: gives a default

fun main() {
    var nickname: String? = null
    println(nickname?.uppercase())          // null, no crash
    println(lengthOf(nickname))             // 0

    nickname = "kit"
    println(lengthOf(nickname))             // 3

    nickname?.let { println("Nickname is $it") }   // runs only when not null

    val input = "12a"
    val number = input.toIntOrNull()        // null instead of an exception
    println(number ?: "not a number")
}
null
0
3
Nickname is kit
not a number

Lambdas and functions as values

A function can be passed around like any other value. A function that takes another function is called a higher-order function. When the last parameter is a function you may write the lambda after the brackets, which is called a trailing lambda. Compose depends on this: Button(onClick = { ... }) { Text("Go") } passes one lambda for the click and a trailing lambda for the content.

fun repeatAction(times: Int, action: (Int) -> Unit) {
    for (i in 1..times) action(i)
}

fun main() {
    repeatAction(3) { n -> println("Run number $n") }

    val double = { x: Int -> x * 2 }
    println(double(21))

    val words = listOf("kotlin", "android", "compose")
    println(words.map { it.replaceFirstChar { c -> c.uppercase() } })
    println(words.joinToString(" | "))
}
Run number 1
Run number 2
Run number 3
42
[Kotlin, Android, Compose]
kotlin | android | compose

Your task

  1. Create a data class Book(val title: String, val pages: Int) and a list of four books.
  2. Print the titles of books with more than 200 pages, using filter and map.
  3. Print the average page count and the title of the longest book. Use ?. so the code is safe if the list is empty.
  4. Write a function describe(text: String?): String that returns "empty" when the text is null or blank and the text in capitals otherwise.

How to check it worked

  • Your list code prints the right titles and average.
  • You can explain what String? means and why !! is risky.
  • You can name three list helpers and say what each does.

Frequently asked questions

What is the difference between a class and a data class?

A data class is a class meant to hold data. Kotlin generates toString, equals, hashCode and copy for it. Use it for things like rows and list items.

Why is null safety such a big deal?

Null pointer errors are one of the most common crashes in apps. Kotlin makes nullability part of the type, so the compiler warns you before the app ever runs.

What does `it` mean in a lambda?

When a lambda has exactly one input, Kotlin names it it automatically. You can write your own name instead, such as student ->, when that reads better.