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:
fun String.isEmail(): Boolean = contains("@") && substringAfter("@").contains(".")
"ada@example.com".isEmail() // true- Inside the function,
thisis the receiver: the string the function was called on. It can usually be left out, as withcontainsabove. - 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:
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:
fun String?.orDash(): String = if (isNullOrBlank()) "-" else thisExtensions 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:
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:
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 // 25Kotlin Extensions Summary
fun Type.name()adds a function to an existing type;thisis 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.
Useful String extensions
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)
}true
false
ALB
4Line-by-line explanation
- Lines 1 to 4Inside the extension, indexOf and endsWith are called on the receiver string.
- Lines 6 to 7joinToString with a block turns each word into its capitalised first letter.
- Lines 9 to 10An extension property, calculated each time it's read.
- Line 16Extra spaces don't matter, because blank pieces aren't counted.
Number extensions and infix
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))
}£19.99
25
3.14Line-by-line explanation
- Line 1Storing money in whole pence and formatting with an extension avoids rounding errors.
- Line 3infix allows 45 percentOf 180 without a dot or brackets.
- Lines 5 to 9this is the Double the function was called on.
Nullable and generic extensions
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())
}Ana: 0123 456
Ben: -
Cy: -
silver
nullLine-by-line explanation
- 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.
- Line 3<T> makes the extension work for lists of any type.
- Line 9No safe call is needed, because the extension handles null itself.
Common Kotlin Extensions Mistakes
Expecting an extension to store data
var String.tag: String = "" // error: extension properties can't have initializersval String.tag: String get() = "#" + lowercase().replace(" ", "")Expecting override behaviour
open class Shape
class Circle : Shape()
fun Shape.name() = "shape"
fun Circle.name() = "circle"
val s: Shape = Circle()
println(s.name()) // shape, not circleopen 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
fun String.saveToDatabase() { ... } // every string now appears to have thisfun 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.
Complete the extension so it can be called as "hi".shout().
fun .shout() = uppercase() + "!"
What does this print?
fun Int.isEven() = this % 2 == 0
println(7.isEven())
What does this print?
val String.lastChar: Char get() = this[length - 1]
println("Kotlin".lastChar)
Can an extension function read a class's private properties?
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.
What is the receiver in
fun String.isEmail()?- isEmail
- The return value
- The parameter list
- String: the type the function is called on
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
What does
infixallow?- 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
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
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