Skip to content

Kotlin Extensions — Extension Functions and Properties Guide

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Kotlin Extensions. We cover key concepts, practical examples, and best practices to help you master this topic.

Kotlin extension functions add new methods to existing classes without modifying their source code, using a receiver type declaration that makes the function behave as if it were a member of the extended class.

What You'll Learn

  • Write extension functions for built-in and third-party classes
  • Create extension properties for computed values
  • Understand extension scope and visibility
  • Use extension functions with nullable receivers
  • Declare companion object extensions
  • Apply extensions for domain-specific APIs
  • Understand extension dispatch (static versus dynamic)

Why It Matters

Extensions are one of Kotlin's most powerful features. They let you add utility methods to classes you do not own: String, List, Date, third-party library classes. Extension functions make code more readable by letting you write operations as method calls instead of static utility functions. They are essential for writing idiomatic Kotlin and are used extensively in the standard library, Android KTX, and frameworks like Ktor.

Real-World Use

DodaTech uses extension functions to add logging, JSON Serialization, and validation methods to built-in types. String extensions validate email and phone formats. List extensions provide domain-specific filtering for malware signature lists. Context extensions in Android simplify resource access.

Learning Path

flowchart LR
  A[Sealed Classes] --> B[Extensions\nYou are here]
  B --> C[Generics]
  style B fill:#f90,color:#fff

Extension Functions

An extension function is declared with the receiver type prefixing the function name.

// Extension on String
fun String.isEmail(): Boolean {
    return this.contains("@") && this.contains(".")
}

// Extension on Int
fun Int.isEven(): Boolean = this % 2 == 0

// Extension on List
fun <T> List<T>.secondOrNull(): T? {
    return if (this.size >= 2) this[1] else null
}

fun main() {
    println("test@example.com".isEmail())  // Output: true
    println("hello".isEmail())             // Output: false
    
    println(4.isEven())  // Output: true
    println(5.isEven())  // Output: false
    
    println(listOf(1, 2, 3).secondOrNull())  // Output: 2
    println(listOf(1).secondOrNull())        // Output: null
    println(emptyList<Int>().secondOrNull()) // Output: null
    
    // Chaining extensions
    val result = "Hello, Kotlin!"
        .filter { it.isLetterOrDigit() || it == ' ' }
        .split(" ")
        .map { it.uppercase() }
        .joinToString("-")
    println(result)  // Output: HELLO-KOTLIN
}

Output: Extension functions are called with dot notation on the receiver object.

Extension Properties

Extension properties provide computed values without modifying the original class.

// Extension property on String
val String.wordCount: Int
    get() = this.split("\\s+".toRegex()).size

val String.isNullOrBlank: Boolean
    get() = this.isBlank()

// Extension property on IntRange
val IntRange.halfway: Int
    get() = (this.first + this.last) / 2

// Extension property on collections
val <T> List<T>.middleItem: T?
    get() = if (isEmpty()) null else this[size / 2]

fun main() {
    val text = "Kotlin extension functions are powerful"
    println(text.wordCount)  // Output: 5
    
    println((1..10).halfway)  // Output: 5
    println((0..100).halfway) // Output: 50
    
    println(listOf("a", "b", "c", "d", "e").middleItem)  // Output: c
    println(emptyList<Int>().middleItem)  // Output: null
}

Output: Extension properties behave like computed properties. They cannot have backing fields because they do not modify the class structure.

Nullable Receiver Extensions

Extensions can be defined on nullable types, allowing calls on null references.

// Extension on nullable String
fun String?.isNullOrEmail(): Boolean {
    if (this == null) return false
    return contains("@") && contains(".")
}

// Extension on nullable List
fun <T> List<T>?.nullSafeSize(): Int {
    return this?.size ?: 0
}

fun String?.toSafeLength(): Int {
    return this?.length ?: 0
}

fun main() {
    val valid: String? = "user@example.com"
    val invalid: String? = "not-an-email"
    val nullStr: String? = null
    
    println(valid.isNullOrEmail())    // Output: true
    println(invalid.isNullOrEmail())  // Output: false
    println(nullStr.isNullOrEmail())  // Output: false (no NPE)
    
    println(listOf(1, 2, 3).nullSafeSize())  // Output: 3
    val nullList: List<Int>? = null
    println(nullList.nullSafeSize())          // Output: 0
    
    println(nullStr.toSafeLength())  // Output: 0
}

