Question

Static Extension Methods in Kotlin: What’s Possible and How to Do It

kotlinstatic-methods

Question

In Kotlin, how can you define a static extension method? Is that possible?

For example, I currently have this extension function:

fun Uber.doMagic(context: Context) {
    // ...
}

This can be called on an instance:

val uberInstance = Uber()
uberInstance.doMagic(context)

But how can I make it work like this instead?

Uber.doMagic(context)

In other words, can Kotlin extension functions be defined as static or class-level methods for a type rather than instance-level calls?

Short Answer

By the end of this page, you will understand how Kotlin extension functions really work, why Uber.doMagic() is not the same as uberInstance.doMagic(), and what alternatives Kotlin provides when you want class-level or utility-style behavior. You will also see practical patterns using companion object, regular functions, and companion extensions.

Concept

Kotlin extension functions let you add callable behavior to an existing type without modifying its source code.

For example:

fun Uber.doMagic(context: Context) {
    // ...
}

This means: “add a function named doMagic that can be called as if it belongs to an Uber instance.”

Important idea

Extension functions do not actually change the class. They are just regular functions that the compiler lets you call with dot syntax.

So this:

uberInstance.doMagic(context)

is essentially compiled like a normal function call where uberInstance is passed as an argument.

Why Uber.doMagic(context) does not work

Uber by itself refers to the class, not an instance of the class. Your extension is defined on Uber instances, so Kotlin expects something like:

val uber = Uber()
uber.doMagic(context)

If you want something callable on the class-like name itself, Kotlin does not have Java-style static members. Instead, Kotlin uses:

  • top-level functions
  • companion objects
  • extension functions on companion objects

What matters in real programming

This distinction matters because developers often want one of two different things:

  • behavior tied to a specific object instance
  • utility behavior tied to a type or namespace

Choosing the right approach keeps your code easier to read and maintain.

Mental Model

Think of an extension function like a shortcut label attached outside a box.

  • The box is the original class, such as Uber
  • The shortcut label says, “when you have an Uber object, you may call this extra function”
  • But the label is not stored inside the class itself

So if you have an actual box:

val uber = Uber()
uber.doMagic(context)

that works.

But Uber as a class name is more like the blueprint for boxes, not a box itself. An instance extension works on boxes, not on blueprints.

If you want behavior on the blueprint-like name, use a companion object or a top-level function.

Take Quiz

Syntax and Examples

1. Instance extension function

This is what you already have:

class Uber

fun Uber.doMagic(context: Context) {
    println("Magic on an Uber instance")
}

Usage:

val uber = Uber()
uber.doMagic(context)

This works because the extension receiver is an instance of Uber.

2. Top-level function as a utility

If you do not need an instance, use a normal function:

fun doMagic(context: Context) {
    println("Magic without an Uber instance")
}

Usage:

doMagic(context)

This is the simplest option when no object state is needed.

3. Use a companion object

If you want class-like access such as Uber.doMagic(...), define a companion object:

class Uber {
    companion object {
        fun doMagic(context: Context) {
            println("Magic from Uber companion")
        }
    }
}

Usage:

Uber.doMagic(context)

4. Extension function on a companion object

You can also extend the companion object itself:

class Uber {
    companion object
}

fun Uber.Companion.doMagic(context: Context) {
    println("Magic from companion extension")
}

Usage:

Uber.doMagic(context)

This is the closest match to a “static extension method” in Kotlin.

When to use which

  • Use fun Uber.doMagic() when the function needs an Uber instance
  • Use fun doMagic() when the function is just a utility
  • Use companion object when the function belongs to the type itself
  • Use fun Uber.Companion.doMagic() when you want extension-style syntax on the companion

Step by Step Execution

Consider this example:

class Uber {
    companion object
}

fun Uber.doMagic() {
    println("instance extension")
}

fun Uber.Companion.doMagic() {
    println("companion extension")
}

fun main() {
    val uber = Uber()
    uber.doMagic()
    Uber.doMagic()
}

What happens step by step

1. val uber = Uber()

This creates an instance of the Uber class.

2. uber.doMagic()

Kotlin looks for a callable function for an Uber instance.

It finds:

fun Uber.doMagic()

So it calls the instance extension and prints:

instance extension

3. Uber.doMagic()

Here, Uber refers to the companion object access point, not an instance.

Kotlin looks for a function that works with Uber.Companion.

It finds:

fun Uber.Companion.doMagic()

So it prints:

companion extension

Key takeaway

The two calls look similar, but they target different receivers:

  • uber.doMagic() → receiver is an Uber instance
  • Uber.doMagic() → receiver is Uber.Companion

Real World Use Cases

Instance extension functions

Useful when behavior depends on a specific object.

Examples:

  • Formatting a model object for display
  • Converting an entity to a DTO
  • Adding helper behavior to Android Context, View, or Fragment
  • Validation logic that depends on object data

Example:

fun User.fullName(): String = "$firstName $lastName"

Companion object functions

Useful when behavior belongs to the type as a whole.

Examples:

  • Factory methods such as User.fromJson(...)
  • Validation rules that create objects
  • Parsing helpers
  • Constants and type-level utility methods

Example:

class User(val name: String) {
    companion object {
        fun fromName(name: String): User = User(name.trim())
    }
}

Companion object extensions

Useful when:

  • you cannot edit the original class directly
  • the class already has a companion object
  • you want ClassName.function() syntax from outside the class

Example:

class Config {
    companion object
}

fun Config.Companion.loadDefault(): Config {
    return Config()
}
Take Quiz

Real Codebase Usage

In real Kotlin codebases, developers usually pick one of these patterns:

1. Top-level helper functions

Very common for stateless logic.

