Skip to main content

Lambdas

A lambda is an unnamed function that can be stored in a variable, passed to another function, or returned as a result.

val double: (Int) -> Int = { value -> value * 2 }
println(double(4)) // 8

(Int) -> Int is the function type: it receives one Int and returns one Int.

Read it from left to right:

  1. (Int) — the lambda accepts one Int.
  2. -> — separates the inputs from the result.
  3. The final Int — the lambda returns an Int.
  4. { value -> value * 2 } — value is the input; the final expression is returned.
Open in Kotlin Playground
Expected: 8, followed by -6.

Reading lambda syntax​

val label: (Int, String) -> String = { id, name ->
"$id: $name"
}
  • Parameters appear before ->.
  • The body appears after ->.
  • The final expression becomes the returned value.
  • The expected function type helps Kotlin infer parameter types.

The implicit it parameter​

For a one-parameter lambda, Kotlin can name the parameter it automatically:

val lengths = listOf("Kotlin", "Java").map { it.length }

Use it only when the meaning is obvious. In nested or longer lambdas, an explicit name is easier to read:

users.filter { user -> user.isActive }

Use it for a tiny, obvious lambda. Name the parameter when the lambda is nested, has several lines, or represents an important business object.

Open in Kotlin Playground
Expected: [Rakesh].

Trailing lambda syntax​

If the last argument is a function, the lambda can move outside the parentheses:

repeat(3) { index ->
println(index)
}

If it is the only argument, the parentheses can disappear. This syntax is common in Compose:

Button(onClick = { viewModel.save() }) {
Text("Save")
}

Capturing outside values​

var total = 0
val add: (Int) -> Unit = { value -> total += value }

add(5)
println(total) // 5

The lambda captures total, meaning it keeps access to a value declared outside its body.

It captures access to the variable, not a frozen copy of its old value. Every invocation reads and updates the current total.

Open in Kotlin Playground
Expected: 8.
Capture carefully

A lambda that outlives an Activity, Fragment, View, or large object can keep that object reachable longer than expected. Lifecycle ownership matters more than the lambda syntax itself.

Callable references​

When an existing function already has the required shape, pass a reference instead of wrapping it:

fun normalize(value: String) = value.trim().lowercase()

val result = listOf(" A ", " B ").map(::normalize)

Member references are also possible:

val lengths = listOf("A", "Android").map(String::length)

::normalize is a reference to the existing function. In this example, it replaces { value -> normalize(value) } without changing the behavior.

Open in Kotlin Playground
Expected: [kotlin, android], followed by [1, 7].

Lambdas with receivers​

StringBuilder.() -> Unit means the lambda runs with a StringBuilder as its receiver, so its members can be called without a variable name.

fun buildMessage(block: StringBuilder.() -> Unit): String =
StringBuilder().apply(block).toString()

val message = buildMessage {
append("Hello")
append(" Android")
}

This style powers builders and Kotlin DSLs, including many Compose APIs. Use it when the receiver provides a clear, focused vocabulary.

Execution of buildMessage:

  1. StringBuilder() creates one mutable builder.
  2. apply(block) runs the caller's lambda with that builder as this.
  3. append calls update the same builder.
  4. toString() creates the final immutable string.
Open in Kotlin Playground
Expected: Result: 8.

Corner cases​

Nullable lambda versus lambda returning null​

val optionalAction: (() -> Unit)? = null
val nullableResult: () -> String? = { null }
  • optionalAction may contain no function at all.
  • nullableResult always contains a function, but calling it may return null.
Open in Kotlin Playground
Expected: action exists = false, followed by result = null.

Android-only example​

The Button { ... } snippet above requires the Compose compiler and Android/Compose libraries, so it cannot run in plain Kotlin Playground. The standalone lambda behavior is demonstrated by the surrounding Playground examples.

Common mistakes​

1. Too many it values hide the meaning​

✕ Hard to read

users.filter { it.orders.any { it.total > 1_000 } }

✓ Clear names

users.filter { user ->
user.orders.any { order -> order.total > 1_000 }
}

Both compile. The second version makes it obvious which object owns orders and which object has total.

2. Capturing a Fragment in long-running work​

✕ Avoid

repository.loadUser { user ->
binding.name.text = user.name // May run after the Fragment view is destroyed
}

✓ Prefer lifecycle-owned collection

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state -> render(state) }
}
}

The problem is not “lambdas cause leaks.” The problem is that the callback may outlive the View it captured. Give long-running work a lifecycle owner.

3. Capturing mutable loop state​

✕ Surprising result

var selectedId = 1L
val onClick = { println(selectedId) }
selectedId = 2L
onClick() // Prints 2, not 1

✓ Capture the intended value

val selectedId = 1L
val onClick = { println(selectedId) }

A lambda captures access to the variable, not necessarily a frozen copy of its old value.

Interview-ready answer

A lambda is an anonymous function value. Its type describes its parameter and return types, such as (Int) -> String. Lambdas can capture outside values, use trailing-lambda syntax, and run with a receiver. Captures are convenient, but lifecycle and concurrency ownership must still be handled correctly.