Output: Nullable receiver extensions handle null gracefully without explicit null checks at the call site.

Companion Object Extensions

You can extend a companion object, adding static-like methods.

class MyClass {
    companion object {
        const val VERSION = "1.0.0"
    }
}

// Extension on companion object
fun MyClass.Companion.createDefault(): MyClass {
    println("Creating default instance (version $VERSION)")
    return MyClass()
}

fun MyClass.Companion.fromConfig(config: Map<String, String>): MyClass {
    println("Creating from config with ${config.size} entries")
    return MyClass()
}

// Extension on any companion
fun <T> T.Companion.safeCall(block: () -> T): T? {
    return try {
        block()
    } catch (e: Exception) {
        println("Error: ${e.message}")
        null
    }
}

fun main() {
    val instance = MyClass.createDefault()
    // Output: Creating default instance (version 1.0.0)
    
    val configured = MyClass.fromConfig(mapOf("key" to "value"))
    // Output: Creating from config with 1 entries
}

Output: Companion object extensions are accessed through the class name, just like regular companion object members.

Extension Scope and Visibility

Extensions are resolved statically, not dynamically. The extension called depends on the declared type of the variable, not its runtime type.

open class Shape
class Circle : Shape()

fun Shape.describe() = "I am a Shape"
fun Circle.describe() = "I am a Circle"

fun main() {
    val shape: Shape = Circle()
    println(shape.describe())  // Output: I am a Shape (static dispatch)
    
    val circle: Circle = Circle()
    println(circle.describe())  // Output: I am a Circle
    
    // Import extensions from other files
    // import com.example.extensions.isEmail
    // "test@test.com".isEmail()  // Available after import
}

Output: Extension resolution is based on the compile-time type. Circle stored in a Shape variable calls Shape's extension.

To import extensions from other packages:

// In file com/example/strings.kt
package com.example.strings
fun String.reverse(): String = this.reversed()

// In another file
import com.example.strings.reverse
// Now "hello".reverse() is available

Member versus Extension Functions

When a class has a member function and an extension function with the same signature, the member wins.

class Greeter {
    fun greet() = println("Hello from member")
}

fun Greeter.greet() = println("Hello from extension")

fun main() {
    val greeter = Greeter()
    greeter.greet()  // Output: Hello from member
}

Output: Member functions always take precedence over extension functions with the same signature. This prevents breaking existing code when a class later adds a member with the same name.

Generic Extension Functions

Extensions can be generic, working across multiple types.

// Generic extension on List
fun <T> List<T>.second(): T {
    if (size < 2) throw NoSuchElementException("List has less than 2 elements")
    return this[1]
}

// Generic extension with type constraint
fun <T : Comparable<T>> List<T>.maxOrFallback(fallback: T): T {
    return this.maxOrNull() ?: fallback
}

// Extension on Map with specific types
fun Map<String, Any?>.toConfigString(): String {
    return this.entries.joinToString("\n") { (key, value) ->
        "$key = $value"
    }
}

fun main() {
    println(listOf("a", "b", "c").second())  // Output: b
    
    val numbers = listOf(3, 7, 1, 9, 4)
    println(numbers.maxOrFallback(0))  // Output: 9
    
    val emptyList = emptyList<Int>()
    println(emptyList.maxOrFallback(-1))  // Output: -1
    
    val config = mapOf("host" to "localhost", "port" to 8080)
    println(config.toConfigString())
    // Output:
    // host = localhost
    // port = 8080
}

Output: Generic extensions work across all types matching the constraints.

