Skip to content

Recommended Reading

Kotlin Extension Functions: Add Functionality Without Modifying Code

13 Minutes

Kotlin Extension Functions: Add Functionality Without Modifying Code

Fix Bugs Faster! Log Collection Made Easy

Get started

Imagine you own a car. It’s reliable, runs smoothly and gets you where you need to go. But one day, you realize you need a GPS navigation system for better routes. What do you do?

Would you redesign the entire car just to integrate GPS, or would you simply install a GPS device on the dashboard? Of course, the smarter choice is to add the GPS instead of modifying the car’s built-in system.

This is exactly how a Kotlin Extension Function works. Instead of modifying the code of an existing class or interface, or relying on its inheritance, developers can add new functionality externally – just like upgrading your car without altering its core structure.

And here’s the really cool part about extension functions: whereas in Java they look like regular functions, in Kotlin, they appear as if they are directly part of the inner class itself. This makes them more intuitive, and it’s great for static method coding (where we call a class directly by name, without creating an object).

Today, we’re going to dive into the whole topic of extension functions usage in Kotlin. It should be ideal for:

Good to go? Let’s get stuck into it.

First off: how do we create a Kotlin function?

In Java, if you want to add a new function to an existing class (such as String or List), you typically extend the class through inheritance or create a separate utility function, both of which can make the code clunkier and harder to maintain. In Kotlin? Well, to show you the difference, let’s run some code.

For example, let’s create a function that performs an operation on a String. In Java code, a developer might do it like this:

String myString = someOperation(input);

But with Kotlin Extension Functions, the source code is way simpler and more natural:

val escaped = input.someOperation()

Here, input is a String, and we are calling someOperation() on it, just like a built-in method of String class.

Not only is this easier to read, but Integrated Development Environments (IDEs) will be able to offer the extension method as an autocomplete option, as if it were a standard method on the String class.

An important thing to note

As we’ve noted, extension functions don’t actually modify the classes they extend. As a Kotlin developer, when we define an extension, we aren’t adding new members to the original class itself, we’re simply allowing new functions to be called using dot notation on the relevant Kotlin variable.

One key thing to remember is that extension functions are resolved at compile time. Kotlin determines which extension function to call, based on the type of the variable, before the program runs.

Excited to learn more?

Great: we’re excited to teach you! Let’s dive in and explore:

  • Writing our own extension functions. Syntax and fundamentals of creating extensions.
  • Leveraging nullability in extension functions. Handling nullable types effectively.
  • Generics in extension functions. Making extensions more flexible.
  • Extension properties. Adding properties without modifying the class.
  • Infix with extension functions. The cherry on the top – Enhancing readability and elegance.
  • Companion object extensions. Extend the companion class object.
  • Advanced extension function ideas. Useful functions you can implement right away.
  • Using extension functions in Java (interoperability). Calling Kotlin extensions from Java.
  • When and how to use extension functions. Best practices and real-world use cases.
  • Organizing extension functions. Keeping your code clean and maintainable.

Writing our own Extension Functions

Now, this is the moment we’ve all been waiting for, right? We’ve talked enough about theory, now let’s get into the coding! 🚀

Understanding the Anatomy of an Extension Function

fun ClassName.functionName(parameters): ReturnType {
    // Function body
}

Writing an extension function is just like writing any regular function in Kotlin. The only difference? We specify the class that we are extending before the function name; this is how we declare an extension!

Example: Creating a Full URL from a Path

Imagine you have an image path, and you always need to append a BASE_URL before using it in your app.

// Base URL
val BASE_URL = "<https://some.base.url>"

// Extension function to convert a relative path into a full URL
fun String.toFullUrl() = BASE_URL + this

fun main() {
    val image = "/images/my_image.jpg"

    val url = image.toFullUrl()

    println(url)
}

// OUTPUT
// <https://some.base.url/images/my_image.jpg>

The toFullUrl() function extends the String class seamlessly, without modifying its internal implementation or structure.

But why use an extension function?

Instead of writing BASE_URL + imagePath everywhere, an extension function keeps it reusable and clean! 🚀

Leveraging nullability in Extension Functions

When working with nullable numbers, comparison operations can get tricky due to the need for explicit null checks. Let’s see how we can simplify this using extension functions!

The Problem: Comparing Nullable Numbers

Imagine you have two nullable numbers that need to be compared:

val numA: Int? = 5
val numB: Int? = null

val isNumAGreaterThanNumB = numA > numB // ERROR!

🚨 Oops! The IDE throws errors:

  • Type mismatch: Required Int, but found Int?
  • Operator call issue: 'numA.compareTo(numB)' is not allowed on a nullable receiver

A Quick Fix: Using ?: (Elvis Operator)

val isNumAGreaterThanNumB = (numA ?: 0) > (numB ?: 0)

This instance works, but writing ?: 0 everywhere is a waste of your time. Instead, it can be improved with an extension function.

The Better Solution: Using an Extension Function

