Kotlin Extension Functions

Add new functions and properties to existing classes, even ones you didn't write, with extension functions, and learn how they're resolved and where to put them.

Lesson 19 of 43Intermediate18 min read

Imagine you often need to check whether a string looks like an email, or format a number of pence as pounds. You could write helper functions like isEmail(text) and formatPence(amount). Kotlin offers something more natural: extension functions, which let you call text.isEmail() and amount.asPounds() as if those functions were part of String and Int all along.

An extension adds a function to a type without changing its source code and without inheritance. That works even for classes you can't modify, such as String from the standard library or classes from other libraries. In fact, much of Kotlin's standard library, including filter, map, trim and isBlank, is written as extension functions.

In this lesson you'll write extension functions and properties, handle nullable receivers, create generic extensions, write readable infix functions, and understand the one rule that surprises people about how extensions are chosen.

Writing an extension function

Put the type you're extending, called the receiver, before the function name:

Kotlin
fun String.isEmail(): Boolean = contains("@") && substringAfter("@").contains(".")

"ada@example.com".isEmail()     // true
  • Inside the function, this is the receiver: the string the function was called on. It can usually be left out, as with contains above.
  • Extensions can take parameters and return values like any function: fun Int.timesOf(s: String) = s.repeat(this).
  • Extensions can only use the receiver's public members; they don't get special access to private ones.

Extension properties

You can add properties too, as long as they're calculated with a getter, because extensions can't store data:

Kotlin
val String.wordCount: Int
    get() = trim().split(Regex("\\s+")).filter { it.isNotEmpty() }.size

val Int.pence: String
    get() = "£%.2f".format(this / 100.0)

Read them like normal properties: "Hello Kotlin world".wordCount is 3, and 1999.pence is "£19.99".

Nullable receivers, generics and infix

An extension on a nullable type can be called on null values, and handles the null case inside. The standard library's isNullOrBlank() works this way:

Kotlin
fun String?.orDash(): String = if (isNullOrBlank()) "-" else this

Extensions can be generic, working with any type: fun <T> List<T>.secondOrNull(): T? = getOrNull(1).

A one-parameter extension or member marked infix can be called without a dot or brackets, which makes some code read like prose. The standard to used in maps is an infix function: "tea" to 2.5.

Extensions are resolved statically

Which extension runs is decided by the declared type of the variable at compile time, not the actual object at run time. If Shape and Circle both have an extension fun X.label(), then for val s: Shape = Circle(), s.label() calls the Shape version. That differs from overridden member functions, which follow the real object.

Also, if a class already has a member function with the same signature, the member always wins over the extension.

What an extension really is underneath

An extension doesn't change the class at all. The compiler turns it into an ordinary static function that takes the receiver as its first parameter. If isEmail is declared in StringExtensions.kt, Java code sees it as StringExtensionsKt.isEmail(text). The dot syntax in Kotlin is just a friendlier way to call it.

That explains the rules you've already met: an extension can't store data, because there's nowhere in the object to put it; it can't see private members, because it lives outside the class; and the declared type decides which one runs, because it's chosen at compile time like any function call.

Because extensions are functions, you can pass them around by reference too:

Kotlin
val emails = listOf("a@b.co", "bad").filter(String::isEmail)   // [a@b.co]

Extensions declared in another package need importing, just like functions: import com.example.util.isEmail. Your IDE usually offers the import when you type the name.

You can even extend a companion object, which makes the new function look like a factory on the class itself. The class needs a companion to extend, even an empty one:

Kotlin
class User(val name: String) {
    companion object
}

fun User.Companion.guest() = User("Guest")

val visitor = User.guest()    // visitor.name is "Guest"

This is handy for adding factory functions to a class that lives in a different part of the codebase, without touching its file.

Kotlin Extensions Syntax

Extension forms

fun String.shout() = uppercase() + "!"
val String.firstWord get() = substringBefore(" ")
fun String?.orEmptyText() = this ?: ""
fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
infix fun Int.percentOf(total: Int) = this * 100 / total
30 percentOf 120     // 25

Kotlin Extensions Summary

  • fun Type.name() adds a function to an existing type; this is the receiver.
  • Extension properties use a getter and can't store state.
  • Extensions on nullable types (String?) can handle null themselves.
  • Generic extensions work on many types; infix functions can be called without dot and brackets.
  • Extensions are chosen by declared type, and members beat extensions with the same signature.

Kotlin Extensions Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Useful String extensions

Kotlin
fun String.isEmail(): Boolean {
    val at = indexOf("@")
    return at > 0 && lastIndexOf(".") > at + 1 && !endsWith(".")
}

fun String.initials(): String =
    split(" ").filter { it.isNotBlank() }.joinToString("") { it.first().uppercase() }

val String.wordCount: Int
    get() = split(Regex("\\s+")).count { it.isNotBlank() }

fun main() {
    println("ada@example.com".isEmail())
    println("ada@example".isEmail())
    println("ada lovelace byron".initials())
    println("  Kotlin   makes this   easy ".wordCount)
}
Output
true
false
ALB
4
Line-by-line explanation
  1. Lines 1 to 4Inside the extension, indexOf and endsWith are called on the receiver string.
  2. Lines 6 to 7joinToString with a block turns each word into its capitalised first letter.
  3. Lines 9 to 10An extension property, calculated each time it's read.
  4. Line 16Extra spaces don't matter, because blank pieces aren't counted.
Example 2

Number extensions and infix

