Classes are “final” by default in Kotlin

If you’re used to Java, Kotlin has a little surprise waiting for you:

Every class in Kotlin is final by default.
Meaning: You cannot inherit from a class unless the class says “yes, I allow it.”

This small rule has a big impact on how you design your app—especially in Android development where base classes and custom behaviors are common.

Let’s understand this in a simple, fun, and practical way (with Jetpack Compose examples too!).


🔒 1. What Does “Final” Really Mean?

A final class is like a sealed box:
You can use it, instantiate it, but you can’t extend it.

Example:

class Animal {
    fun makeSound() = println("Some sound")
}

Try this:

class Dog : Animal()

Kotlin errors:

This type is final, so it cannot be inherited from

Why?
Because you didn’t mark Animal as open.


🔓 2. Making a Class Inheritable: open

open class Animal {
    open fun makeSound() = println("Some sound")
}

class Dog : Animal() {
    override fun makeSound() = println("Woof! 🐶")
}

Important points:

  • open class Animal → allows inheritance.
  • open fun makeSound() → allows overriding.
  • Both class and function need open.

🧬 3. Functions Are Also Final by Default

Even inside an open class:

open class Animal {
    fun walk() = println("Walking…")    // cannot be overridden
    open fun makeSound() = println("Some sound")  // can be overridden
}

Kotlin’s rule:

“If you want polymorphism, you must ask clearly.”


🧠 4. Why Kotlin Chooses “Final by Default”

✔ Safer APIs

No accidental inheritance.

✔ Predictable behavior

Override only when intended.

✔ Better performance

Final classes and methods can be optimized more aggressively.

✔ Encourages composition over inheritance

Kotlin nudges you toward cleaner architectures.


🎨 5. Jetpack Compose Example: UI State Modeling Uses Final Classes

Compose applications often use immutable UI state models.

Example:

sealed class HomeUiState {
    object Loading : HomeUiState()
    data class Success(val items: List<String>) : HomeUiState()
    data class Error(val message: String) : HomeUiState()
}

Here:

  • sealed class → subclasses allowed only within the same file
  • No one outside can extend HomeUiState
  • This guarantees predictable state handling in when expressions

Using it inside a Composable:

@Composable
fun HomeScreen(uiState: HomeUiState) {
    when (uiState) {
        is HomeUiState.Loading -> CircularProgressIndicator()
        is HomeUiState.Success -> ItemsList(uiState.items)
        is HomeUiState.Error -> ErrorMessage(uiState.message)
    }
}

Imagine if classes were open by default—your state machine could become unpredictable fast.

Kotlin protects you from that.


🎛 6. Jetpack Compose Example: ViewModel Customization With open

Sometimes you do want inheritance.

Example: a common analytics logger inside ViewModels.

open class BaseViewModel : ViewModel() {
    open fun log(event: String) {
        println("Log: $event")
    }
}

Subclasses can override:

class HomeViewModel : BaseViewModel() {
    override fun log(event: String) {
        println("Home Event: $event")
        super.log(event)
    }
}

Why use open here?

  • You intentionally want custom behavior per screen
  • You control the inheritance chain
  • You avoid accidental extension elsewhere

🧩 7. Real-world Compose Use Case: Custom Modifiers

Imagine you want a base clickable card modifier.

open class CardStyle {
    open fun modifier(): Modifier = Modifier
        .padding(16.dp)
        .clip(RoundedCornerShape(12.dp))
}

A specialized style:

class ElevatedCardStyle : CardStyle() {
    override fun modifier(): Modifier = super.modifier()
        .shadow(8.dp)
}

And using it:

@Composable
fun StyledCard(style: CardStyle, content: @Composable () -> Unit) {
    Box(
        modifier = style.modifier()
            .background(Color.White)
            .padding(12.dp)
    ) {
        content()
    }
}

Because we explicitly made the class open, we safely allow customization.


🚧 8. Using final override to Stop Further Extension

Useful when you want to override something exactly once:

open class Animal {
    open fun sound() = println("Some sound")
}

open class Dog : Animal() {
    final override fun sound() = println("Woof!")
}

class Puppy : Dog() {
    // ERROR: Cannot override `sound`
}

Great for API stability.


🛠 9. When Should You Use open?

Use open when:

✔ You’re designing reusable base components
✔ You expect custom overrides
✔ You’re modeling polymorphic behavior intentionally

Do not use open:

✘ “Just in case someone needs it”
✘ When composition or delegation is cleaner
✘ For data classes (they’re meant to be value holders)


📝 10. Summary

ModifierMeaning
finalNo inheritance (default)
openAllow inheritance
abstractMust be overridden
sealedControlled inheritance — subclasses must be in the same file

Kotlin’s default final behavior is not a limitation—it’s a safeguard.
It keeps your code clean, scalable, and predictable, especially in Android and Jetpack Compose projects.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *