Intro to Data Classes in Kotlin

Eliminate boilerplate and and easily destructure data.

Tired of writing (or generating) lengthy, boilerplate code for objects which do nothing but store data?

Well, Kotlin has the solution for you!

Almost every software project we create has a number of classes which exist solely to store data/state but have almost no actual functionality in terms of operations. In more complex apps, this number can be rather high (applications which feature a clean architecture approach often have 2–3 times as many due to a separation of entities between layers).

These generally contain the same concepts every time:

A constructor

Fields to store data

Getter and setter functions

hashCode(), equals() and toString() functions

Example — Storing Video Game Data

If we wanted to store some data about a video game in Java, we would usually create a class similar to this:

public class VideoGame {



private String name;

private String publisher;

private int reviewScore;



public VideoGame(String name, String publisher, int reviewScore) {

this.name = name;

this.publisher = publisher;

this.reviewScore = reviewScore;

}



public String getName() {

return name;

}



public void setName(String name) {

this.name = name;

}



public String getPublisher() {

return publisher;

}



public void setPublisher(String publisher) {

this.publisher = publisher;

}



public int getReviewScore() {

return reviewScore;

}



public void setReviewScore(int reviewScore) {

this.reviewScore = reviewScore;

}



@Override

public boolean equals(Object o) {

if (this == o) return true;

if (o == null || getClass() != o.getClass()) return false;



VideoGame that = (VideoGame) o;



if (reviewScore != that.reviewScore)

return false;

if (name != null ? !name.equals(that.name) :

that.name != null) {

return false;

}

return publisher != null ?

publisher.equals(that.publisher) :

that.publisher == null;



}



@Override

public int hashCode() {

int result = name != null ? name.hashCode() : 0;

result = 31 * result + (publisher != null ?

publisher.hashCode() : 0);

result = 31 * result + reviewScore;

return result;

}



@Override

public String toString() {

return "VideoGame{" +

"name='" + name + '\'' +

", publisher='" + publisher + '\'' +

", reviewScore=" + reviewScore +

'}';

}

}

That’s a lot of code just to store 3 fields of data for a video game!

Data Classes in Kotlin

Fortunately for us, the above code is no longer necessary in Kotlin due to the useful data class concept provided by the language. A data class is a class in Kotlin created to encapsulate all of the above functionality in a succinct manner.

To recreate the VideoGame class in Kotlin, we can simply write:

data class VideoGame(val name: String, val publisher: String, var reviewScore: Int)

Much better!

When we specify the data keyword in our class definition, Kotlin automatically generates field accessors, hashCode(), equals(), toString(), as well as the useful copy() and componentN() functions (more on these later).

Any of the functions above which are manually defined by us in the class will not be generated.

Creating an Instance of a Data Class

Data classes are instantiated in the same manner as a standard class:

val game: VideoGame = VideoGame("Gears of War", "Epic Games", 8)

We can now access the members of game:

print(game.name) // "Gears of War"

print(game.publisher) // "Epic Games"

print(game.reviewScore) // 8

game.reviewScore = 7

and print the contents of the class:

print(game.toString())

// prints

// "Game(name=Gears Of War, publisher=Epic Games, reviewScore=7)"

Visibility Modifiers

We can control the visibility modifiers of the getters/setters generated by providing them in the constructor:

data class VideoGame(private val name: String, val publisher: String, private var reviewScore: Int

Read-Only Fields

If we only wish to expose getters and not setters, we just provide val instead of var for each field (val properties are Kotlin’s equivalent of final in Java). In the below example, name and reviewScore have read/write access, while publisher is read-only:

data class VideoGame(var name: String, val publisher: String, private var reviewScore: Int

copy() Function

Since our data classes are immutable, we must create a copy if we wish to change some data. We are also able to specify if we only wish to change specific attributes for the new copy. For example, if we wish to change the review score of a game, this can be done by writing:

val game: VideoGame = VideoGame("Gears of War", "Epic Games", 8) val betterGame = game.copy(reviewScore = 10)

Destructuring Declarations

This is the name of the syntax provided by Kotlin which allows us to map an object into individual fields. This is where the componentN() functions stated above come into play.

For each property we specify for our data class (in our video game example we have 3), Kotlin will generate a componentN() function which maps to that property, where ’N’ represents the properties order in the definition. So in our case, we have the following:

game.component1() // name

game.component2() // publisher

game.component3() // reviewScore

These generated functions allow us to use destructuring declarations to do some cool things:

We can destructure an object to create three val properties at once:

val game: VideoGame = VideoGame("Gears of War", "Epic Games", 8) val (theName, thePublisher, theReviewScore) = game // val theName == "Gears of War"

// val thePublisher == "Epic Games"

// val theReviewScore == 8

We can also destructure data directly from a function:

// function which returns a new video game

fun getNamePublisherAndReviewScore() = VideoGame("Street Fighter", "Capcom", 10) val (anotherName, anotherYear, anotherReviewScore) = getNamePublisherAndReviewScore() // anotherName == "Street Fighter"

// anotherYear == "Capcom"

// anotherReviewScore == 10

In fact, destructuring declarations can be used in most cases where the right hand side of the declaration can be broken down into componentN() functions.

We can destructure data directly inside a loop through maps/collections:

val listOfGame: List<VideoGame> = listOf(game, betterGame) for ((gameName, gamePublisher, gameScore) in listOfGame) { print(gameName) // print the gameName

// do something else with the gamePublisher

// share the gameScore

}

This is very useful when dealing with key/value stores (from the Kotlin documentation):

for ((key, value) in map) {

// do something with the key and the value

}

Some Built-in Data Classes

Kotlin also has the built-in data classes Pair and Triple for common operations:

val pair: Pair<Int, String> = Pair(10, "Ten")

val triple: Triple<Int, String, Boolean> = Triple(1, "One", true)

Although as the docs state, in most cases it is usually better to create your own data class (even if it has 2 or 3 attributes) which has a more descriptive/relevant name for your use case.

Rules for Creating Data Classes

The Kotlin documentation on data classes notes that there are some basic restrictions in order to maintain consistency/behaviour of generated code:

The primary constructor needs to have at least one parameter;

All primary constructor parameters need to be marked as val or var ;

or ; Data classes cannot be abstract, open, sealed or inner;

Data classes may not extend other classes (but may implement interfaces).

Conclusion

Like most other aspects of Kotlin, data classes aim to reduce the amount of boilerplate code you write in your project (and do so to great effect!).

To learn more, please visit the Data Classes and Destructuring Declarations pages in the official Kotlin Documentation.

If you are an Android developer like myself or even just interested in Kotlin, please feel free to get in touch via Twitter:

https://twitter.com/DarrenAtherton

Thanks for reading!