fun parseUberId(value: String): Int = value.toInt()

Why teams use it:

  • simple
  • easy to test
  • no unnecessary class wrapping

2. Extension functions for readability

Often used to make code read naturally.

fun String.isValidUberCode(): Boolean = length == 8

Why teams use it:

  • expressive call sites
  • good for transformations and small helpers

3. Companion objects for factory methods

A common pattern when object creation should be controlled.

class Uber private constructor(val id: Int) {
    companion object {
        fun create(idText: String): Uber {
            val id = idText.toIntOrNull() ?: error("Invalid id")
            return Uber(id)
        }
    }
}

4. Guard clauses and validation

Companion or top-level functions are often used for validation before object creation.

fun requireValidName(name: String) {
    require(name.isNotBlank()) { "Name must not be blank" }
}

5. Extension on companion for external libraries

If a class comes from a library and exposes a companion object, teams may extend it externally.

class ExternalType {
    companion object
}

fun ExternalType.Companion.defaultInstance(): ExternalType = ExternalType()

This gives convenient call syntax without modifying library code.

Common Mistakes

1. Expecting extension functions to truly become class members

Beginners often think this:

fun Uber.doMagic() {}

adds a real method inside Uber.

It does not. It only adds a new callable syntax for code that can see the extension.

2. Trying to call an instance extension on the class name

Broken example:

class Uber

fun Uber.doMagic() {}

fun main() {
    Uber.doMagic() // Error
}

Why it fails:

  • Uber is a type name here
  • the extension expects an Uber instance

Fix:

val uber = Uber()
uber.doMagic()

or use a companion-based solution.

3. Confusing companion object functions with Java static

Kotlin does not use static the same way Java does.

Instead of:

Uber.doMagic();

in Kotlin you typically write a companion object function:

class Uber {
    companion object {
        fun doMagic() {}
    }
}

4. Using an extension when a normal function is clearer

Not everything should be an extension.

Less clear:

fun Uber.Companion.calculateTax(amount: Double): Double = amount * 0.2

Sometimes clearer:

fun calculateTax(amount: Double): Double = amount * 0.2

Use the style that best matches the meaning.

5. Forgetting the companion object declaration

This will not work if there is no companion object to extend:

class Uber

fun Uber.Companion.doMagic() {} // Error if no companion exists

Fix:

class Uber {
    companion object
}

Comparisons

ApproachCall syntaxNeeds instance?Best for
Instance extensionuber.doMagic()YesBehavior related to one object
Top-level functiondoMagic()NoGeneral utility logic
Companion object memberUber.doMagic()NoType-level behavior, factories
Companion object extensionUber.doMagic()NoAdd class-like behavior from outside the class

Instance extension vs companion extension

class Uber {
    companion object
}

fun Uber.doMagic() = println("instance")
fun Uber.Companion.doMagic() = println("companion")

Usage:

Uber().doMagic() // instance
Uber.doMagic()   // companion

Companion member vs companion extension

Companion member

Defined inside the class:

class Uber {
    companion object {
        fun doMagic() {}
    }
}

Companion extension

Defined outside the class:

class Uber {
    companion object
}

fun Uber.Companion.doMagic() {}

Use a companion member when you control the class source. Use a companion extension when you want to add behavior from outside.

Take Quiz

Cheat Sheet

// Instance extension
fun Uber.doMagic(context: Context) { }
Uber().doMagic(context)

// Top-level utility function
fun doMagic(context: Context) { }
doMagic(context)

// Companion object member
class Uber {
    companion object {
        fun doMagic(context: Context) { }
    }
}
Uber.doMagic(context)

// Companion object extension
class Uber {
    companion object
}
fun Uber.Companion.doMagic(context: Context) { }
Uber.doMagic(context)

Rules to remember

  • fun TypeName.function() extends instances of that type
  • TypeName.function() only works if function belongs to the companion object or a companion extension
  • Kotlin extensions do not modify the original class
  • Kotlin has no direct static keyword for normal class members like Java
  • If no instance is needed, a top-level function may be the simplest choice

Quick decision guide

  • Need object data? → use an instance extension
  • Need class-level access? → use a companion object
  • Cannot edit the class but need ClassName.function()? → use a companion extension
  • Just a helper? → use a top-level function

FAQ

Can Kotlin extension functions be static?

Not in the Java static sense. An extension on Uber applies to Uber instances. For class-level syntax, use a companion object or an extension on the companion object.

How do I call a function like Uber.doMagic() in Kotlin?

Define doMagic inside companion object, or define an extension on Uber.Companion.

What is the closest thing to a static extension method in Kotlin?

An extension function on a companion object:

fun Uber.Companion.doMagic() { }

Do extension functions actually modify the class?

No. They are resolved by the compiler and behave like regular functions with special call syntax.

Should I use a companion object or a top-level function?

Use a companion object when the behavior logically belongs to the type. Use a top-level function when it is just a general helper.

Can I extend a class companion object from another file?

Yes, as long as the class has a companion object.

Why does fun Uber.doMagic() not allow Uber.doMagic()?

Because the receiver is an Uber instance, not the class name. Uber.doMagic() requires a companion-based receiver instead.

Take Quiz

Mini Project

Description

Create a small Kotlin example that demonstrates the difference between an instance extension function and a companion object extension function. This project is useful because many Kotlin developers need to choose between object-level behavior and type-level behavior when designing APIs.

Goal

Build a class where one function is called on an instance and another is called on the class name using a companion object extension.

Requirements

  • Create a class named Uber.
  • Add an instance extension function that prints a message.
  • Add a companion object extension function that prints a different message.
  • Call both functions from main.
  • Make the output clearly show which function was used.
Take Quiz

Keep learning

Related questions

View more