Pocket Studio Academy
HomePart 11.18

Extension functions and scope functions

Full course10 min read·4 questions

Add your own functions to types you did not write, so they can be called with a dot as if they had always been there — then meet the five little scope functions that appear in every Android codebase.

Adding to something you do not own

Suppose you keep needing shouty text. The obvious move:

Playground.ktkotlin
1fun shout(text: String): String {
2  return text.uppercase() + "!"
3}

Call it with shout("hello") and you get HELLO!. Fine. But now compare how it reads next to Kotlin's own functions:

Playground.ktkotlin
println(shout("hello".trim().lowercase()))

The reading order has gone inside out. trim and lowercase flow left to right; shout wraps around the outside, so your eye has to go out to the front and back again.

You cannot open up Kotlin's String and add a shout to it — it is not your code. What you can do is write a function that behaves exactly as though you had.

Think of it like this

Think of a clip-on macro lens for a phone camera.

You did not open the phone. You did not modify the camera. You clipped something onto the outside — and now the phone takes close-up photos as if that had always been one of its features.

An is a clip-on lens for a type. The original is untouched, and the new ability behaves as though it were built in.

Extension functions

Playground.ktkotlin
1fun String.shout(): String {
2  return uppercase() + "!"
3}

Now "hello".shout() prints HELLO!. The only new thing is String. in front of the function name.

The type in front is the receiver — the thing the function will be called on. Inside the body, refers to it, and as usual you may leave this out, which is why uppercase() on its own works.

Now the reading order is fixed:

Playground.ktkotlin
println("hello".trim().lowercase().shout())

Left to right, one step after another, with yours indistinguishable from Kotlin's.

Extensions work on any type, including your own and including :

Playground.ktkotlin
fun Int.isEven(): Boolean = this % 2 == 0

After that line, 4.isEven() is true and 7.isEven() is false.

What is really happening

No magic, and worth knowing: the turns "hello".shout() into an ordinary function call, shout("hello"). Nothing is added to String — the dot is a convenience, decided at compile time.

Two consequences follow directly:

  • An extension cannot see parts of the type it extends. It is outside code, and stays outside.
  • If the type already has a real function with that name and shape, the real one wins.

You have been using extensions all along without knowing it. filter, map, sumOf and toSet from Lessons 1.11 and 1.12 are all extension functions on collections, living in Kotlin's standard library rather than inside List itself.

Tip

Write extensions at the top level of a file, outside any function. An extension declared inside main only exists inside main, which is almost never what you want and is the usual cause of "Unresolved reference" on your own extension.

The five scope functions

A does one small thing: it hands you a block of code that already has one object in scope, so you can do several things with it without repeating its name.

There are exactly five, and they differ in only two ways: what the object is called inside the block, and what comes back out.

FunctionInside, the object isHands back
letitwhatever your block produces
runthiswhatever your block produces
withthiswhatever your block produces
applythisthe object itself
alsoitthe object itself

That table is the entire feature. Two of the five are worth learning properly today.

apply — set several things up

Playground.ktkotlin
1val r = Robot().apply {
2  name = "Bolt"
3  power = 9
4}

Inside the braces you are "in" the robot, so you can name its properties directly with no r. in front. And because hands the object back, the whole thing is still an expression you can assign. Compare it with the long way:

Playground.ktkotlin
1val r = Robot()
2r.name = "Bolt"
3r.power = 9

Same result. apply is tidier and, more importantly, keeps the setup visibly attached to the thing being set up.

let — do something with a value, especially a nullable one

Playground.ktkotlin
1val nick: String? = null
2nick?.let {
3  println(it.shout())
4}

Read the two symbols together. The means only if this is not null, and means run this block with the value as it. Put them side by side and you get the cleanest way in Kotlin to say "do all of this, but only if there is something there".

With nick = null, the block simply does not run. Nothing is printed and nothing crashes.

let also hands back whatever the block produced, so it can sit in the middle of a chain:

Playground.ktkotlin
val len = nick?.let { it.length } ?: 0

The other three, briefly

also is for a step on the side — logging, checking — because it hands the object straight through unchanged: list.also { println(it.size) }. run and with are let and apply's cousins that use this instead of it; you will meet them in real code and can look them up in the table then.

Careful

Scope functions are addictive, and stacking them makes code worse, not better. If you find yourself writing a.let { b.apply { c.also { ... } } }, stop and use ordinary named values — val costs four characters and reads perfectly.

Reach for apply when configuring one object, and ?.let when guarding against null. Those two cover almost everything.

Try it in Pocket Studio
  1. Open Pocket Studio, tap Projects, and open your Kotlin practice project.
  2. Tap Editor and open Playground.kt.
  3. Select everything in the file and delete it.
  4. Type the program from the walkthrough above.
  5. Tap Run and read the Output panel.

You should see exactly this — three lines, because the let block is skipped:

text
1HELLO!
2BOLT AT 9!
3done
  1. Change line 17 to val nick: String? = "psst" and run again. A fourth line appears, PSST!, between BOLT and done. Same code, different data, no if anywhere.
  2. Now change apply on line 12 to also and run. It fails: Unresolved reference: name. Inside also the robot is called it, not this, so the bare property names stop working.
  3. Put apply back and run once more to confirm you are back to the three original lines.
Error Doctor5 common errors
e: Unresolved reference: shout
MeansKotlin cannot find your extension where you called it. Nine times out of ten it was declared inside another function — usually inside main — so it only exists in there.
FixMove the fun String.shout() declaration to the top level of the file, outside every other function. If it lives in a different file in a different package, add an import for it.
e: A 'return' expression required in a function with a block body ('{...}')
MeansYou promised a result type — : String — but the body in braces never returns anything.
FixAdd return in front of the value, or switch to the short form with an equals sign: fun String.shout() = uppercase() + "!", which returns its expression automatically.
e: Unresolved reference: name
MeansYou used bare property names inside a scope function that provides it rather than thisalso or let.
FixEither switch to apply, which gives you this and lets you write bare names, or keep also and write it.name = "Bolt".
e: Type mismatch: inferred type is String but Int was expected
MeansYou expected the block's result but got the object back, or the other way round. also and apply hand back the original object; let and run hand back whatever the block produced.
FixFor a computed result use let: val n: Int = name.let { it.length }. Check the table in this lesson when the types surprise you — it is always one of those two behaviours.
e: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
MeansYou wrote nick.let { ... } with a plain dot on a nullable value. let is an ordinary function, so calling it on null is no more allowed than anything else.
FixUse the safe call: nick?.let { ... }. That pairing is the entire reason let is so common in Android code.
Recap
  • An is written as fun Type.name() and is called with a dot, as though it belonged to that type. The is available inside as .
  • Nothing is really added to the type — the compiler rewrites the call — so extensions cannot see private members.
  • The five differ only in what the object is called inside (it or this) and what comes back (the block's result, or the object).
  • configures an object and hands it back. with a safe call runs a block only when a value is not null.
  • Do not stack them. Two named values are almost always clearer than three nested blocks.
  • Next: coroutines — why slow work freezes a screen, and how suspend, launch and delay let your app wait for something without going numb.