Kotlin OOP in Depth: Classes, Sealed Types, Generics, SOLID

Android Development with Kotlin: 21 Real Projects · Kotlin and Android in Depth · 4 min read · Updated

Sealed types force every case

  1. A sealed type lists every possible subtype in one place.
  2. A when expression over it has to handle each one.
  3. The screen shows something sensible for every case.
  4. Add a new case later and the compiler points to every when you must update.

Lesson 3 gave you the basics of classes. Professional Android code uses the full object-oriented toolbox, and interviewers ask about it constantly. This lesson walks through each tool with a small example and says when to use it.

Classes, constructors and properties

A class has a primary constructor in its header and may have secondary constructors. Writing val or var in the header makes the parameter a property. init blocks run when an object is created. Properties can have custom getters, and a private set hides who may change them.

class User(val name: String, var age: Int = 18) {
    val isAdult: Boolean get() = age >= 18

    init {
        require(name.isNotBlank()) { "name is empty" }
    }

    constructor(name: String, birthYear: Int, now: Int) : this(name, now - birthYear)
}

Inheritance, abstraction and polymorphism

Kotlin classes are final by default. Mark a class open to allow subclasses, and mark members open to allow overriding. An abstract class cannot be created itself and may hold shared code. An interface lists functions (and may give default bodies); a class can implement many interfaces but extend only one class. Polymorphism means code that works with the parent type accepts any child. Prefer interfaces for what something *can do*, and inheritance for a true is-a relationship.

interface Shape { fun area(): Double }

class Circle(private val r: Double) : Shape {
    override fun area() = Math.PI * r * r
}

class Square(private val side: Double) : Shape {
    override fun area() = side * side
}

fun totalArea(shapes: List<Shape>) = shapes.sumOf { it.area() }

Enums, sealed classes and objects

An enum class is a fixed set of named values. A sealed class or interface is a closed family of types, and a when over it must cover every case or the compiler complains. Android developers use sealed types for UI state and for results. An object is a class with exactly one instance (a singleton). A companion object holds members that belong to the class itself, like Java statics.

sealed interface Result<out T> {
    data class Success<T>(val value: T) : Result<T>
    data class Failure(val message: String) : Result<Nothing>
    data object Loading : Result<Nothing>
}

fun show(r: Result<String>) = when (r) {
    is Result.Success -> "OK: " + r.value
    is Result.Failure -> "Error: " + r.message
    Result.Loading -> "Loading"
}

Nested classes, generics and delegation

A nested class cannot see its outer object. An inner class can. Generics let one class or function work with many types while staying type-safe: class Box<T>(val item: T). You can restrict the type with <T : Comparable<T>>. Delegation with by lets a class pass interface work to another object, and by lazy or Delegates.observable delegate a property. Delegation is Kotlin's answer to "prefer composition over inheritance".

class Logger : Printer by ConsolePrinter()   // forwards Printer calls

val expensive: String by lazy { loadBigThing() }  // computed once, on first use

Visibility modifiers

  • public (default): visible everywhere.
  • private: inside the class or file only.
  • protected: the class and its subclasses.
  • internal: inside the same module (a Gradle module). Useful in multi-module apps.

SOLID and common patterns

  • Single responsibility: a class has one reason to change. A ViewModel holds screen state; a repository gets data.
  • Open/closed: add behaviour by adding a new class, not by editing a long when.
  • Liskov substitution: a child must work wherever the parent is expected.
  • Interface segregation: small interfaces, not one huge one.
  • Dependency inversion: depend on an interface; let Hilt supply the real class. This is how you swap a fake repository into a test.

Patterns you will meet: Singleton (object), Factory (a function or companion that builds objects), Builder (Kotlin named arguments and apply replace most builders), Observer (Flow and LiveData) and Strategy (pass a lambda or an interface). Do not force a pattern. Use one when it removes real duplication.

Your task

  1. Create a sealed interface PaymentMethod with Card, Upi and Cash subclasses, and a when that returns a fee for each.
  2. Write an interface Notifier with two implementations and a function that takes the interface.
  3. Make a generic Stack<T> class with push, pop and peek.
  4. Add a private set property and show that outside code cannot change it.

How to check it worked

  • The compiler warns when you forget a sealed case.
  • You can explain open, abstract, interface and sealed in one sentence each.
  • You can name the five SOLID principles with an Android example.

Frequently asked questions

Class, abstract class or interface?

Use an interface to describe what something can do. Use an abstract class when subclasses share real code or state. Use a plain class when no subclassing is planned.

Why are Kotlin classes final by default?

Inheritance makes code fragile, so Kotlin makes you choose it on purpose with open. Composition and interfaces are usually safer.

What is the difference between object and companion object?

An object is a standalone singleton. A companion object belongs inside a class and holds members you call on the class name.