Data classes
One extra word in front of class gives you readable printing, comparison by contents, and a free copy function. It is the class you will write most often in this course.
Two annoyances, one word
At the end of Lesson 1.14 we left two things broken. Here they are together:
1class Note(val id: Int, val text: String)
2
3fun main() {
4 val a = Note(1, "Milk")
5 val b = Note(1, "Milk")
6 println(a)
7 println(a == b)
8}1Note@4517d9a3
2falseThe first line tells you nothing. The second is worse: two notes with the same id and the same text are reported as not equal.
Now change one word — put data in front of class — and run it again:
1Note(id=1, text=Milk)
2trueNothing else changed. That is what this lesson is about.
Imagine two printed concert tickets. Same band, same night, same seat number: 14F.
Ask "are these the same ticket?" and there are two honest answers.
No — they are two separate pieces of paper. You could burn one and still hold the other.
Yes — they say exactly the same thing. Anyone reading them learns the same facts.
An ordinary answers the first way: it compares the paper. A answers the second way: it compares what is printed on it.
For a note, a score, a saved setting — anything that simply is its contents — the second answer is the one you want.
What the word buys you
data class Note(val id: Int, val text: String)That single line generates five things you would otherwise write by hand.
| You get | What it does |
|---|---|
toString() | Printing shows Note(id=1, text=Milk) |
equals() | == compares contents, not memory addresses |
hashCode() | Notes can be used in sets and as map keys |
copy() | Build a near-identical object, changing only what you name |
component1(), component2() | Lets you unpack the object into separate names |
All of it comes from the properties in the constructor brackets. That is the deal: a data class is a thing defined by its contents, so Kotlin uses those contents for everything.
Writing that lot yourself is about forty lines of pure — code that carries no meaning but must exist. Deleting boilerplate is one of Kotlin's main jobs.
Comparison by contents
1val a = Note(1, "Milk")
2val b = Note(1, "Milk")
3println(a == b) // true
4println(a === b) // false== asks is this the same information? That is , and it is what you almost always mean.
===, with three signs, asks is this literally the same object in memory? Two separately built notes are two objects, so it says false. You will need it roughly never, but it is worth knowing the difference exists so the two-sign version is not mysterious.
Because equality works properly, so does everything built on it:
println(setOf(a, b).size)That prints 1. The looked at both notes, saw identical contents, and kept one. With a plain class it would have printed 2.
copy — change one thing, keep the rest
You cannot edit a val. So how do you change a note's text?
You do not. You make a new note that is the same except for the text:
1val c = a.copy(text = "Milk and eggs")
2println(a)
3println(c)1Note(id=1, text=Milk)
2Note(id=1, text=Milk and eggs) takes named arguments for whatever you want different, and quietly carries everything else over. The original is untouched — a still says Milk.
This "make a changed copy rather than edit in place" habit is not a curiosity. It is how state works in from Part 3 onwards: your screen holds one object describing what to show, and every change produces a new object with copy. Compose spots the difference and redraws. Getting comfortable with copy now makes Part 4 much easier.
Destructuring — unpack in one line
1val (id, text) = c
2println("$id -> $text")pulls the properties out into separate names, in constructor order. You already used it in Lesson 1.11 to unpack a map entry into a key and a value — that works for exactly the same reason.
The order matters and the names do not. val (text, id) = c compiles perfectly and gives you completely wrong data, so keep the order honest.
When not to use one
A data class is right when the thing is its contents: a note, a dice roll, a saved setting, the state of a screen.
A plain class is right when the thing has behaviour and an identity of its own — a game engine, a database connection, a timer that is currently running. Two timers that happen to show the same number are not the same timer.
Two rules the compiler enforces:
- A data class must have at least one parameter in its constructor. An empty one would have nothing to compare.
- Only the constructor properties count. Anything declared inside the braces is invisible to
toString,equalsandcopy.
- Open Pocket Studio, tap Projects, and open your Kotlin practice project.
- Tap Editor and open
Playground.kt. - Select everything in the file and delete it.
- Type the program from the walkthrough above.
- Tap Run and read the Output panel.
You should see exactly this:
1Note(id=1, text=Milk)
2true
3false
4Note(id=1, text=Milk and eggs)
51
61 -> Milk and eggs- Now delete the word
datafrom line 1, leavingclass Note(...), and tap Run. - Three things break at once: line 9 fails with Unresolved reference: copy, and line 12 fails with a message about
component1(). Those functions only existed because of the word you deleted. - Put
databack. Run once more and check you get the six lines above again.
data class Empty() with nothing in the brackets. A data class is defined by its contents, and this one has none, so every generated function would be meaningless.class — or, if you want exactly one of something with no data at all, an object, which Lesson 1.16 introduces.copy on an ordinary class. copy is one of the functions generated only for data classes, so on a plain class the name simply does not exist.data in front of class. If the type genuinely should not be a data class, build a new object by hand with its constructor instead.val (id, text) = note on a type that is not a data class. Unpacking relies on the generated component1(), component2() and so on.val id = note.id.note.text = "something" on a val property. Data class properties are almost always val, on purpose.copy: val updated = note.copy(text = "something"). You get a new note and the old one stays valid, which is what the rest of your app will want anyway.Note(1) when it needs both an id and some text.Note(1, "Milk"). Or give the parameter a default in the class, as val text: String = "", if an empty note is meaningful.- A is a class whose job is holding information. Add
dataand Kotlin writestoString,equals,hashCode,copyand the unpacking functions for you. ==then means ;===still means same object.- makes a changed duplicate and leaves the original alone — the habit that Compose state depends on.
- with
val (a, b) = thingunpacks in constructor order. - Use one for things that are their contents; use a plain class for things that do things.
- Next: enums and sealed classes — how to say "this can only be one of these few things", and get the compiler to check that you handled every one.