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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Deep Dive Into Kotlin Data Classes for Android
20 mins
- Getting Started
- Data Classes
- Declaring Data Classes
- Constructing Data Classes
- Using Data Classes
- Destructuring Declarations
- Value-Based Equality
- Fixing BuildDriversViewModel
- Data Classes in Hash-Based Data Structures
- Copying Data Classes
- Tests
- Extras
- Easy toString()
- Extension Functions
- Data Class Limitations
- Where to Go From Here?
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:
- You changed the generic type parameter passed to the parent ListAdapter class from
Driver
toDriverWithSelection
- Retrieved the
driver
andisSelected
values from the list. - 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.
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!
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.