What is a Data Class?
A data class is a concept not tied to any specific programming language, it’s a pattern that’s convenient enough to most programmers as a simple way to represent, encapsulate and move information around.
A data class refers to a class that contains only fields and crud methods for accessing them (getters and setters). These are simply containers for data used by other classes. These classes do not contain any additional functionality and cannot independently operate on the data that they own.
Usually, data classes represent real-world entities, and it’s common to have dozens or hundreds of these classes in a project, which means that creating, modifying and manipulating these objects it’s a very common task for a developer.
A Data class in Java
This is how a data class usually looks in Java:
You’ll notice we are overriding
hashCode() methods (declared in the
Object.java class ). Why is overriding these methods relevant in data classes?
When implementing those three methods we create value objects, classes for which any two instances with suitably equal field values are considered interchangeable. (Note: To be completely true, we’ll need to make the class immutable. Making the fields final and removing the setters helps. Immutability is often considered a good practice and recommended when possible)
equals(): By default, returns
truejust if the two variables refer to the same object, i.e. if the position in memory is the same. We override this method for returning
trueif the objects contain the same information, in other words, if the objects represent the same entity. It checks the properties are the same and they contain the same value.
hashCode(): By default, returns the object’s memory address in hexadecimal. It is a numeric value to identify an object during equality testing i.e. equal objects have the same hash code. This method must be overridden when
toString()is overridden so it returns a hash code calculated from the values of the properties.
toString(): By default returns the object type and the
hashCode(), for instance
User@34033bd0+. We override this method for having a more human-readable version of the object like
Despite being such an important concept, there is nothing in the Java code above that makes this class any different from any other. Programmers can recognize it as a data class because of the class structure and patterns, but from the compiler point of view, this class is just another class.
Creating data classes is so common that developers often use the IDE and other plugins to help them with this repetitive task. The pain of creating a data class in Java can be alleviated by plugins or the IDE, but most bugs are introduced on further modifications of those classes. It’s very easy to forget to modify all the companion methods accordingly every time a field it’s removed or added.
A data class in Java lacks language support. It’s a repetitive and bug-prone task that represents too much friction for a modern programming language.
A data class in Kotlin
The same data class in Kotlin would look something like this:
data class User(var name: String, var age: Int)
Kotlin elevates data classes to first class citizens introducing the
data keyword. Let’s break it down.
- Getters and setters
The getters and setters are created automatically in Kotlin when we declare properties. In short, what
var name: String means is that the
User class has a property that is public (default visibility in Kotlin), mutable (
var) and is a
String type. Given that is public it creates the getter, and given that is mutable it creates the setter.
If we want to make the class read-only (no setters), we need to use
data class User(val name: String, val age: Int)
We can mix
var in the same class declaration. You can think of
val as a
final variable in Java.
Everything explained so far, it’s common to any class declaration in Kotlin, but the
data keyword is what’s making a difference here.
Declaring a class as a data class is gonna get us implemented
equals() automatically the same way we described above for the Java class. So if we create a user class like
User("Steve Jobs",56) and call the
toString() method we’ll get something like:
User(name=Steve Jobs, age=56) .
data keyword provides functions that allow destructuring declarations. In short, it creates a function for every property so we can do things like this:
data keyword gives us a handy way of copying classes changing the value of some properties. If we want to create a copy of a user changing the age, this is the way we would do it:
Properties declared in the class body are ignored
The compiler only uses the properties defined inside the primary constructor for the automatically generated functions.
address it’s not going to be treated by the
data keyword, so it means that the auto-generated implementations will ignore it.
Pair and Triple
Triple are standard data classes in the library, but the Kotin docs themselves discourage the usage of them in favor of more readable and custom-tailored data classes.
Requirements and limitations
- A data class constructor needs to have at least one parameter.
- All parameters need to be market as
- A data class can’t be
hashCodemethods can be explicitly overridden.
- Explicit implementations for
copy()functions are not allowed.
- Deriving a data class from a type with a
copy()function matching signature was deprecated in Kotlin 1.2 and was prohibited in Kotlin 1.3.
dataclass cannot extend from another
dataclass may extend other classes (since Kotlin 1.1)
Data classes are first-class citizens in Kotlin. In a very short syntax they offer a frictionless solution with all the advantages and no compromises.
What does it mean for Android
I’m gonna try to explain what are the main advantages that I’ve found using Kotlin data classes in my Android projects. These are not all of them and might be not the most important ones, but these are the most obvious from my experience so far.
- Data models
Architecture in Android was (and still is) a hot topic. There are multiple options, but most of them have in common the separation of concerns and single responsibility principles. Clean Architecture -very famous in the Android community- is a set of good practices to follow when aiming for a good architecture that puts emphasis on using different models for different layers of the architecture. This means a project with 3 or 4 different data models become standard.
What that means is that the number of data classes in a project is very high, and operations like create, read, modify, copy, compare, map… data model classes are daily tasks that benefit from the Kotin Data Class auto-generated code. Saves a huge amount of time and reduces the opportunities to introduce bugs.
- No more Auto-Value
Using value types is a good practice very extended in Android, especially among data classes.
A value type is an object from a immutable value class.
A value class is a class for which equality depends on its content.
An immutable class is a class that can’t be modified after creation.
AutoValue is a popular library from Google that helps us to create value types. It does its job but it is very verbose for something that shouldn't be that complicated.
Using the kotlin
data classes with the
val access modifier give us a close enough approximation to value types.
data class User(val name : String, val age : Int)
The previous User class is a class close enough to value types, in a much shorter syntax. For a good chunk of people, AutoValue can be replaced by Kotlin. Less code, no annotation processing, and one less library to depend on.
Note: Kotlin offers read-only properties and classes with the
val keyword. Read-only and immutable is not the same (more here) but in general, is considered good enough for practical purposes.
- No more Lombok
One of the ways developers tried to save time when dealing with data classes is using libraries to generate getter and setter methods. Lombok is one of the (in)famous ones in Android/Java. It requires not only the library but as well a plugin for AS. The long story short is for most developers Lombok bring as many advantages as headaches, so it becomes that library you start loving but after a while, you can’t wait to get rid of but you never do because is everywhere.
Given that Kotlin Data Classes don’t require to manually write getter/setter methods, the main need for Lombok is gone.
- RecyclerView DiffUtil
RecyclerView in Android is the widget. It’s in every app in multiple screens. One important component when implementing RecyclerView adapters is the DiffUtil class. DiffUtil calculates the diff between two lists in order to dispatch the changes -if any- to the adapter. It’s much more efficient than refreshing the whole list over and over again, and it animates the changes beautifully.
DiffUtil relies heavily on the equality of the items, meaning two items are the same when their content is the same. Every time you use a RecyclerView, you should be using DiffUtil, which means you need a way of comparing if two objects contain the same information. This is one of the reasons we need to override
equals in our Java data classes, but with Kotlin
data classes it comes for free.
Note: If you aren’t familiarized with DiffUtil check this quick start guide.
In test classes, we are constantly checking if the expected values match the actual values. Comparing objects equality (as described before) is a very common task.
Even if you don’t need to override the
toHash triplet for you regular app runtime, the chances that you are gonna need to override those methods for testing purposes are high, so Kotlin data classes clear the path to testing with no excuses.
Let’s talk about some other common scenarios worth mentioning when working with Kotlin data classes. This is not strictly related to data classes, but are especially common among them.
- Multiple constructors
data class User(val name : String, val age : Int)
User class we defined previously, we need to explicitly specify the
age when creating an instance like
User("Steve", 56) .
In Kotlin we can define default values for arguments in such a way that in case we don’t pass a value for that argument, the default value is assigned to it.
data class User(val name : String, val age :Int = 0)
User("Steve", 56) is still valid, but now a second constructor
User("Steve") is allowed. In that case, the
age value will be 0.
We can assign default values to both parameters allowing a third constructor
User() where the
name will be empty and the
age will be 0.
data class User(val name : String = "", val age : Int = 0)
User("Steve",56) all are valid calls.
This has some limitations: Optional parameters need to be last parameters in the constructor. The next won’t compile.
data class User(val name : String = "", val age : Int)
val user = User(56) // This doesn't compile
Another limitation is that if we have multiple optional parameters, they have to be skipped from right to left.
data class User(
val name : String,
val surname : String = "",
val age : Int = 0
User("Steve", "Jobs", 56)
User("Steve",56) // This wont compile
To deal with this limitations Kotlin offers named arguments. This allows us to specify to which argument belongs to every value. Now we can do things like
User(name = "Steve", age = 56) — or shorter
User("Steve", age = 56) — where the surname will be assigned to the default value.
Default arguments and named arguments are a very handy way of offering multiple constructors and overloads from a very compact declaration.
All explained in the previous point is not taking into consideration calls from the Java side. Kotlin is interoperable with Java so we need to be able to create instances of the
User class from Java.
If you are not gonna use it from Java then you’re done, but otherwise, you’ll need the
@JvmOverloads annotation given that Java doesn’t offer named arguments.
data class User @JvmOverloads constructor(
val name : String,
val surname : String = "",
val age : Int = 0
What this is doing generating multiple constructors so it can be called from Java.
Note: It’s important to notice that it’s not going to create every possible permutation but the ones resulting from removing the optional arguments from right to left. More here
Given Kotlin offers named parameters and default arguments, it makes it less likely the need for builders. On top of that, modern IDEs like Android Studio already show the name of the parameter on the calling side making it easier to read making it less interesting just for readability purposes.
In the cases where the builder pattern is still needed. Kotlin does not offer anything special to help us with it. A builder pattern in Kotlin looks like:
It’s very common for some data model classes to use annotation processing. For instance API Models (data classes to represent deserialized responses from the API) are often annotated with Gson or Moshi annotations. The persistence models (data classes to represent data stored in local storage/databases) are often annotated with Room or Realm annotations.
In Kotlin we can still use those annotations. For instance, this is a User model to be stored in the Room database:
Kotlin data classes are the result of years of learning from pain and frustration with data classes in Java. They aim for having all the advantages and none of the downsides. Using them is very simple and enjoyable and once you get used to it, it’s very difficult to look back.
In Android, they help us in different ways, but mostly they save a lot of time and reduce bugs.
A Kotlin data class is a good example of what Kotlin is as a programming language: concise, pragmatic and a joy for developers.
- Data Class
- Kotlin Guide: Data Classes
- Data Classes for Java
- Learn in 2 minutes: @JvmOverloads in Kotlin