Pocket Studio Academy
HomePart 11.15

Data classes

Full course9 min read·4 questions

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:

Playground.ktkotlin
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}
text
1Note@4517d9a3
2false

The 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:

text
1Note(id=1, text=Milk)
2true

Nothing else changed. That is what this lesson is about.

Think of it like this

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

Playground.ktkotlin
data class Note(val id: Int, val text: String)

That single line generates five things you would otherwise write by hand.

You getWhat 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

Playground.ktkotlin
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:

Playground.ktkotlin
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:

Playground.ktkotlin
1val c = a.copy(text = "Milk and eggs")
2println(a)
3println(c)
text
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.

Tip

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

Playground.ktkotlin
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, equals and copy.
Try it in Pocket Studio
  1. Open Pocket Studio, tap Projects, and open your Kotlin practice project.
  2. Tap Editor and open Playground.kt.
  3. Select everything in the file and delete it.
  4. Type the program from the walkthrough above.
  5. Tap Run and read the Output panel.

You should see exactly this:

text
1Note(id=1, text=Milk)
2true
3false
4Note(id=1, text=Milk and eggs)
51
61 -> Milk and eggs
  1. Now delete the word data from line 1, leaving class Note(...), and tap Run.
  2. 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.
  3. Put data back. Run once more and check you get the six lines above again.
Error Doctor5 common errors
e: Data class must have at least one primary constructor parameter
MeansYou wrote 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.
FixGive it at least one property, or use a plain class — or, if you want exactly one of something with no data at all, an object, which Lesson 1.16 introduces.
e: Unresolved reference: copy
MeansYou called 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.
FixAdd the word 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.
e: Destructuring declaration initializer of type Note must have a 'component1()' function
MeansYou wrote val (id, text) = note on a type that is not a data class. Unpacking relies on the generated component1(), component2() and so on.
FixMake it a data class, or read the properties one at a time: val id = note.id.
e: Val cannot be reassigned
MeansYou tried note.text = "something" on a val property. Data class properties are almost always val, on purpose.
FixUse 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.
e: No value passed for parameter 'text'
MeansYou built the object with fewer arguments than the constructor requires — Note(1) when it needs both an id and some text.
FixPass both: Note(1, "Milk"). Or give the parameter a default in the class, as val text: String = "", if an empty note is meaningful.
Recap
  • A is a class whose job is holding information. Add data and Kotlin writes toString, equals, hashCode, copy and 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) = thing unpacks 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.