Diverse Types of Classes in Kotlin and when to use them

Emmanuel Iyke
8 min readSep 1, 2024

--

Becoming a Pro Kotlin developer requires you to understand different types of classes in Kotlin and when to use them, This helps significantly in writing more efficient, clean, and maintainable code that is more robust and prone to error. In this article, I will share my experience with the different types of classes Kotlin has to offer and the different scenarios to use them.

What are classes?

I know it sounds cheesy to ask but understanding what classes really are makes way to understand the different types there are. I know you have seen this definition before “A class is a blueprint for creating objects” So just as blueprints in architecture can lead to different types of buildings like houses, skyscrapers, or bridges — depending on the design, a class in Kotlin can also take on different forms depending on what it’s meant to do.

Why Different types of classes?

Now, not all classes are created equal. Some are meant to just hold data, others to enforce certain behaviors, and some to provide a single, global instance. That’s why Kotlin offers different types of classes, each tailored to specific needs. Just like you wouldn’t use the same blueprint for a skyscraper and a small cottage, you shouldn’t use the same type of class for all your coding needs.

Each type of class in Kotlin is designed to serve a particular purpose, allowing you to write cleaner, more efficient code. Let’s explore these different types and see when and why you’d want to use them.

Sealed class

A sealed class is a class that restricts its inheritance to a specific set of subclasses. In other words, it allows you to define a restricted set of subclasses right inside the sealed class itself, and no other subclasses can be created outside of this definition. This makes sealed classes perfect for situations where you know all the possible options in advance, like handling different states of a network request (e.g., Success, Error, Loading). By using a sealed class, you ensure that every possible case is covered, which makes your code more reliable and easier to manage.

Example:

sealed class NetworkResult<out T> {
data class Success<out T>(val data: T) : NetworkResult<T>()
data class Error(val exception: Throwable) : NetworkResult<Nothing>()
object Loading : NetworkResult<Nothing>()
}

fun handleResponse(result: NetworkResult<String>) {
when (result) {
is NetworkResult.Success -> println("Data: ${result.data}")
is NetworkResult.Error -> println("Error: ${result.exception.message}")
NetworkResult.Loading -> println("Loading...")
}
}

fun main() {
val result = NetworkResult.Success("Kotlin is awesome")
handleResponse(result)
}

Sealed classes are useful in situations where you have a fixed set of possible classes that need to be represented such as

  • Representing Success and Failure States: Common in network operations or database queries, Just like in the example above where you might have a NetworkResult type representing success, error, or loading states.
  • Defining State Machines: Useful for modeling state transitions in complex systems.
  • Type-safe Event Handling: Ensuring all possible events are handled in user interfaces or reactive systems.

Data Class

A data class in Kotlin is a concise and efficient way to create classes that are primarily used to hold data. Kotlin makes it easier to work with immutable data objects by automatically generating several useful functions for data classes.

When you declare a data class, Kotlin automatically generates functions like equals(), hashCode(), toString(), and copy(). The copy() function is particularly helpful for creating a new instance with some properties changed, while keeping the original data intact. The data keyword also provides functions that allow destructuring declarations, which means you can break down an object into its component properties easily. For example, if you have a data class User(val name: String, val age: Int), you can destructure it like val (name, age) = user.

Example:

data class User(
val id: Int,
val name: String,
val email: String
)

fun main() {
// Creating an instance of the User data class
val user1 = User(1, "cyber iyke", "cyber.@example.com")

// Using the copy() function to create a new instance with a modified name
val user2 = user1.copy(name = "James")

// Printing the original and copied instances
println(user1)
println(user2)

// Destructuring the User object into individual variables
val (id, name, email) = user1
println("ID: $id, Name: $name, Email: $email")
}

Kotlin’s data classes are designed for scenarios where the primary purpose of the class is to hold data. Here are the key situations where using a data class is ideal

  • Library Integration: Use data classes when working with libraries like kotlinx.serialization for JSON serialization or Parcelize for Android’s Parcelable implementation. Data classes integrate smoothly with these tools, making your code more efficient.
  • Auto-Generated Methods: Opt for a data class when you need easy comparison, debugging, or cloning of objects. With automatically generated methods like equals(), hashCode(), toString(), copy(), and componentN(), data classes make these tasks straightforward.
  • Concise Syntax for Data Handling: Use a data class when you want to reduce boilerplate code. If your class is mainly meant to store data and doesn’t require complex logic, a data class provides a clear and concise way to define it.

Abstract Class

An abstract class in Kotlin serves as a blueprint or template for other classes. It cannot be instantiated on its own, meaning you can’t create an object directly from it. Instead, it’s designed to be extended by other subclasses, which then build upon its structure.

Abstract classes are ideal when you want to enforce common behavior across multiple related classes, ensuring that certain methods or properties are shared and implemented consistently.

Example:

// Abstract class representing a generic person
abstract class Person {
abstract var height: Int
abstract var age: Int
abstract fun speak()
}

// Subclass representing a specific person
class Student(override var height: Int, override var age: Int) : Person() {
override fun speak() {
println("I'm a student, and I love learning!")
}
}

fun main() {
val student = Student(170, 20)
student.speak() // Output: I'm a student, and I love learning!
}

