If you’re used to Java, Kotlin has a little surprise waiting for you:
Every class in Kotlin is
finalby 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
whenexpressions
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
| Modifier | Meaning |
|---|---|
final | No inheritance (default) |
open | Allow inheritance |
abstract | Must be overridden |
sealed | Controlled 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.

Leave a Reply