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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Destructuring Declarations

In DriversList.kt in build.driver, refactor DriversListAdapter to use the new DriverWithSelection:

class DriversListAdapter(
  private val onDriverClicked: (Driver) -> Unit
) : ListAdapter<DriverWithSelection, DriverViewHolder>(DriverDiffer()) { // 1
  /* ... */

  override fun onBindViewHolder(holder: DriverViewHolder, position: Int) {
    val (driver, isSelected) = getItem(position) // 2
    val team = ConstructorsRepository.forId(driver.currentTeamId)
    holder.bind(driver, team, isSelected) // 3
  }
}

Here’s what that code does:

  1. You changed the generic type parameter passed to the parent ListAdapter class from Driver to DriverWithSelection
  2. Retrieved the driver and isSelected values from the list.
  3. You passed the correct values to the bind method of the ViewHolder.

Notice the syntax used to declare the values driver and isSelected. This is known as a destructuring declaration, or simply, destructuring.

Destructuring allows you to succinctly extract values stored inside a data class. It uses the auto-generated componentN() methods to map declared values to the class’s properties. component1() returns the first property, component2() returns the second property, and so on.

Thus, the above code snippet is functionally equivalent to the following:

val item = getItem(position)
val driver = item.component1()
val isSelected = item.component2()

Be careful with the use of destructuring declarations. Generated componentN() methods can unexpectedly break your code if you change the order of properties in a data class without updating the order of values in the destructured declaration.

For example, consider the following snippet:

data class Driver(val name: String, val team: String)

fun main() {
  val seb = Drivers("Sebastian Vettel", "Aston Martin")
  val (sebName, sebTeam) = driver // "Sebastian Vettel", "Aston Martin"
}

If you change the definition of Driver some time later to include a driver number, it’d cause sebName and sebTeam to have incorrect values:

data class Driver(val number: Int, val name: String, val team: String)

fun main() {
  val seb = Drivers(5, "Sebastian Vettel", "Aston Martin")
  val (sebName, sebTeam) = driver // 5, "Sebastian Vettel"
}

With that warning out of the way, build and run the app. Unfortunately, the compilation should fail again. This is because DriversListAdapter uses the DriverDiffer class for DiffUtil support. You need to update the class to use DriverWithSelection. The implementation of DiffUtil is closely tied to the equals() method on a class.

Next, you’ll learn how data classes auto-generate this method to follow value-based equality.

Value-Based Equality

In DriversList.kt in java ▸ build ▸ driver inside app module, refactor DriverDiffer to use the new DriverWithSelection type.

So, replace the existing class with the following code:

class DriverDiffer : DiffUtil.ItemCallback<DriverWithSelection>() {
  override fun areItemsTheSame(
      oldItem: DriverWithSelection,
      newItem: DriverWithSelection,
  ): Boolean {
    return oldItem.driver.id == newItem.driver.id
  }

  override fun areContentsTheSame(
      oldItem: DriverWithSelection,
      newItem: DriverWithSelection,
  ): Boolean {
    return oldItem == newItem
  }
}

Notice the use of == in areContentsTheSame. This method checks if its two parameters contain the same contents. The auto-generated equals() method on a data class makes this check easy. It compares two instances using value equality rather than referential equality.

If two variables have referential equality, they contain the same contents and refer to the same object instance. If two variables have value equality, they contain the same contents but might point to different object instances.

In most situations, you need value equality rather than referential equality. In the rare event where you do need referential equality, use the === operator.

For instance, check out these examples to understand it better:

/* ----- Referential-equality ----- */
object MercedesW10

// Both values point to the same object instance
val mercedes2019Car = MercedesW10
val racingPoint2020Car = MercedesW10


/* ----- Value-equality -----*/
data class MercedesW11(val goesFast: Boolean)

// Both variables have the same contents, but point to different instances in memory
val mercedes2020Car = MercedesW11(goesFast = true)
val racingPoint2021Car = MercedesW11(goesFast = true)

The generated equals() calls equals() on each property of the data class. Therefore, it works correctly even for complex property types such as lists. However, arrays are an exception, as Array.equals() checks for referential equality only.

Fixing BuildDriversViewModel

You need to make a few more changes to get the app working again.

Navigate to BuildDriversViewModel.kt in java ▸ build ▸ driver inside app module.

First, change _driversWithSelection and driversWithSelection to use DriverWithSelection:

class BuildDriversViewModel : ViewModel() {
  // Use the new DriverWithSelection class
  private val _driversWithSelection = MutableStateFlow<List<DriverWithSelection>>(emptyList())
  
  /* ... */

  val driversWithSelection: Flow<List<DriverWithSelection>>
    get() = _driversWithSelection

  /* ... */
}

Second, initialize _driversWithSelection with the list of all drivers and their selection status:

class BuildDriversViewModel : ViewModel() {
  /* ...  */

  init {
      // Initialize with the list of all drivers and their selection status set to false
    _driversWithSelection.value = DriversRepository.all().map { driver ->
      DriverWithSelection(driver, false)
    }
  }

  /* ...  */
}

Finally, update toggleDriver to update _driversWithSelection every time a driver is selected/unselected:

class BuildDriversViewModel : ViewModel() {
  /* ... */

  fun toggleDriver(driver: Driver) {
    if (driver in _selectedDrivers) {
      _selectedDrivers.remove(driver)
    } else {
      _selectedDrivers.add(driver)
    }
    updateSelectionSet()
  }

  private fun updateSelectionSet() {
    _driversWithSelection.value = DriversRepository.all().map { driver ->
      DriverWithSelection(driver, driver in _selectedDrivers)
    }
  }
}

Now, every time you select/unselect a driver in the list, toggleDriver will trigger a new emission in the driversWithSelection flow. This will update the data in the list.

Note: It won’t disrupt the user’s scrolling because DriversListAdapter uses DiffUtil. Rather, it efficiently calculates and applies differences between the old and the new lists.

Build and run. You should now have a working driver selection list!

An image showing the driver selection screen with two selected drivers

Data Classes in Hash-Based Data Structures

Navigate to BuildDriversViewModel in java ▸ build ▸ driver inside app module. Notice _selectedDrivers, which is a Set that keeps track of selected drivers. It’s constructed using the factory method mutableSetOf(), which returns a LinkedHashSet.

A HashSet is a hash-based data structure that uses an object’s hashCode() to establish its identity. The default implementation doesn’t follow value equality. Therefore, it’s possible to have multiple objects with the same value in a HashSet, since they’d have different hashes. So, it’s important to manually override this method with a correct implementation on any class used with hash-based data structures.

For instance, have a look at the following example using a regular class:

class GrandPrix(val location: String)

fun main() {
  val grandPrixes = setOf<GrandPrix>(
    GrandPrix("Silverstone"),
    GrandPrix("Silverstone"),
  )
  
  println("Without data class: $grandPrixes")
  // Prints "Without data class: [GrandPrix@279f2327, GrandPrix@2ff4acd0]"
}

Now, have a look at the following code, which is using a data class:

data class GrandPrix(val location: String)

fun main() {
  val grandPrixes = setOf<GrandPrix>(
    GrandPrix("Silverstone"),
    GrandPrix("Silverstone"),
  )
  
  println("With data class: $grandPrixes")
  // Prints "With data class: [GrandPrix(location=Silverstone)]"
}

The auto-generated hashCode() method on a data class follows value equality. So, it prevents the presence of multiple instances of the same value in a hash-based data structure.