Abstract classes in Kotlin are exceedingly beneficial in architecture and software development, especially when dealing with large-scale apps with intricate class hierarchies. They yield a standard template for classes that share common functionalities, promoting clean and DRY (Don’t Repeat Yourself) code. Below are perfect scenarios abstract classes are required:

  • Designing Functional Units: When creating large, functional units in your application, abstract classes help define a common structure that can be extended and customized by subclasses.
  • Building Toolkits and Frameworks: If you’re developing toolkits or frameworks where end-users need to extend your base classes, abstract classes offer a flexible foundation for them to build upon.
  • Enforcing Common Behavior: When you need to provide a common mechanism across multiple subclasses while allowing for some flexibility in implementation, abstract classes are ideal.

Interfaces:

An interface in Kotlin is a contract that a class can implement, defining a set of abstract methods that the implementing class must provide. An abstract class and an interface in Kotlin both allow you to define methods that other classes must implement, but they serve different purposes and have key distinctions.

An abstract class can hold state, meaning it can have properties with actual values, and it can also provide both fully implemented methods and abstract methods that subclasses must override. In contrast, an interface cannot hold state and can only declare properties that must either be abstract or provide accessor implementations. While a class can only inherit from one abstract class, it can implement multiple interfaces, which makes interfaces a better choice for defining shared behavior across unrelated class hierarchies. Additionally, interface members are ‘open’ by default, allowing multiple inheritance of behavior, whereas abstract classes are more suited for scenarios where you need to provide a base class with common functionality and state management that can be shared among its subclasses.

Example:

interface ViewModelContract<T> {
val data: T?
val isLoading: Boolean

fun fetchData()
fun clearData()
}

class UserViewModel : ViewModelContract<User> {
override var data: User? = null
override var isLoading: Boolean = false

override fun fetchData() {
isLoading = true
// Simulate a network request or database operation
data = User("John", "cyber.doe@example.com")
isLoading = false
}

override fun clearData() {
data = null
}
}

Abstract classes and interfaces both play crucial roles in Kotlin’s object-oriented design. Abstract classes are used when you want to share state and behavior across a set of related classes, while interfaces are used to define a contract that any class can implement, regardless of where it fits in the class hierarchy. Understanding these differences allows you to use them effectively to design flexible and maintainable code.

Enum class:

Enum classes are your go-to when you need a set of related constants that won’t change. They are used to represent fixed choices or categories. For example, if you’re dealing with something like gender, days of the week, or cardinal directions, an enum class allows you to define all the possible options in one place.

Example

enum class DayOfWeek {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

fun isWeekend(day: DayOfWeek): Boolean {
return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY
}

fun main() {
val today = DayOfWeek.FRIDAY
println("Is today a weekend? ${isWeekend(today)}")
// Output: Is today a weekend? false
}

When to use Enum Class:

  • Fixed Set of Values: Use an enum class when you have a clearly defined set of related constants that won’t change over time. For example, days of the week (Monday, Tuesday, etc.), directions (North, South, etc.), or user roles (Admin, User, Guest).
  • Type Safety: If you want to ensure that a variable can only take on one of a predefined set of values, an enum class is the right choice. This reduces the risk of invalid values being assigned and makes your code more robust

Inner class

An inner class in Kotlin is a class that is defined inside another class using the inner keyword. By using the inner keyword, the inner class gains a special relationship with its outer class, allowing it to access all of the outer class’s properties and methods, including private ones.

An inner class is useful in situations where inner class needs to interact with or rely on the outer class’s instance. This is particularly helpful in scenarios where the inner class needs to use or manipulate the outer class’s data or behavior, creating a tightly-coupled relationship.

Example

class ClassRoom(val name: String) {

private val outerPrivateProperty: String = "This is a private property"

// Inner class declaration
inner class Student(val age: Int) {

fun displayInfo() {
println("ClassRoom name: $name")
println("Students age: $age")
}
}
}

Nested class

A nested class in Kotlin is a class defined within another class, but without the inner keyword. This means that the nested class is completely independent of its outer class. It does not have access to the members of the outer class unless they are explicitly passed to it.

Nested classes are useful when you need to logically group classes together to keep your code organized. They are especially helpful when the nested class is only relevant in the context of the outer class but doesn’t need access to its members. This helps in encapsulating the nested class within the outer class, indicating that the nested class is related to the outer class, but without establishing a strong relationship.

class Cookie {
val type = "Chocolate Chip"

class ChocolateChip {
fun getAmount(): String {
return "lots of" // Everyone loves lots of chocolate chips!
}
}

fun describe() {
val chips = ChocolateChip()
println("This is a $type cookie with ${chips.getAmount()} chips!")
}
}

Summary

Understanding Kotlin classes is key to becoming proficient in the language. From data classes that make handling data simple and efficient to abstract and sealed classes that help structure your code, each type of class serves a unique purpose. Enum classes provide clarity when dealing with fixed sets of constants, while nested and inner classes allow for more complex relationships within your code. Mastering these concepts will help you write cleaner, more maintainable, and effective Kotlin code, making you a Super developer.

Thank you for reading till the end! For more insightful articles, follow me on Medium and Linkedin

--

--