Kotlin Data Classes
While working on our apps sometimes we create some classes whose main purpose is to hold data and have functions to work with that data. But we need to write some functions like copy, equals etc. and these functions are repeated in every such class. So is there a way out to this?
The answer is yes, to make our work more easier Kotlin provides us the Data classes. These classes have all the functionalities of a simple class (automatic generated getters and setters or you can visit this article to know more) but also have some inbuilt functions which are used frequently while working with the data.
Defining a data class is very easy just a word data in before class:
data class User(val name: String, val age: Int)
To ensure consistency and meaningful behavior of the generated code, data classes have to fulfill the following requirements:
The primary constructor needs to have at least one parameter; All primary constructor parameters need to be marked as val or var; Data classes cannot be abstract, open, sealed or inner;
Extra Functions
Along with a data class, we get a handful of interesting functions for free, apart from the properties we already talked about (which prevent us from writing the accessors):
equals():
it compares the properties from both objects to ensure they are identical.
hashCode():
: we get a hash code for free, also calculated from the values of the properties.
toString():
of the form "User(name=John, age=42)";
copy():
you can copy an object, modifying the properties you need. We’ll see an example later.
A set of numbered functions that are useful to map an object into variables. It will also be explained soon.
Copying
It’s often the case that we need to copy an object altering some of its properties, but keeping the rest unchanged. This is what copy() function is generated for. For the User class above, its implementation would be as follows:
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
This allows us to write:
val jack = User("Jack", 1) val olderJack = jack.copy(age = 2)
Destructuring Declarations
The process of mapping an object into variables is called destructing declarations. It really saves us from writing different lines for copying each property.
val jane = User("Jane", 35) val (name, age) = jane println("$name, $age years of age") // prints "Jane, 35 years of age"
This is same as:
val jane = User("Jane", 35) val name = User.name val age = User.age println("$name, $age years of age") // prints "Jane, 35 years of age"
This feature has many uses, for instance, Map class has some extension functions implemented that allow to recover its keys and values in an iteration:
for ((key, value) in map) { Log.d(“map”, “key:$key, value:$value”) }
Adding Parameterless Constructor
When you create a data class as shown above, which is the general way to create one, you end up in getting a parameterised constructor.
This means, you’ll have to initialise your objects with all the parameters that you’ve declared, which is not the ideal case every time.
You might need to initialise an object with no data or few data and then use it’s setters to fill in appropriate data.
To add a parameterless constructor to your data class, you need to specify default values for each field.
Therefore, your new data class will look something like this:
data class User(val name: String = "", val age: Int = 0)
You can now initialize the object easily
val user = User()
Adding constructor overloads
There might be cases where you need to have multiple overloaded constructors to suit your needs.
For example, you might need to have an User object without an age field, or you might be planning to initialise that field later on in your app.
To do that, you just need to specify a default value for the specific field you want to omit.
data class User( var name: String, var age:Int = 0 )
And there you go.
You can now initialise your User object like this:
val user = User("name")
You can add the user’s age details later on like this:
user.age = 29
Playing cool with annotations
High chances are that you’ll be using a data class as an API response model.
If you’re using Retrofit with the GSON adapter to map your response JSON to your data classes, you’ll need to mark your fields with @SerializedName()annotations.
How to do that?
Just add @SerializedName("field_name")
before every field you want to annotate. Kind of like this:
data class User( @SerializedName("userage") var age: Int, @SerializedName("username") var name: String )
Properties Declared in the Class Body
Note that the compiler only uses the properties defined inside the primary constructor for the automatically generated functions. To exclude a property from the generated implementations, declare it inside the class body:
data class Person(val name: String) { var age: Int = 0 }
Only the property name
will be used inside the toString()
, equals()
, hashCode()
, and copy()
implementations, and there will only be one component function component1()
. While two Person
objects can have different ages, they will be treated as equal.
val person1 = Person("John") val person2 = Person("John") person1.age = 10 person2.age = 20 person1 == person2: true person1 with age 10: Person(name=John) person2 with age 20: Person(name=John)
Standard Data Classes
The standard library provides Pair and Triple. In most cases, though, named data classes are a better design choice, because they make the code more readable by providing meaningful names for properties.
val pair: Pair<Int, String> = Pair(10, "Ten") val triple: Triple<Int, String, Boolean> = Triple(1, "One", true)
This is how these data classes can ease our process to hold and manage data. Comment below if something needs to be corrected.
References:
- A Look into Kotlin’s Fancy Data Classes
- Official Kotlin Documentation
- Book — Kotlin for android developers by Antonio Levia