Antonello Zanini I'm a software engineer, but I prefer to call myself a technology bishop. Spreading knowledge through writing is my mission.

Everything you need to know about Kotlin extensions

6 min read 1793

Everything You Need To Know About Kotlin Extensions

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!

What is a Kotlin extension?

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

How Kotlin extensions work

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.

Extension functions

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.

Extension properties

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.

Extending companion objects

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!"

Advanced use of Kotlin extensions

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.

Defining extensions inside a class

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.

Kotlin extensions in action

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.

Function extensions

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

Property extensions

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 LocalDateinstance now has access to two new americanFormatString and europeanFormatString properties, which can save you time and avoid duplicate and boilerplate code.

Conclusion

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: Instantly recreate issues in your Android apps.

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 — .

Antonello Zanini I'm a software engineer, but I prefer to call myself a technology bishop. Spreading knowledge through writing is my mission.

Leave a Reply