Common Mistakes

  1. Shadowing member functions: If a class adds a member with the same name as your extension in a future version, your extension will no longer be called. Test after library updates.

  2. Using extensions for essential functionality: Extensions are external. They cannot access private members. If a feature requires access to private data, submit a Pull Request to the original library instead.

  3. Overusing extensions when utility functions suffice: Not everything needs to be an extension. If a function does not operate on a primary receiver, a top-level function may be more appropriate.

  4. Forgetting that extensions cannot be overridden: Extensions use static dispatch. Subclass-specific behavior cannot be achieved with extensions. Use member functions for polymorphic behavior.

  5. Defining extensions in the same file as the receiver class: Extensions are meant to extend classes you do not own. If you own the class, add the method directly as a member.

  6. Not importing before use: Extensions defined in other packages must be explicitly imported. IDEs typically auto-import, but CLI compilation will fail.

Practice Questions

  1. Can an extension function access private members of the receiver class?

Answer: No. Extension functions have the same access as regular external functions. They cannot access private or protected members.

  1. How is extension function dispatch different from member function dispatch?

Answer: Extension functions use static dispatch (compile-time type). Member functions use dynamic dispatch (runtime type). The declared variable type determines which extension is called.

  1. Can you define an extension property with a backing field?

Answer: No. Extension properties cannot have backing fields because they cannot add state to existing classes. They must be computed properties only.

  1. What happens if a member function has the same signature as an extension function?

Answer: The member function always wins. The extension function is never called for that class.

  1. Challenge: Create a utility library of String extensions for a content management system. Include extensions for slugify, truncate with ellipsis, word count, reading time estimate, and hashtag extraction.

Answer:

fun String.slugify(): String {
    return this.lowercase()
        .replace(Regex("[^a-z0-9\\s-]"), "")
        .replace(Regex("[\\s-]+"), "-")
        .trim('-')
}

fun String.truncate(maxLength: Int, ellipsis: String = "..."): String {
    return if (this.length <= maxLength) this
           else this.take(maxLength - ellipsis.length) + ellipsis
}

val String.wordCount: Int
    get() = this.split(Regex("\\s+")).size

val String.readingTimeMinutes: Int
    get() = (this.wordCount / 200).coerceAtLeast(1)

fun String.extractHashtags(): List<String> {
    return Regex("#\\w+").findAll(this).map { it.value.lowercase() }.toList()
}

fun main() {
    val title = "Kotlin Extension Functions: A Complete Guide!"
    println(title.slugify())       // Output: kotlin-extension-functions-a-complete-guide
    
    val longText = "This is a very long text that needs to be truncated for display in a list view"
    println(longText.truncate(30))  // Output: This is a very long text ...
    
    val article = "Kotlin is a modern language. It runs on JVM. It is fun."
    println(article.wordCount)           // Output: 12
    println("${article.readingTimeMinutes} min read")  // Output: 1 min read
    
    val post = "Loving #Kotlin and #Extensions! #coding #100DaysOfCode"
    println(post.extractHashtags())  // Output: [#kotlin, #extensions, #coding, #100daysofcode]
}

Mini Project

Create a collection of extension functions for a task management application. Requirements:

  • String extensions: isValidTaskTitle, toTaskPriority, formatDueDate
  • Int extensions: daysUntil (for due dates), toDurationString
  • List extensions: sortedByPriority, filterOverdue, groupByStatus
  • Extension properties: taskList.completionPercentage
  • Nullable receiver extensions for safe access to nullable task fields

This project demonstrates extensions across multiple types in a cohesive domain.

FAQ

Are extension functions compiled to static methods?

Yes. Extension functions are compiled to static methods with the receiver as the first parameter. This is why they use static dispatch.

Can I define an extension on a generic type?

Yes. Prefix the function with the type parameter: 'fun List.second(): T'. The type parameter can include constraints.

Can I use extension functions in Java?

Not directly. Extension functions are a Kotlin-only syntax feature. However, they compile to static Java methods that can be called from Java.

How do I organize extension functions in a project?

Group related extensions in files named after the extended class, e.g., StringExtensions.kt, ListExtensions.kt, ContextExtensions.kt.

Can extension functions be used with infix notation?

Yes. If the extension has one parameter, mark it with the infix keyword: 'infix fun String.repeat(n: Int): String'.

What's Next

After mastering extensions, learn generics for writing type-safe reusable code. You can also explore coroutines for asynchronous programming.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro