Skip to content
All posts
May 28, 20264 min read

Kotlin Extension Functions: Patterns That Actually Improve Your Code

Extension functions are Kotlin's most addictive feature. Used well, they make APIs more expressive and reduce boilerplate. Used poorly, they scatter code and hide dependencies. Here are the patterns worth adopting and the anti-patterns worth avoiding.

KotlinAndroidEngineeringPatterns
Share:

Extension functions let you add methods to existing classes without inheritance. They're one of Kotlin's most powerful features — and one of the easiest to abuse.

Here's how to use them well.


When Extension Functions Are the Right Tool

Converting between types (mappers):

kotlin
// Domain model ↔ Entity conversions live as extension functions
fun TaskEntity.toDomain(): Task = Task(
    id = id,
    title = title,
    completed = completed,
    dueDate = dueDate?.let { LocalDate.parse(it) }
)

fun Task.toEntity(): TaskEntity = TaskEntity(
    id = id,
    title = title,
    completed = completed,
    dueDate = dueDate?.toString()
)

Clean, readable, doesn't pollute either class, and can be organized in a mapper file.

Utility functions on standard types:

kotlin
fun String.toSlug(): String = lowercase()
    .replace(Regex("[^a-z0-9]+"), "-")
    .trim('-')

fun Long.toDateString(): String = 
    SimpleDateFormat("MMM d, yyyy", Locale.getDefault()).format(Date(this))

fun Int.dp(context: Context): Int = 
    (this * context.resources.displayMetrics.density).toInt()

// Usage
"My Blog Post Title".toSlug() // → "my-blog-post-title"
task.createdAt.toDateString() // → "Mar 15, 2026"

Fluent builder patterns:

kotlin
fun NotificationCompat.Builder.addDismissAction(context: Context): NotificationCompat.Builder {
    val intent = PendingIntent.getBroadcast(
        context, 0,
        Intent(context, DismissReceiver::class.java),
        PendingIntent.FLAG_IMMUTABLE
    )
    addAction(0, "Dismiss", intent)
    return this
}

// Usage — reads like configuration
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setContentTitle(title)
    .addDismissAction(context) // Extends the builder fluently
    .build()

Context-specific helpers:

kotlin
fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, duration).show()
}

fun Fragment.hideKeyboard() {
    val imm = requireContext().getSystemService(InputMethodManager::class.java)
    imm.hideSoftInputFromWindow(view?.windowToken, 0)
}

Organizing Extension Functions

Don't scatter extension functions randomly. Organize by:

By the type being extended:

code
extensions/
  StringExtensions.kt       // All String extensions
  ContextExtensions.kt      // All Context extensions
  FlowExtensions.kt         // All Flow extensions
  ViewExtensions.kt         // All View extensions

By domain:

code
extensions/
  TaskExtensions.kt         // Task-related extensions across types
  DateExtensions.kt         // Date/time extensions

The key: consistency across the project. Pick one convention and stick to it.


Extension Functions on Nullable Types

Extensions can be defined on nullable receivers:

kotlin
fun String?.orEmpty(): String = this ?: ""
fun String?.isNotNullOrBlank(): Boolean = !isNullOrBlank()

fun <T> List<T>?.orEmpty(): List<T> = this ?: emptyList()

// Usage — no null checks needed
val title = task.title.orEmpty()
val tags = task.tags.orEmpty()

When NOT to Use Extension Functions

When it hides a dependency that should be explicit:

kotlin
// Bad — looks like a simple property but is actually a complex operation
fun Task.toFormattedString(): String {
    return "${title} (${dateFormatter.format(dueDate)})" // Where does dateFormatter come from?
}

// Better — make the dependency explicit
fun Task.toFormattedString(dateFormatter: DateFormatter): String {
    return "${title} (${dateFormatter.format(dueDate)})"
}

When the logic belongs in the class itself:

kotlin
// Bad — business logic masquerading as a utility
fun Task.isHighPriorityAndOverdue(): Boolean = 
    priority == Priority.HIGH && isOverdue

// Better — belongs in the Task domain model
data class Task(...) {
    val isHighPriorityAndOverdue: Boolean get() = 
        priority == Priority.HIGH && isOverdue
}

When it's on a type you control:

If you own the class, add the method directly rather than using an extension. Extensions on your own classes are a code smell — they suggest the method belongs in the class.

When it creates naming confusion:

If

code
String.format()
already exists and you define
code
String.format()
as an extension, Kotlin resolves to the member function. Confusing naming leads to bugs.


Extension Properties

Extensions can also be properties:

kotlin
val Task.displayStatus: String
    get() = if (completed) "Done" else "Active"

val Context.screenWidth: Int
    get() = resources.displayMetrics.widthPixels

val View.isVisible: Boolean
    get() = visibility == View.VISIBLE
    set(value) { visibility = if (value) View.VISIBLE else View.GONE }

Extension properties are read-only by default. Settable extensions (with

code
set
) change state on the receiver, which can be confusing — use with care.


Extension Functions on Companion Objects

Useful for factory methods:

kotlin
data class Task(val id: String, val title: String, val completed: Boolean) {
    companion object
}

fun Task.Companion.empty(): Task = Task(
    id = UUID.randomUUID().toString(),
    title = "",
    completed = false
)

// Usage
val newTask = Task.empty()

Takeaways

  • Extension functions excel at type conversion, utility operations, and fluent builders
  • Organize by either the extended type or the domain — be consistent
  • Don't hide dependencies in extensions — explicit parameters are clearer
  • If you own the class, add the method to the class directly
  • Extension properties with setters change receiver state — use them carefully
  • Business logic belongs in domain classes, not scattered in extension files
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this