Instead of adding null checks manually every time, let’s create a reusable extension function that handles nullability for us:

fun Number?.isGreaterThan(number: Number?): Boolean {
    if (this == null) return false
    if (number == null) return true
    return this.toDouble() > number.toDouble()
}

fun main() {
    val numA: Int? = 5
    val numB: Int? = null
    println(numA.isGreaterThan(numB)) // true
}

And that’s it – no more manual null checks! 🎉

This simple extension function removes boilerplate code, improves readability and ensures our comparisons always work, regardless of null values or numeric types.

But wait, there’s more!

We’ve mastered handling nullability, but what if we want our extension functions to be even more flexible?

Let’s dive into the power of Generics in Extension Functions!

Generics in Extension Functions! 🚀

Generics allow us to write a single extension function that works with multiple types.

For example, instead of writing different functions for Int, Double, and String class, we can create a generic extension function that works with any type.

Example: A Generic Logging Function

fun <T> T.log() {
    println("Value: $this (Type: ${this::class.simpleName})")
}

fun main() {
    val number = 42
    val text = "Hello, Kotlin!"
    val pi = 3.14159

    number.log()
    text.log()
    pi.log()
}

Output:

Value: 42 (Type: Int)
Value: Hello, Kotlin! (Type: String)
Value: 3.14159 (Type: Double)

Here, <T> makes the function work with any type without needing separate functions.

Extension Properties

We’ve explored extension functions, but Kotlin has another powerful feature, called Extension Properties. Just like functions, you can add properties to a class without modifying its original definition.

How do Extension Properties work?

Let’s say you have a Person class, and you want to add an extension property that returns the full name without changing the class itself:

class Person(val firstName: String, val lastName: String)

// Extension property to get the full name
val Person.fullName: String
    get() = "$firstName $lastName"

fun main() {
    val person = Person("Jhon", "Doe")
    println(person.fullName) // Output: Jhon Doe
}

Limitations of Extension Properties

  1. Cannot Store Values. Extension properties don’t have a backing field, so they can only return computed values, not stored data. This means mutable (var) extension properties are not allowed.
  2. No Private Access. They can’t access private properties or methods of the class they extend, only public members.
  3. No Field Addition. They don’t actually add new properties to a class, just a way to compute values dynamically.
  4. Naming Conflicts. If an extension property has the same name as a class property, the class property takes priority, which can lead to confusion.

Infix with Extension Functions: The cherry on top! 🍒

In Kotlin, functions marked with the infix keyword can be called without using dots or parentheses. This makes function calls look more natural and readable, especially when designing DSL-style code.

However, not all functions can use infix notation. Instead they must meet the following conditions:

  • They must be member functions or extension functions.
  • They must have exactly one parameter.
  • The parameter must not be vararg and must not have a default value.

Understanding Infix Expressions

Normally, when calling a function, we use dot notation:

fun Int.add(number: Int): Int {
    return this + number
}

val num = 10
val newNum = num.add(5)
println(newNum)  // Output: 15

By adding the infix keyword, we can call the function without the dot and parentheses, making the syntax cleaner:

infix fun Int.add(number: Int): Int {
    return this + number
}

val num = 10
val newNum = num add 5  // Now called using infix notation
println(newNum)  // Output: 15

This approach improves readability and makes function calls feel more natural.

Companion Object Extensions

What if you want to extend the functionality of a companion object? Kotlin allows this through companion object extensions. These work just like regular extension functions, but they specifically extend the companion object of a class.

💡In Kotlin, a companion object is a special type of object that belongs to a class but acts like a static member (similar to static in Java). It allows you to define functions and properties that can be accessed without creating an instance of the class.

Example:

class MyClass {
    companion object { }  // This is the companion object
}

// Define an Extension Function for the Companion Object
fun MyClass.Companion.printCompanion() { 
    println("MyClass companion") 
}
  • This function extends the companion object of MyClass.
  • It can be called using just the class name, without needing an instance of MyClass.

Usage:

fun main() {
    MyClass.printCompanion()  // Calling the function directly on the class
}

Output:

MyClass companion

Advanced Extension Function ideas

Network Connectivity Check (Android)

fun Context.isOnline(): Boolean {
    val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val networkInfo: NetworkInfo? = connMgr.activeNetworkInfo
    return networkInfo?.isConnected == true
}

// Usage
val isOnline = applicationContext.isOnline()
// or
val isOnline = context.isOnline()

Converting Any Object to JSON (Using Gson)

fun Any.toJson(): String = Gson().toJson(this)

// Usage
data class User(val name: String, val age: Int)

val user = User("Jhon", 25)
val json = user.toJson() // Output: {"name":"Jhon","age":25}

Converting JSON String to Any Object (Using Gson)

inline fun <reified T> String.fromJson(): T = Gson().fromJson(this, T::class.java)

// Usage
val jsonString = """{"name":"Jhon","age":25}"""
val user: User = jsonString.fromJson()
println(user.name) // Jhon

