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
- Create a
data class Book(val title: String, val pages: Int)and a list of four books. - Print the titles of books with more than 200 pages, using
filterandmap. - Print the average page count and the title of the longest book. Use
?.so the code is safe if the list is empty. - Write a function
describe(text: String?): Stringthat 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.