On this page
Java codebases tend to accumulate StringUtils, ViewUtils and DateUtils classes full of static helpers. Kotlin lets you attach those helpers directly to the types they work on, and pass behaviour as easily as data. Used well, this makes Android code read almost like prose.
Extension functions
// Adds a function to String without inheritance or wrappers
fun String.isValidEmail(): Boolean =
android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()
// Extension on a nullable type: safe to call on null
fun String?.orDash(): String = if (isNullOrBlank()) "โ" else this
// Extension property
val Int.dpToPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt()
// Usage reads naturally
if (email.isValidEmail()) submit()
Text(user.nickname.orDash())What they compile to
An extension is a static function whose first parameter is the receiver. "a@b.com".isValidEmail() compiles to roughly ExtensionsKt.isValidEmail("a@b.com"). Consequences:
- Extensions are resolved statically, from the declared type and not the runtime type, so they don't override member functions.
- They can't access private members of the class.
- A member function always wins over an extension with the same signature.
Higher-order functions
A higher-order function takes a function as a parameter or returns one. Function types look like (Int) -> String or suspend () -> Unit.
suspend fun <T> retry(
times: Int = 3,
initialDelayMs: Long = 500,
block: suspend () -> T, // behaviour passed in as a parameter
): T {
var delayMs = initialDelayMs
repeat(times - 1) {
try {
return block()
} catch (e: IOException) {
delay(delayMs)
delayMs *= 2 // exponential backoff
}
}
return block() // last attempt: let the exception propagate
}
// Usage
val articles = retry { api.getArticles() }Lambdas with receivers: the secret behind DSLs
A function type with a receiver, T.() -> Unit, lets the lambda body call the receiver's members as if it were inside the class. That's how apply, buildString, Gradle's android { } block and Compose's RowScope work.
class NotificationSpec {
var title = ""
var body = ""
var channelId = "general"
}
fun Context.notification(build: NotificationSpec.() -> Unit): Notification {
val spec = NotificationSpec().apply(build)
return NotificationCompat.Builder(this, spec.channelId)
.setSmallIcon(R.drawable.ic_stat_notify)
.setContentTitle(spec.title)
.setContentText(spec.body)
.build()
}
// Usage
val n = context.notification {
title = "Order shipped"
body = "Your parcel arrives tomorrow"
channelId = "orders"
}In Compose, Modifier.weight() and Modifier.align() are only available inside RowScope or ColumnScope, because they're extensions defined on those scope receivers. The compiler stops you using weight where it makes no sense.
Custom Compose modifiers as extensions
fun Modifier.cardSurface(): Modifier = this
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceVariant) // โ not allowed: not @ComposableThat version doesn't compile, because MaterialTheme must be read in a composable. Pass the colour in instead:
fun Modifier.cardSurface(color: Color, radius: Dp = 16.dp): Modifier = this
.clip(RoundedCornerShape(radius))
.background(color)
.padding(16.dp)
// Usage: Box(Modifier.cardSurface(MaterialTheme.colorScheme.surfaceVariant)) { ... }inline, noinline and crossinline
Every lambda passed to a normal function can allocate an object. inline copies the function body and the lambda into the call site, so there's no allocation, and return inside the lambda returns from the enclosing function (a non-local return). The standard library's map, filter and let are inline for this reason.
inline fun <T> measured(tag: String, block: () -> T): T {
val start = SystemClock.elapsedRealtime()
return block().also { Log.d(tag, "took ${SystemClock.elapsedRealtime() - start} ms") }
}noinline: a lambda parameter that must be stored or passed on, not inlined.crossinline: a lambda called from another context (for example inside a Runnable), where non-local return isn't allowed.- Inline only small functions that take lambdas. Inlining large bodies bloats bytecode.
Real-world uses
Android KTX
Google's KTX libraries are mostly extensions, such as sharedPreferences.edit { }, view.isVisible and bundleOf(), that make platform APIs idiomatic.
Mapping layers
fun UserDto.toDomain() keeps conversions next to the types without polluting the models.
Event handlers in Compose
Composables accept onClick: () -> Unit, making them stateless and reusable.
When not to use them
- Extensions on very general types (
Any,String) for domain-specific logic.String.toInvoiceNumber()pollutes autocomplete everywhere. Prefer a function on a domain type. - Hiding important behaviour in extensions far from the class, so readers can't find it.
- Deeply nested lambdas that obscure control flow.
- Unstable lambdas in Compose: creating new lambda instances that capture changing values can affect skipping. Usually fine, but measure on hot paths.
Frequently asked questions
Can extensions be private or scoped?
Yes. Make them private to a file, internal to a module, or declare them inside a class so they're only available there.
Are extension functions slower?
No. They compile to ordinary static calls.
What's the difference between a lambda and an anonymous function?
They're nearly equivalent. Anonymous functions (fun(x: Int): Int { โฆ }) allow explicit return types and local return semantics.
Conclusion
Extensions add behaviour to existing types as static functions. Higher-order functions pass behaviour as values. Lambdas with receivers combine both into DSLs like Compose and Gradle. Use them to make APIs read naturally, scope them tightly, and inline small lambda-taking helpers.
Next step: write type-safe reusable code with Kotlin Generics and Reified Types.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Kotlin fluency makes everything else in Android easier, from coroutines and Flow to Compose and clean architecture. The fastest way to build that fluency is to write real Kotlin every day and get feedback on it.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.