We have all used the process of inheritance, the object-oriented programming (OOP) concept in which any given class may inherit all the variable fields and functionality from another class.
The class inheriting the properties is called the child class, or subclass, while the class that provides the baseline properties is known as the parent class. This pattern is found all over Android applications.
All of our activity classes inevitably extend the superclass Activity
to override and manipulate some of its most fundamental functionality, such as its lifecycle.
With the arrival of Kotlin, we were introduced to several new ideas that hadn’t been present in Java before, such as Kotlin extensions, which provide the ability to extend a class with new functionality without having to inherit or subclass from it, as Kotlin’s official documentation explains.
Because of the similar nature that both classic inheritance and the contemporary Kotlin extensions provide, it’s difficult to determine which one of these tools to leverage to solve a problem at hand. This is why these concepts are usually contrasted side by side.
In this tutorial, we’ll compare and contrast inheritance and Kotlin extensions to understand the pros and cons of each. Here’s what we’ll cover:
Let’s expand on our previous example using the Activity
class. Traditionally, inheritance in Java was signified by the extends
keyword, which made it very explicit and apparent to the reader what was going on:
public class MyActivity extends Activity { ...
Nowadays in Kotlin, the inheritance pattern syntax is not as obvious as it used to be:
class MyActivity() : Activity() { ...
By default, and as the Kotlin official documentation describes, all classes in Kotlin inherit from a common superclass, Any
, which serves as the default parent class for all classes that do not declare one explicitly.
Any
has three main methods that all classes inherit: equals(), hasCode(), toString()
. All Kotlin classes are final, so they cannot be inherited. To make a class inheritable, the keyword open
needs to be present at the beginning of the class signature, which also makes them non-final.
open class Activity() { ... } class MyActivity() : Activity() { ...
Inheritance also allows the child class to access all protected fields, functions, and variables from the parent class, which are typically hidden from all outside callers. If access wasn’t enough, child classes may also override either functions or variables from the superclass, as long as they’re visible to the subclass and non-final as well.
open class Activity() { ... } class MyActivity(): Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... } ... }
For more detailed information on inheritance, check out either Java’s official documentation for a more traditional approach to OOP’s inheritance or Kotlin’s more modern approach to the topic.
Kotlin extensions allow us to extend a class without the need to inherit, or without using design patterns that enable this sort of functionality, such as the Decorator design pattern.
We can extend the functionality of all kinds of classes, including final classes, third-party library classes, etc., without the need to create a subclass. Because we are not subclassing our base class, extension functions only have access to public fields, and do not protect ones as regular inheriting classes usually do.
The added functionality from extensions is resolved statically and can be called normally as any other regular method and from any instance of the class that has been extended.
Here’s an example of a very common function we execute in every Android application:
fun Activity.requestPermission(permission: String, requestCode: Int) { ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode) }
Requesting permissions usually requires an instance of Activity
, which serves as a perfect opportunity to add such functions as an extension function into the general Activity
class. It can be called directly from any particular Activity
class implementation.
open class Activity() { ... } fun Activity.requestPermission(permission: String, requestCode: Int) { ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode) } class MyActivity: Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... requestPermission( permission = Manifest.permission.ACCESS_FINE_LOCATION, requestCode = 2022 } ... }
By extending a class, we are not inserting new properties into it. Instead, we’re creating new functions that can be called using the dot-notation (.) on variables of this type.
The new Kotlin extensions paradigm also introduced extension variables with it, which work similarly to how extension functions do except that, because we don’t have full visibility or access to the base class, extension variables cannot be backed up by the base class altogether. They also cannot access protected fields from the base class.
For a deeper analysis of the extent of possibilities that Kotlin extensions can provide, check out my fellow LogRocketeer’s article on the topic.
Let’s compare and contrast the main differences between them.
We’ll start with the type of classes each of these concepts can modify. While inheritance can only inherit properties from non-final classes, i.e., open classes in Kotlin, Kotlin extensions can add functionality to all kinds of classes, no matter their “openness.”
open class InheritableClass() { … }
Kotlin extensions can extend properties into classes without the need to subclass. When it comes to the accessibility of the base class, inherited child classes have access to all protected variables from the parent class, whereas extensions do not have access to the protected fields of their base class the same way.
open class InheritableClass() { … } // This class can be inherited from class InheritingClass : InheritableClass() { … } // This class is inheriting, but may not be inherited from fun InheritingClass.someExtension() { … } // Extension may happen in non-open classes
Although child classes can add new variables into the object instance declaration of the parent class, Kotlin extensions cannot add variables that are backed up using extension variables, as these are only processed statically.
open class InheritableClass() { … } class InheritingClass : InheritableClass() { … }
Here’s a comparative table to visualize the differences more structurally:
Adding functionality | Adding functionality without subclassing | Extending final classes (non-open) | Adding variables with backing fields | Accessing protected fields and functions | |
Inheritance | ✅ | ❌ | ❌ | ✅ | ✅ |
Kotlin Extensions | ✅ | ✅ | ✅ | ❌ | ❌ |
Of course, different tools and concepts are meant to be used for different situations. Kotlin extensions are great for creating quick-access functionality for any given class, no matter where it comes from. They serve as a great substitute for the old Java notion of utility
classes, as we can now simply add new functions that are easily discoverable and utilized without much boilerplate code.
On the other hand, using inheritance is ideal whenever we need to create a hierarchy of related classes, as it allows for natural grouping thanks to the parent-child relationship it creates between the members of such a hierarchy. It also allows the inheriting class to override the previous functionality of parent classes, something that can be effective whenever we need similar objects to act slightly differently.
Both inheritance and extensions have their advantages and disadvantages. We may prefer one over the other, depending on the type of project you’re working on. I hope this overview helped you better understand Kotlin extensions and inheritance. Thanks for reading!
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your Android apps — try LogRocket for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.