Extension functions and scope functions
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:
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:
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 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
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:
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 :
fun Int.isEven(): Boolean = this % 2 == 0After 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.
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.
| Function | Inside, the object is | Hands back |
|---|---|---|
let | it | whatever your block produces |
run | this | whatever your block produces |
with | this | whatever your block produces |
apply | this | the object itself |
also | it | the object itself |
That table is the entire feature. Two of the five are worth learning properly today.
apply — set several things up
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:
1val r = Robot()
2r.name = "Bolt"
3r.power = 9Same 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
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:
val len = nick?.let { it.length } ?: 0The 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.
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.
- Open Pocket Studio, tap Projects, and open your Kotlin practice project.
- Tap Editor and open
Playground.kt. - Select everything in the file and delete it.
- Type the program from the walkthrough above.
- Tap Run and read the Output panel.
You should see exactly this — three lines, because the let block is skipped:
1HELLO!
2BOLT AT 9!
3done- Change line 17 to
val nick: String? = "psst"and run again. A fourth line appears,PSST!, between BOLT and done. Same code, different data, noifanywhere. - Now change
applyon line 12 toalsoand run. It fails: Unresolved reference: name. Insidealsothe robot is calledit, notthis, so the bare property names stop working. - Put
applyback and run once more to confirm you are back to the three original lines.
main — so it only exists in there.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.: String — but the body in braces never returns anything.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.it rather than this — also or let.apply, which gives you this and lets you write bare names, or keep also and write it.name = "Bolt".also and apply hand back the original object; let and run hand back whatever the block produced.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.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.nick?.let { ... }. That pairing is the entire reason let is so common in Android code.- 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 (
itorthis) 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,launchanddelaylet your app wait for something without going numb.