Kotlin gives you the ability to add more functionality to an existing class you may not even have access to, without inheriting it. That is what Kotlin extensions are in a nutshell.
Using extensions is easy and takes only a few lines of code. They allow you to adapt third-party and inbuilt classes to your needs. As a result, they are such an impressive tool that every Kotlin developer should know how to use them.
Let’s now dive into Kotlin extensions. First, you will learn what they are and how they work. Then, it will be time to delve into their most relevant uses. Finally, you will learn how to use them in real-world scenarios. Let’s get started!
As explained here in the official documentation, Kotlin allows you to extend a class by adding new features to it without having to inherit from the class or use the Decorator pattern. This is possible thanks to what Kotlin calls extensions.
In particular, Kotlin extensions let you add functions to a class that you cannot modify. By using them, you will be able to call these new functions as if they were part of the original class. Similarly, you can use this mechanism to add new properties to existing classes. You can also extend Kotlin companion objects.
As you can imagine, Kotlin extensions are an extremely powerful tools. Thankfully, defining them is easy, and you can do this with just a bunch of lines of code, as follows:
fun MutableList<String>.concatenateLowercase() : String { return this.map{ s -> s.lowercase() }.joinToString("") }
Notice how the this
keyword inside an extension function corresponds to the receiver object the function is called on.
Now, every MutableList<String>
instance has a concatenateLowercas()
function, as you can see here:
val list = mutableListOf("First", "seConD", "ThIRd") list.concatenateLowercase()
This snippet prints:
firstsecondthird
To understand how Kotlin extensions work, you need to learn how they are dispatched.
In particular, Kotlin extensions are resolved statically. This means that the extended function to call is determined by the type of the expression on which it is invoked at compile-time, rather than on the type resulting from evaluating that expression at runtime.
Let’s understand this better with an example:
open class Car class Convertible: Car() // defining the getType() extensions function on Car fun Car.getType() = "Generic car" // defining the getType() extensions function on Convertible fun Convertible.getType() = "Convertible car" fun getCarType(car: Car) : String { return convertible.getType() } fun main() { print(getConvertibleType(Convertible())) }
This would print:
Generic car
In OOP (Object-Oriented Programming) logic, you would expect this to print “Convertible car.” Well, this is not what happens when using extension functions. In fact, the getType()
extension function called is the one coming from the declared type of the car
parameter known at compile-time, which is the Car
class.
Also, you should know that Kotlin extensions are usually defined on the top-level scope, directly under package and import lines:
package com.logrocket.extensions fun MutableList<String>.concatenateLowercase() : String { return this.map{ s -> s.lowercase() }.joinToString("") }
Then, if you need to use it outside its declaring package, you can import it as you would with any external dependency:
package com.logrocket.example // importing the extension function defined in // the com.logrocket.extensions package import com.logrocket.extensions.concatenateLowercase fun main() { val list = mutableListOf("First", "seConD", "ThIRd") list.concatenateLowercase() }
Finally, you have to be aware that Kotlin extensions can also be defined on nullable types. Consequently, Kotlin extension functions can be called on an object variable even when its value is null.
In detail, you can handle a nullable receiver-type by manually checking for this == null
inside the body of the Kotlin extension function. Keep in mind that after the null check, this
will be automatically cast to the corresponding non-null type.
Let’s see this in action through an example:
fun Any?.hashCode(): Int { if (this == null) { return 0 } // 'this' is no longer nullable here // fallback logic // calling the original hashCode method defined in the // Any class return hashCode() }
Read this if you want to learn more about how null safety works in Kotlin.
Kotlin extensions are not limited to functions. On the contrary, this is a versatile and effective mechanism that allows you to achieve endless results. Let’s now delve into its most popular uses.
This is the most common use of the Kotlin extension feature. As you have already seen, adding an extension function to a class is very easy, and can be achieved as follows:
fun Int.doubleValue() : Int { return this * 2 }
In this way, any Int
instance will now have a doubleValue()
function that returns twice its current value. This was defined by taking advantage of the special this
keyword. With it, you can access the instance of the object with the type as resolved statically, and use it to implement your desired logic.
With Kotlin extensions, you can also add a new property to an existing class. Kotlin extension properties can be defined as shown in the example below:
val <T> List<T>.penultimateElement: T? get() = if (size < 1) null else list.get(size - 2)
Such a property allows you to easily retrieve the penultimate element of a list, if present. Let’s now see how to access it:
val list = mutableListOf("first", "second", "third") print(list.penultimateElement)
This would show the following text in your console:
second
As you can see, this new extension property can be accessed like any other normal property. The main difference with them is that extension properties cannot have initializers. This means that their value can only be handled by explicitly providing getters and setters.
In other words, the following snippet will return an “Extension property cannot be initialized because it has no backing field” error:
var <T> List<T?>.penultimateElement: T? = null get() = if (size < 1) null else get(size - 2)
The problem here lies in the first line because initializers are not allowed for extension properties in Kotlin.
If a class has a companion object, you can combine what you have just learned and define extension functions and/or properties for the companion object as well.
Since companion objects are accessible by using the Companion
property, all you have to do is specify it in your extension definition to add the extension to the object and not the class. Then, just like any other properties or functions of the companion object, they can be accessed or called using only the class name as the qualifier, as shown here:
class FooClass { // defining an empty companion object companion object { } } fun FooClass.Companion.sayHello() { println("Hello, World!") } fun main() { FooClass.sayHello() // this is just like writing FooClass.Companion.sayHello() }
When run, this snippet prints:
"Hello, World!"
Extensions can also be used in more intricate situations, but this is considerably more uncommon. However, let’s see them in action in an advanced case.
Extensions of one class can be defined inside another class. You might want to access both the external and the current classes. In other words, there are multiple implicit receivers in this case.
By default, this
refers to the object of the type where the extension is being defined. But if you wanted to access the external class, you can by using the qualified this
syntax. Let’s see how this works through a simple example:
class Foo() { fun sayHello() { println("Hello, World!") } fun sayBye() { println("Bye!") } } class User(val name: String, val foo: Foo) { // extending Foo with a new function fun Foo.sayGoodbye() { // calling the Foo sayBye() function this.sayBye() // calling the User sayGoodbye() function [email protected]() } fun sayGoodbye() { println("Goodbye, World!") } fun introduceYourself() { foo.sayHello() // Hello, World! println("My name is ${name}!") sayGoodbye() // "Goodbye, World!" } fun introduceYourselfWithExtendedFoo() { foo.sayHello() // Hello, World! println("My name is ${name}!") foo.sayGoodbye() // Bye! // Goodbye, World! } } fun main() { val foo = Foo() User("Maria", foo).introduceYourself() println("---------------") User("Maria", foo).introduceYourselfWithExtendedFoo() // foo.saidGoodBye() --> Error! The extension function is unavailable outside User }
This is what the example prints:
Hello, World! My name is Maria! Goodbye, World! --------------- Hello, World! My name is Maria! Bye! Goodbye, World!
As you can see, by using the qualified this
syntax, it was possible to access both the User
sayGoodbye()
function and the Foo
one. This is how conflicts between the implicit members of an extended function are resolved in Kotlin.
You have seen both basic and advanced ways to deal with Kotlin extensions. Now, you are ready to see them in action through two real-world examples.
Having to retrieve the day after a particular date is a common task. You can do it by using inbuilt functions, but you can also define an extension function with a more appealing and easy-to-remember name as follows:
import java.time.LocalDate fun LocalDate.getTomorrow() : LocalDate { return this.plusDays(1) } fun main() { val date = LocalDate.of(2022, 2, 15) println("Today:") println(date) println("\n----------\n") println("Tomorrow:") println(date.getTomorrow()) }
This snippet prints:
Today: 2022-02-15 ---------- Tomorrow: 2022-02-16
When dealing with dates in Kotlin, being able to easily access the string representation of the date in American or European format would be very useful. You can easily implement this with two extensions properties, as follows:
import java.time.LocalDate import java.text.SimpleDateFormat import java.time.format.DateTimeFormatter val LocalDate.americanFormatString : String get() = this.format(DateTimeFormatter.ofPattern("MM-dd-yyyy")).toString() val LocalDate.europeanFormatString : String get() = this.format(DateTimeFormatter.ofPattern("dd-MM-yyyy")).toString() fun main() { val date = LocalDate.of(2022, 2, 15) println("American format:") println(date.americanFormatString) println("\n----------\n") println("European format:") println(date.europeanFormatString) }
When run, this prints:
American format: 02-15-2022 ---------- European format: 15-02-2022
This way, each LocalDate
instance now has access to two new americanFormatString
and europeanFormatString
properties, which can save you time and avoid duplicate and boilerplate code.
In this article, we looked at what Kotlin extensions represent, how they work, and how and when to use them. As shown, this Kotlin feature represents one of the coolest for this programming language, and it allows you to extend classes coming from third-party libraries without inheritance. Also, you can use them to extend and adapt inbuilt classes to your needs.
Kotlin extensions allow you to customize classes defined by others and import these optional extensions only when and where required. Thus, they are a powerful tool that every Kotlin developer should be able to master.
Thanks for reading! I hope that you found this article helpful. Feel free to reach out to me with any questions, comments, or suggestions.
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.
Would you be interested in joining LogRocket's developer community?
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 nowBuild 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.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]