Deep Copy Using (Using Gson)

This function converts an object to JSON and then deserializes it back into a new object, ensuring a deep copy is created.

inline fun <reified T> T.jsonCopy(): T {
    val json = Gson().toJson(this)
    return Gson().fromJson(json, T::class.java)
}

// Usage
data class User(val name: String, val age: Int)
val user = User("Jhon", 25)
val copiedUser: User = user.jsonCopy()

println("Original: $user")
println("Copied: $copiedUser")
println("Are objects the same? ${user === copiedUser}") // false (different instances)

Prevent Double Clicks on a View

fun View.setSingleClickListener(delay: Long = 500, action: (View) -> Unit) {
    this.setOnClickListener(object : View.OnClickListener {
        private var lastClickTime = 0L

        override fun onClick(v: View) {
            val currentTime = System.currentTimeMillis()
            if (currentTime - lastClickTime >= delay) {
                lastClickTime = currentTime
                action(v)
            }
        }
    })
}

// Usage
button.setSingleClickListener {
    println("Button clicked!")
}

Since you can create functions based on your needs, the possibilities are endless.

Android-KTX libraries frequently use extension functions to simplify development. Examples include:

  • List<T>.getOrNull() – Safe element retrieval.
  • List<T>.lastIndex – Gets the last valid index.
  • Canvas.withClip() – Simplifies clipping operations.
  • Bitmap.toDrawable() – Converts a Bitmap to a Drawable.

Extension functions reduce boilerplate and improve code readability, making Kotlin even more powerful🚀

Using extension functions in Java (Interoperability)

Since Kotlin is built on top of Java, it ensures seamless interoperability between both languages. However, Kotlin extension functions don’t directly translate into member functions in Java. Instead, they act as static utility functions, requiring explicit calls from Java class.

Example: Accessing an Extension Function in Java class

Let’s say we define an extension function in StringExt.kt:

// Extension function to convert a relative path into a full URL
fun String.toFullUrl() = BASE_URL + this

To call this function from Java, we refer to the Kotlin file name followed by Kt, as Kotlin compiles extension functions into static methods:

String image = "/images/my_image.jpg";
String url = StringExtKt.toFullUrl(image);

Although extension functions behave like utility functions in Java, they remain a powerful tool for keeping Kotlin code concise and readable.

When and How to Use Extension Functions

Extension functions are powerful, but they should be used thoughtfully. Here are some best practices and real-world use cases:

When to Use Extension Functions

  • Enhancing Existing Classes. Add functionality to third-party or built-in classes without modifying their source.
  • Improving Code Readability. Replace repetitive utility functions with cleaner, more natural syntax.
  • Reducing Boilerplate Code. Avoid redundant code by defining reusable extensions.

When to Avoid Extension Functions

  • If it makes code hard to understand. Overuse of extensions can make it unclear where a function is defined.
  • If it conflicts with class members. If an extension function has the same name as a class function, the member function takes precedence.
  • If you need private access. Extension functions can’t access private members of the class they extend.

Organizing Extension Functions

To keep your code clean and maintainable, extension functions should be structured properly.

Best Practices for Organizing Extensions

  • Group by context. Store extension functions in files related to their purpose.
    • StringExt.kt → String-related extensions
    • ViewExt.kt → Android View extensions
    • NetworkExt.kt → Network-related extensions
  • Use meaningful file names. Avoid dumping all extensions into one file. Use descriptive names based on functionality.
  • Keep extensions modular. Avoid making extensions too large or complex. Each extension should be focused on a single responsibility.
  • Document important extensions. If an extension function does something non-obvious, add documentation to avoid confusion.

By following these practices, you ensure that extension functions remain an asset rather than a burden, keeping your Kotlin codebase clean, structured and easy to navigate. 🚀

To Recap

Kotlin’s extension functions provide a clean and efficient way to enhance existing classes without inheritance or modification. They allow better code organization, reusability, and readability, making development faster and more intuitive.

  • We explored how extension functions work under the hood, their syntax, and real-world use cases.
  • Extension properties help add computed properties, while infix notation makes function calls more expressive.
  • Advanced extension functions demonstrate their power in Android, JSON serialization, and utility operations.
  • Interoperability with Java ensures Kotlin extension functions remain useful in mixed-language projects.
  • Best practices for structuring and using extension functions help maintain a scalable and organized codebase.

By leveraging extension functions effectively, you can write concise, elegant, and highly maintainable Kotlin code. 🚀

May your coding adventures in Kotlin be productive and enjoyable. Happy coding!

Expect The Unexpected!

Debug Faster With Bugfender

Start for Free
blog author

Meet Miyani

Android Developer skilled in Kotlin with 3 years of experience. Loves learning new tech and keeping up with the latest in coding. Eager to explore and grow in the tech world. You can contact him on Linkedin, Stack Overflow and Github

Join thousands of developers
and start fixing bugs faster than ever.