Deep Dive Into Kotlin Data Classes for Android

In this Kotlin data classes tutorial, you’ll learn when and how to use data classes, how they vary from regular classes and what their limitations are. By Kshitij Chauhan.

Leave a rating/review
Download materials
Save for later
Share

Mobile applications work with a lot of data. Whether from a database or a network API, data is at the heart of modern Android applications. Modeling data in code has traditionally been a complex affair on Android with Java. The language provides few tools to correctly construct, copy and compare data model classes. Luckily, Kotlin data classes on Android make this process easy, concise and fun.

In this tutorial, you’ll build Paddock Builder, an app to create your own Formula 1 team for the 2021 season. You’ll learn about:

  • Creating and using Kotlin data classes
  • The advantages of data classes over regular classes
  • Using data classes with various Android components
  • Limitations of data classes in object-oriented programming
Note: This tutorial assumes familiarity with the basics of Kotlin for Android development. If you’d like to revisit the basics, consider reading Kotlin for Android: An Introduction first.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Then, open the starter project in Android Studio to discover Paddock Builder.

Build and run the app. You’ll see the following screen:

First screen of "Paddock Builder"

Tapping the Build Your Team button allows you to select drivers for your team. But the driver selection doesn’t work, and the data models in the application use regular classes. So, you’ll implement the missing driver and team building functionality. Then eventually migrate the application to use data classes.

Before you do that, you need to learn more about data classes in Kotlin.

Data Classes

In object-oriented programming (OOP), a class contains both properties and functions. However, classes that serve only as data models focus on properties. In such classes, the compiler can derive some functionality from its member properties. Kotlin facilitates this use case with data classes.

Data classes specialize in holding data. The Kotlin compiler automatically generates the following functionality for them:

  • A correct, complete, and readable toString() method
  • Value equality-based equals() and hashCode() methods
  • Utility copy() and componentN() methods

To appreciate the amount of functionality a data class provides automatically, compare the following equivalent code snippets.

First, Java:

// Java
public final class User {
  @NotNull
  private final String name;
  @Nullable
  private final String designation;

  @NotNull
  public final String getName() {
    return this.name;
  }

  @Nullable
  public final String getDesignation() {
    return this.designation;
  }

  public User(@NotNull String name, @Nullable String designation) {
      this.name = name;
      this.designation = designation;
   }

  @NotNull
  public final String component1() {
    return this.name;
  }

  @Nullable
  public final String component2() {
    return this.designation;
  }

  @NotNull
  public final User copy(@NotNull String name, @Nullable String designation) {
    return new User(name, designation);
  }

  @NotNull
  public String toString() {
    return "User(name=" + this.name + ", designation=" + this.designation + ")";
  }

  public int hashCode() {
    String var10000 = this.name;
    int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
    String var10001 = this.designation;
    return var1 + (var10001 != null ? var10001.hashCode() : 0);
  }

   public boolean equals(@Nullable Object var1) {
    if (this != var1) {
      if (var1 instanceof User) {
        User var2 = (User)var1;
        if (Intrinsics.areEqual(this.name, var2.name) && Intrinsics.areEqual(this.designation, var2.designation)) {
          return true;
        }
      }
      return false;
    } else {
      return true;
    }
  }
}

Now, Kotlin:

// Kotlin
data class User(
  val name: String,
  val designation: String?
)

As you can see, it’s incredible how much code data classes can save you from writing. The less code you write, the less code you need to maintain, and the faster you go. If you’re a fan of Formula 1, you will love going fast!

Declaring Data Classes

You declare data classes similar to how you declare regular classes, except:

  1. The keyword data must precede the keyword class.
  2. The primary constructor must not be empty, and it should contain only val or var properties.

Open Models.kt in repository inside the app module, where you’ll find two classes: Driver and Team. Refactor this code to use data classes by adding the data keyword, as shown below:

data class Driver(
    val id: String,
    val number: Int,
    val firstName: String,
    val lastName: String,
    val nationality: String,
    val currentTeamId: String,
)

data class Constructor(
    val id: String,
    val name: String,
    val drivers: List<Driver>
)

Build and run the app. The project should compile successfully and you should see no changes.

Constructing Data Classes

Data classes can have two types of constructors: primary and secondary.

The primary constructor on a data class can only declare properties. You can optionally create a secondary constructor, but it must delegate to the primary using the this keyword.

Here’s an example:

// Primary Constructor
data class GrandPrix(
  val name: String,
  val location: String,
  val year: Int,
  val numTeams: Int,
) {

  // Secondary Constructor
  constructor(
    name: String,
    location: String,
    year: Int,
  ): this(name, location, year, 10)
}

Open Grid.kt in repository. This file contains the details of all the teams and drivers. Note that refactoring Driver and Constructor didn’t break any code here. This is because data classes are constructed like regular classes: by invoking their constructors.

Using Data Classes

The process of selecting a driver doesn’t work well in the current app: There’s no visual feedback to show the selection status of any list item.

To fix this, open DriversList.kt in java ▸ build ▸ driver inside app module. This file contains the RecyclerView adapter responsible for the driver’s list in BuildDriversFragment. Currently, this adapter doesn’t know whether the user has selected a driver or not. As such, it cannot visually differentiate between selected and unselected drivers.

Create a new data class, DriverWithSelection, in the same file with the following content:

data class DriverWithSelection(
  val driver: Driver,
  val isSelected: Boolean
)

You’ll use this data class to differentiate between selected and unselected drivers in the list.

Now, refactor DriverViewHolder to the following:

@SuppressLint("SetTextI18n")
fun bind(driver: Driver, team: Constructor, isSelected: Boolean) { // 1
  binding.apply {
    driverName.text = "${driver.firstName} ${driver.lastName}"
    driverTeamName.text = team.name
    driverNumber.text = driver.number.toString()
    driverContainer.setBackgroundColor(getBackgroundColor(isSelected))  // 2
    driverContainer.setOnClickListener {
      onDriverClicked(driver)
    }
  }
  
  // getBackgroundColor(isSelected) method definition
}

Here, you have:

  1. Changed the bind method to accept a third parameter indicating the selection status of a driver.
  2. Used this property to change the background color.

Now, try to build and run the app. The compilation should fail, as the onBindViewHolder method of DriversListAdapter needs refactoring.

Head to the next section to learn how to fix this.