Kotlin
val Int.pounds: String get() = "£%.2f".format(this / 100.0)

infix fun Int.percentOf(total: Int): Int = this * 100 / total

fun Double.roundTo(decimals: Int): Double {
    var factor = 1.0
    repeat(decimals) { factor *= 10 }
    return kotlin.math.round(this * factor) / factor
}

fun main() {
    val pricePence = 1999
    println(pricePence.pounds)
    println(45 percentOf 180)
    println(3.14159.roundTo(2))
}
Output
£19.99
25
3.14
Line-by-line explanation
  1. Line 1Storing money in whole pence and formatting with an extension avoids rounding errors.
  2. Line 3infix allows 45 percentOf 180 without a dot or brackets.
  3. Lines 5 to 9this is the Double the function was called on.
Example 3

Nullable and generic extensions

Kotlin
fun String?.orDash(): String = if (this.isNullOrBlank()) "-" else this

fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null

data class Contact(val name: String, val phone: String?)

fun main() {
    val contacts = listOf(Contact("Ana", "0123 456"), Contact("Ben", null), Contact("Cy", " "))
    for (c in contacts) println("${c.name}: ${c.phone.orDash()}")

    println(listOf("gold", "silver", "bronze").secondOrNull())
    println(listOf(42).secondOrNull())
}
Output
Ana: 0123 456
Ben: -
Cy: -
silver
null
Line-by-line explanation
  1. Line 1The receiver is String?, so orDash can be called even when the phone is null. After the check, this is smart cast to String.
  2. Line 3<T> makes the extension work for lists of any type.
  3. Line 9No safe call is needed, because the extension handles null itself.

Common Kotlin Extensions Mistakes

Expecting an extension to store data

Don't
var String.tag: String = ""     // error: extension properties can't have initializers
Do
val String.tag: String get() = "#" + lowercase().replace(" ", "")

Expecting override behaviour

Don't
open class Shape
class Circle : Shape()
fun Shape.name() = "shape"
fun Circle.name() = "circle"
val s: Shape = Circle()
println(s.name())               // shape, not circle
Do
open class Shape { open fun name() = "shape" }
class Circle : Shape() { override fun name() = "circle" }

Use member functions with override when behaviour should depend on the real object.

Putting unrelated actions on common types

Don't
fun String.saveToDatabase() { ... }     // every string now appears to have this
Do
fun saveName(db: Database, name: String) { ... }

Kotlin Extensions in Real Projects

Extensions in practice

Android's KTX libraries are collections of extensions that make Android APIs friendlier, such as view.isVisible = true and sharedPreferences.edit { putString("k", "v") }. Teams add extensions for formatting dates and money, validating input and converting between data models. Jetpack Compose's Modifier chains (Modifier.padding(8.dp).fillMaxWidth()) are built from extension functions too.

Kotlin Extensions FAQ

Can a Kotlin extension function access private members?

No. An extension is compiled as a separate function outside the class, so it can only use public members, plus internal ones when it's in the same module. If you need private access, the code belongs inside the class as a member function.

What happens if an extension has the same name as a member function?

The member always wins. If a class already has fun format(): String and you write an extension fun Thing.format(): String with the same parameters, calls go to the member and the compiler warns that the extension is shadowed. An extension with different parameter types is fine, because it's a different overload.

Can I add an extension function to a Java class?

Yes. Extensions work on any type Kotlin can see, including Java classes such as java.io.File or java.time.LocalDate, and classes from libraries you can't edit. That's how the Kotlin standard library adds readText() to Java's File.

How do I call a Kotlin extension function from Java?

Call the static method on the class generated for the file, passing the receiver as the first argument: StringExtensionsKt.isEmail("ada@example.com"). To give that class a friendlier name, put @file:JvmName("StringUtils") at the top of the Kotlin file.

When should I use an extension function instead of a regular function?

When the function reads naturally as something the type itself can do, such as price.formatAsMoney() or email.isValidEmail(), and it only needs the receiver's public members. If it mainly coordinates other objects, like saving to a database, a regular function or a method on a service class is clearer.

Kotlin Extensions Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the extension so it can be called as "hi".shout().

fun .shout() = uppercase() + "!"
Predict the outputeasy

What does this print?

fun Int.isEven() = this % 2 == 0
println(7.isEven())
Predict the outputmedium

What does this print?

val String.lastChar: Char get() = this[length - 1]
println("Kotlin".lastChar)
Multiple choicemedium

Can an extension function read a class's private properties?

Multiple choicehard

A class has a member fun greet() and there's also an extension fun MyClass.greet(). Which runs for obj.greet()?

Kotlin Extensions Quiz

5 questions to check what you learned.

  1. What is the receiver in fun String.isEmail()?

    • isEmail
    • The return value
    • The parameter list
    • String: the type the function is called on
  2. Why can't an extension property have an initial value?

    • It's a bug
    • Extensions can't add stored data to a class, so properties must use a getter
    • Properties must be var
    • Only Int properties are allowed
  3. What does infix allow?

    • Calling a function from Java
    • Making a function private
    • Calling a one-parameter function without a dot or brackets, like a to b
    • Overloading operators
  4. Can you call an extension on String? when the value is null?

    • Yes; the extension decides how to handle null
    • No, it always crashes
    • Only with !!
    • Only in main
  5. How is the extension to call chosen?

    • By the actual object at run time
    • Alphabetically
    • Randomly
    • By the variable's declared type at compile time