Welcome back! So far you have used functions the way most beginners do: you write one, you call it, it does its job. In this lesson we change how you think about functions. In Kotlin a function is not only something you call. It is also a value, a thing you can keep in a variable, hand to another function, or get back as a result. That one idea powers almost everything you will write in Jetpack Compose. Every Button(onClick = { ... }) you will ever type is a function being passed as a value.
Think of a market trader who sells phone credit. She does not only know how to sell credit. Sometimes she hands a customer a voucher: a small slip that says "when you reach the counter, do this". The voucher is not the action itself. It is the action written down so that it can be carried somewhere else and carried out later. A lambda is that voucher.
By the end of this lesson you will be able to:
- Write a function type such as
(Int, Int) -> Intand read it aloud. - Store a function in a variable and call it.
- Write lambda expressions, including the shortcut
itfor a single parameter. - Use trailing lambda syntax, the shape you see in nearly all Compose code.
- Write your own higher-order functions that accept or return functions.
- Pass an existing function by reference with
::. - Explain what a closure is and why lambdas can change outside variables.
- Declare nullable function types and call them safely.
- Solve a set of practice problems in the style of Google's Practice: Kotlin Fundamentals.
How this fits the bigger picture: This lesson matches the Use function types and lambda expressions in Kotlin codelab and the Practice: Kotlin Fundamentals problem set in Unit 2, Pathway 1 of Google's official Android Basics with Compose course. We take the ideas more slowly, add more examples and write our own practice problems. The official pathway link is in the resources at the end.
Why this matters for Android
Open any Compose code in the next lessons and you will see things like this:
Button(onClick = { println("tapped") }) {
Text("Tap me")
}
Two different lambdas are hiding in those four lines. If lambdas feel strange, Compose will feel like magic. If lambdas feel natural, Compose will feel like plain English. Spend the time here.
You should be comfortable with the following. If any item feels shaky, revisit the lesson shown.
1. Functions, parameters and return types (lesson 08)
A function has a name, optional parameters and an optional return type:
fun add(a: Int, b: Int): Int {
return a + b
}
A single-expression function is the short form: fun add(a: Int, b: Int) = a + b. We will use both styles.
2. Unit: the "nothing useful" return type (lesson 08)
A function that does not return a useful value actually returns Unit. When you do not write a return type, Kotlin assumes Unit. You will see Unit inside function types in this lesson.
3. Variables, val and var (lesson 07)
val creates a label that cannot be pointed at something else. var creates one that can. Lambdas can capture both kinds, and we will see what that means later.
4. Conditionals with if and when (lesson 09)
You will use if as an expression and when with ranges in the practice set at the end. Both are covered in lesson 09.
5. Classes and objects (lesson 11)
The practice set contains two small classes. You need to know class, val properties and open/override.
The core idea
Look at these two lines:
val age = 24 // a value of type Int
val greet = { println("Hello!") } // a value of type () -> Unit
The first stores a number. The second stores an action. Notice that println("Hello!") did not run when greet was created. It is only written down. It runs when you ask for it:
greet() // now it prints Hello!
Calling a function-valued variable looks exactly like calling an ordinary function: name, then parentheses.
How to read a function type
A function type describes the shape of a function: what goes in and what comes out. The pattern is:
(parameter types) -> return type
| Function type | Read it as | Example function |
|---|---|---|
() -> Unit |
takes nothing, returns nothing useful | prints a greeting |
(Int) -> Int |
takes an Int, returns an Int | doubles a number |
(Int, Int) -> Int |
takes two Ints, returns an Int | adds two numbers |
(String) -> Boolean |
takes a String, returns true or false | checks if a name is long |
(Double) -> Double |
takes a Double, returns a Double | converts a temperature |
The arrow -> means "becomes". Two functions with the same type are interchangeable, the way two electric plugs of the same shape both fit the same socket. That is the whole reason function types exist: they let one piece of code say "I will accept any behaviour of this shape".
The three ways to produce a function value
1. A lambda expression { a: Int, b: Int -> a + b }
2. A function reference ::add
3. An anonymous function fun(a: Int, b: Int): Int = a + b (rare, you can skip it)
We will spend our time on the first two.
Anatomy of a lambda
{ a: Int, b: Int -> a + b }
| \_________/ | \_____/ |
| parameters | body |
| arrow closing brace
opening brace
Key rules:
- A lambda lives inside curly braces
{ }. - Parameters come first, separated by commas, then an arrow
->. - The last expression in the body is the lambda's result. There is no
returnkeyword. - If there are no parameters, you skip the arrow:
{ println("Hi") }.
Analogy: A normal function is a restaurant with a name over the door. A lambda is a street vendor with no sign: you do not need a name when you only use it once, right here.
Think about it: If
{ a: Int, b: Int -> a + b }has the type(Int, Int) -> Int, what do you think the type of{ println("Hi") }is? (It takes nothing and its last expressionprintlnreturnsUnit, so the type is() -> Unit.)
Run these in the Kotlin Playground at play.kotlinlang.org or in a scratch file in Android Studio (see lesson 06 for how).
Example 1: Storing a lambda in a variable
Input (code):
fun main() {
val greet: () -> Unit = { println("Welcome to Techbase!") }
greet()
greet()
}
Expected output:
Welcome to Techbase!
Welcome to Techbase!
Explanation: The variable greet has the declared type () -> Unit. Writing the lambda did nothing visible. Each greet() ran the stored action once, so we see two lines.
Example 2: Parameters and a result
Input (code):
fun main() {
val add: (Int, Int) -> Int = { a, b -> a + b }
val multiply = { a: Int, b: Int -> a * b }
println(add(3, 4))
println(multiply(3, 4))
}
Expected output:
7
12
Explanation: There are two ways to tell Kotlin the parameter types. In add, the variable has a declared type, so the lambda can leave out Int on a and b because Kotlin already knows them. In multiply, the variable has no declared type, so the lambda must name the parameter types itself. You need the type in one of the two places, never neither.
Example 3: The shortcut it
When a lambda has exactly one parameter, you may leave out the parameter list and arrow and call the parameter it.
Input (code):
fun main() {
val double: (Int) -> Int = { it * 2 }
val isLong: (String) -> Boolean = { it.length > 5 }
println(double(21))
println(isLong("Ibadan"))
println(isLong("Lagos"))
}
Expected output:
42
true
false
Explanation: "Ibadan" has six characters, which is more than five, so true. "Lagos" has five, which is not more than five, so false. Use it for short lambdas. If the body is long or confusing, give the parameter a proper name instead ({ name -> ... }) so the reader knows what it is.
Example 4: A higher-order function
A higher-order function is a function that takes another function as a parameter, or returns one. Here is one that runs any "operation" on two numbers:
Input (code):
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
fun main() {
val sum = calculate(10, 5, { x, y -> x + y })
val difference = calculate(10, 5, { x, y -> x - y })
println("Sum: $sum")
println("Difference: $difference")
}
Expected output:
Sum: 15
Difference: 5
Explanation: calculate does not know whether it will add, subtract or do something else. The caller supplies the behaviour. This is the voucher idea: calculate is the counter, and each lambda is a different voucher presented there.
Example 5: Trailing lambda syntax
Kotlin has a rule that makes lambdas look much cleaner: if the last parameter of a function is a function, you may write the lambda outside the parentheses.
Input (code):
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
fun main() {
val product = calculate(6, 7) { x, y -> x * y }
println("Product: $product")
repeat(3) { round ->
println("Round ${round + 1}")
}
}
Expected output:
Product: 42
Round 1
Round 2
Round 3
Explanation: calculate(6, 7) { x, y -> x * y } is exactly the same call as calculate(6, 7, { x, y -> x * y }). The second form is the one you will see in all real code. repeat is a standard Kotlin function whose last parameter is a function; it runs that function the given number of times and passes in the counter, which starts at zero. If a function has only a lambda parameter, you may drop the parentheses completely: run { println("Hi") }.
Example 6: Function references with ::
If a normal named function already has the right shape, you do not need to wrap it in a lambda. Put :: in front of its name.
Input (code):
fun isEven(n: Int): Boolean = n % 2 == 0
fun countMatching(from: Int, to: Int, test: (Int) -> Boolean): Int {
var count = 0
for (n in from..to) {
if (test(n)) count++
}
return count
}
fun main() {
println(countMatching(1, 10, ::isEven))
println(countMatching(1, 10) { it % 3 == 0 })
}
Expected output:
5
3
Explanation: The numbers from 1 to 10 that are even are 2, 4, 6, 8 and 10, so five. The numbers divisible by 3 are 3, 6 and 9, so three. The first call passes a function reference, the second passes a lambda, and countMatching treats them identically.
Example 7: A function that returns a function
Input (code):
fun makeMultiplier(factor: Int): (Int) -> Int {
return { number -> number * factor }
}
fun main() {
val triple = makeMultiplier(3)
val tenTimes = makeMultiplier(10)
println(triple(5))
println(tenTimes(5))
}
Expected output:
15
50
Explanation: makeMultiplier(3) hands back a brand-new function that remembers factor is 3. Think of a machine that builds custom calculators on demand.
Example 8: Closures: lambdas remember and change outside variables
A lambda can use variables from the code around it, even after that code has finished. This is called a closure.
Input (code):
fun makeCounter(): () -> Int {
var count = 0
return {
count++
count
}
}
fun main() {
val counterA = makeCounter()
val counterB = makeCounter()
println(counterA())
println(counterA())
println(counterA())
println(counterB())
}
Expected output:
1
2
3
1
Explanation: Each call to makeCounter() creates its own count variable and a lambda that carries it along. counterA and counterB therefore count separately. The lambda's last expression is count, so that is what it returns. This idea matters in Compose: a click lambda often reads or changes a variable that lives outside it (lesson 17).
Example 9: Nullable function types
Sometimes a function parameter is optional, such as "what to do when the user taps, if anything". Wrap the type in parentheses and add ?:
Input (code):
fun downloadFile(name: String, onFinished: (() -> Unit)? = null) {
println("Downloading $name...")
onFinished?.invoke()
}
fun main() {
downloadFile("notes.pdf")
downloadFile("photo.jpg") { println("Saved to gallery!") }
}
Expected output:
Downloading notes.pdf...
Downloading photo.jpg...
Saved to gallery!
Explanation: The parentheses matter. (() -> Unit)? means "maybe a function". Without the outer parentheses, () -> Unit? would mean "a function that returns a nullable Unit", which is something else. To call a nullable function use ?.invoke(): it calls the function if there is one and does nothing if there is not. The default value null lets callers skip the argument entirely.
Example 10: Returning from a lambda
A lambda has no return of its own. Its last expression is its result. If you need to leave early, use a labelled return, naming the function the lambda was passed to:
Input (code):
fun main() {
repeat(5) { n ->
if (n == 2) return@repeat // skips the rest of THIS round only
println("n is $n")
}
}
Expected output:
n is 0
n is 1
n is 3
n is 4
Explanation: return@repeat ends only the current run of the lambda, a bit like continue in a loop. A bare return here would be a different thing (it would leave the whole main), so prefer the labelled form inside lambdas.
You have not built a Compose screen yet (lessons 15 to 17 do that), but you can already read this, and that is worth a moment:
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun SaveButton(onSave: () -> Unit) {
Button(onClick = onSave) {
Text("Save")
}
}
Read it like this:
onSave: () -> Unitis a parameter whose type is a function that takes nothing and returns nothing. The caller decides what saving means.Button(onClick = onSave)hands that voucher to the button. The button does not know what saving is. It only knows to run the voucher when tapped.- The block
{ Text("Save") }after the parentheses is a trailing lambda too. It describes the button's content.
Notice the naming habit: parameters that receive behaviour are usually named onSomething (onClick, onSave, onFinished). Follow it and your code will look like everyone else's.
Exercise 1: A discount machine
Objective: Write a higher-order function and call it with a lambda.
Steps:
- Write
fun applyDiscount(price: Int, rule: (Int) -> Int): Intthat returnsprice - rule(price). - In
main, call it with a lambda that gives 10 percent off (the rule returns a tenth of the price). - Call it again using trailing lambda syntax with a rule that always takes off a flat 200.
- Print both results with string templates for a price of 3000.
fun applyDiscount(price: Int, rule: (Int) -> Int): Int {
return price - rule(price)
}
fun main() {
val tenPercentOff = applyDiscount(3000, { it / 10 })
val flatOff = applyDiscount(3000) { 200 }
println("After 10% off: ₦$tenPercentOff")
println("After ₦200 off: ₦$flatOff")
}
Expected output:
After 10% off: ₦2700
After ₦200 off: ₦2800
Why it works: 3000 / 10 is 300, so 3000 - 300 = 2700. The second lambda ignores its input and returns 200. A lambda with one parameter that never uses it is allowed.
Exercise 2: Build a function factory
Write fun makeGreeter(greeting: String): (String) -> String that returns a function. The returned function takes a name and gives back the greeting followed by a comma, a space and the name. Create a Yoruba greeter with "Bawo ni" and an English one with "Good morning", and call each with "Ade".
fun makeGreeter(greeting: String): (String) -> String {
return { name -> "$greeting, $name" }
}
fun main() {
val yoruba = makeGreeter("Bawo ni")
val english = makeGreeter("Good morning")
println(yoruba("Ade"))
println(english("Ade"))
}
Expected output:
Bawo ni, Ade
Good morning, Ade
The returned lambda remembers greeting (a closure), so each greeter keeps its own greeting.
Exercise 3: Run a callback only when it exists
Write fun login(user: String, onSuccess: ((String) -> Unit)? = null). It prints Checking user..., then, if a callback was given, calls it with the user's name. Call it once without a callback and once with a lambda that prints Welcome back, followed by the name.
fun login(user: String, onSuccess: ((String) -> Unit)? = null) {
println("Checking $user...")
onSuccess?.invoke(user)
}
fun main() {
login("tunde")
login("amaka") { name -> println("Welcome back, $name") }
}
Expected output:
Checking tunde...
Checking amaka...
Welcome back, amaka
The nullable function type is wrapped in parentheses, and ?.invoke(user) safely does nothing when the callback is null.
In the Playground, write a function fun timeIt(label: String, action: () -> Unit) that prints Starting <label>, runs action(), then prints Finished <label>. Call it with a trailing lambda that prints three lines of your choice. Predict the exact output before you run it.
Google's official pathway closes with a practice set called Practice: Kotlin Fundamentals, where you solve small problems from scratch with no hand-holding. Below is our own set in the same spirit, covering conditionals, when, function types, classes, nullability and inheritance. Try each problem before revealing the solution. Struggling for ten minutes teaches more than reading the answer.
Problem 1: Notification summary
Write fun printNotificationSummary(numberOfMessages: Int). If there are fewer than 100 messages, print You have 12 notifications. (with the real number). Otherwise print Your phone is blowing up! You have 99+ notifications. Call it with 0, 7, 99 and 100.
fun printNotificationSummary(numberOfMessages: Int) {
if (numberOfMessages < 100) {
println("You have $numberOfMessages notifications.")
} else {
println("Your phone is blowing up! You have 99+ notifications.")
}
}
fun main() {
printNotificationSummary(0)
printNotificationSummary(7)
printNotificationSummary(99)
printNotificationSummary(100)
}
Expected output:
You have 0 notifications.
You have 7 notifications.
You have 99 notifications.
Your phone is blowing up! You have 99+ notifications.
The boundary matters: 99 < 100 is true, 100 < 100 is false.
Problem 2: Cinema ticket price
Write fun ticketPrice(age: Int, isMonday: Boolean): Int. Rules: ages 0 to 12 pay 15; ages 13 to 60 pay 30, except on Monday when they pay 25; ages 61 to 100 pay 20; any other age is invalid and returns -1. Use when with ranges. Test with (10, false), (30, true), (30, false), (70, false) and (120, true).
fun ticketPrice(age: Int, isMonday: Boolean): Int {
return when (age) {
in 0..12 -> 15
in 13..60 -> if (isMonday) 25 else 30
in 61..100 -> 20
else -> -1
}
}
fun main() {
println(ticketPrice(10, false))
println(ticketPrice(30, true))
println(ticketPrice(30, false))
println(ticketPrice(70, false))
println(ticketPrice(120, true))
}
Expected output:
15
25
30
20
-1
if is an expression here, so it produces the value for that branch of when.
Problem 3: Temperature converter (function types)
Create two function values: celsiusToFahrenheit of type (Double) -> Double (multiply by 9, divide by 5, add 32) and fahrenheitToCelsius (subtract 32, multiply by 5, divide by 9). Write fun show(label: String, value: Double, converter: (Double) -> Double) that prints the label, an equals sign and the converted value. Convert 0.0 and 100.0 from Celsius, and 50.0 from Fahrenheit.
fun show(label: String, value: Double, converter: (Double) -> Double) {
println("$label $value = ${converter(value)}")
}
fun main() {
val celsiusToFahrenheit: (Double) -> Double = { it * 9 / 5 + 32 }
val fahrenheitToCelsius: (Double) -> Double = { (it - 32) * 5 / 9 }
show("C to F:", 0.0, celsiusToFahrenheit)
show("C to F:", 100.0, celsiusToFahrenheit)
show("F to C:", 50.0, fahrenheitToCelsius)
}
Expected output:
C to F: 0.0 = 32.0
C to F: 100.0 = 212.0
F to C: 50.0 = 10.0
Working it out: 100 * 9 / 5 + 32 = 900 / 5 + 32 = 180 + 32 = 212. And (50 - 32) * 5 / 9 = 90 / 9 = 10. These values divide exactly, so there are no long decimals.
Problem 4: Song class
Create a class Song with properties title, artist, yearPublished and playCount. Add a computed property isPopular that is true when playCount is 1000 or more, and a function description() returning "<title>, performed by <artist>, was released in <year>.". Make a song and print whether it is popular and its description.
class Song(
val title: String,
val artist: String,
val yearPublished: Int,
val playCount: Int
) {
val isPopular: Boolean
get() = playCount >= 1000
fun description(): String {
return "$title, performed by $artist, was released in $yearPublished."
}
}
fun main() {
val song = Song("Ojuelegba", "Wizkid", 2014, 5000)
println("Popular: ${song.isPopular}")
println(song.description())
}
Expected output:
Popular: true
Ojuelegba, performed by Wizkid, was released in 2014.
isPopular has a custom getter, so it is recalculated every time you read it and always matches playCount.
Problem 5: Profile with nullable values
Create a class Person(val name: String, val age: Int, val hobby: String?, val referrer: Person?). Add fun showProfile() that prints the name and age, then either Likes to <hobby>. or Doesn't have a hobby., then either Has a referrer named <name>. or Doesn't have a referrer. Create one person with no referrer and a second whose referrer is the first.
class Person(
val name: String,
val age: Int,
val hobby: String?,
val referrer: Person?
) {
fun showProfile() {
println("Name: $name")
println("Age: $age")
if (hobby != null) {
println("Likes to $hobby.")
} else {
println("Doesn't have a hobby.")
}
if (referrer != null) {
println("Has a referrer named ${referrer.name}.")
} else {
println("Doesn't have a referrer.")
}
}
}
fun main() {
val amaka = Person("Amaka", 28, "football", null)
val tunde = Person("Tunde", 21, null, amaka)
amaka.showProfile()
println("---")
tunde.showProfile()
}
Expected output:
Name: Amaka
Age: 28
Likes to football.
Doesn't have a referrer.
Name: Tunde
Age: 21
Doesn't have a hobby.
Has a referrer named Amaka.
Because hobby and referrer are val properties declared in the same module, Kotlin can smart-cast them to non-null inside the if branches (lesson 10).
Problem 6: Foldable phone (inheritance)
Create open class Phone(var isScreenLightOn: Boolean = false) with open fun switchOn(), fun switchOff() and fun checkPhoneScreenLight() that prints The phone screen's light is on. or ...is off.. Then create class FoldablePhone(var isFolded: Boolean = true) : Phone() that overrides switchOn() so the screen only lights up when the phone is not folded, and has fold() and unfold(). Test: switch on while folded, check, unfold, switch on, check.
open class Phone(var isScreenLightOn: Boolean = false) {
open fun switchOn() {
isScreenLightOn = true
}
fun switchOff() {
isScreenLightOn = false
}
fun checkPhoneScreenLight() {
val state = if (isScreenLightOn) "on" else "off"
println("The phone screen's light is $state.")
}
}
class FoldablePhone(var isFolded: Boolean = true) : Phone() {
override fun switchOn() {
if (!isFolded) {
isScreenLightOn = true
}
}
fun fold() {
isFolded = true
}
fun unfold() {
isFolded = false
}
}
fun main() {
val phone = FoldablePhone()
phone.switchOn()
phone.checkPhoneScreenLight()
phone.unfold()
phone.switchOn()
phone.checkPhoneScreenLight()
}
Expected output:
The phone screen's light is off.
The phone screen's light is on.
A new FoldablePhone starts folded, so the first switchOn() does nothing. After unfold() the override lets the light turn on.
Mistake #1: Forgetting the parentheses when calling a stored function. The wrong code: val greet = { println("Hi") } then greet. The problem: this only mentions the variable, so nothing runs (Android Studio may even warn that the expression is unused). The fix: greet().
Mistake #2: No type anywhere in a lambda. The wrong code: val add = { a, b -> a + b }. The problem: Kotlin cannot work out what a and b are. The fix: give types in the lambda, { a: Int, b: Int -> a + b }, or on the variable, val add: (Int, Int) -> Int = { a, b -> a + b }.
Mistake #3: Missing parentheses around a nullable function type. The wrong code: onDone: () -> Unit? = null. The problem: that is a function that returns Unit?, not an optional function, and null is not a legal default. The fix: onDone: (() -> Unit)? = null.
Mistake #4: Using it when there are two or more parameters. The wrong code: calculate(1, 2) { it + it } for a (Int, Int) -> Int function. The fix: it exists only for a single parameter. Name both: { a, b -> a + b }.
Mistake #5: Writing return inside a lambda. The wrong code: val double: (Int) -> Int = { return it * 2 }. The problem: this is a compile error because a bare return is not allowed there. The fix: just write { it * 2 }, or use a label like return@repeat when leaving early from a lambda passed to a function.
Mistake #6: Expecting a lambda's body to run when it is created. The wrong code: val timer = { println("Started") } and expecting output. The fix: a lambda is a stored action. Nothing runs until you call it.
Mistake #7: A trailing lambda placed before other arguments. The wrong code: calculate { x, y -> x + y }(4, 5). The fix: the lambda goes last: calculate(4, 5) { x, y -> x + y }. Only the final parameter can be moved outside the parentheses.
Answer each question in your head (or on paper), then reveal the answer.
Question 1. What does the type (String, Int) -> Boolean describe?
A function that takes a String and an Int and returns a Boolean.
Question 2. What is a higher-order function?
A function that accepts another function as a parameter, returns a function, or both. Examples in this lesson: calculate, countMatching and makeMultiplier.
Question 3. What is the value of val f = { n: Int -> n + 1 } then f(f(5))?
7. The inner call gives 6, and the outer call adds one more.
Question 4. Rewrite calculate(2, 3, { a, b -> a + b }) using trailing lambda syntax.
calculate(2, 3) { a, b -> a + b }. Because the lambda is the last parameter, it can sit outside the parentheses.
Question 5. Why do we write onClick?.invoke() instead of onClick() when onClick is of type (() -> Unit)?
Because the function might be null. ?.invoke() calls it only when it exists and safely does nothing otherwise. A plain onClick() on a nullable type would not compile.
Go deeper: official resources
- Android Basics with Compose, Unit 2, Pathway 1: Kotlin fundamentals: the official pathway this lesson is based on.
- Kotlin docs: Higher-order functions and lambdas: the complete reference, including function references and closures.
- Kotlin docs: Functions: a refresher on parameters, defaults and named arguments.
- Kotlin Playground: run every example in this lesson in your browser.
Project goal: Mobile money services charge fees in different ways: a flat fee, a percentage, or tiers based on the amount. Build a small Kotlin program that treats each fee rule as a function value and compares them for different transfer amounts. This is a real use of higher-order functions: one printing function, many interchangeable rules.
Requirements:
- Create three function values of type
(Int) -> Int(amount in naira in, fee out): a flat rule that always charges 50; a percent rule that charges 2 percent of the amount; and a tiered rule usingwhen(amounts up to 5000 pay 10, amounts up to 50000 pay 25, anything larger pays 50). - Write
fun printQuote(label: String, amount: Int, rule: (Int) -> Int)that prints<label>: fee ₦<fee>, total ₦<amount + fee>. - Print quotes for transfers of 20000 and 100000 using each of the three rules.
- Write
fun capped(limit: Int, rule: (Int) -> Int): (Int) -> Intthat returns a new rule whose fee never exceedslimit. - Print quotes for both amounts using
capped(100, percent). - Add a comment at the top naming the program.
Stretch goal: Add an optional parameter onExpensive: ((Int) -> Unit)? = null to printQuote that is called with the fee whenever the fee is above 1000. Pass a lambda that prints a warning.
// Fee Calculator: comparing transfer fee rules as function values
fun printQuote(
label: String,
amount: Int,
rule: (Int) -> Int,
onExpensive: ((Int) -> Unit)? = null
) {
val fee = rule(amount)
println("$label: fee ₦$fee, total ₦${amount + fee}")
if (fee > 1000) {
onExpensive?.invoke(fee)
}
}
fun capped(limit: Int, rule: (Int) -> Int): (Int) -> Int {
return { amount -> minOf(limit, rule(amount)) }
}
fun main() {
val flat: (Int) -> Int = { 50 }
val percent: (Int) -> Int = { amount -> amount * 2 / 100 }
val tiered: (Int) -> Int = { amount ->
when {
amount <= 5000 -> 10
amount <= 50000 -> 25
else -> 50
}
}
val warn: (Int) -> Unit = { fee -> println(" Warning: a fee of ₦$fee is high!") }
for (amount in listOf(20000, 100000)) {
println("Transfer of ₦$amount")
printQuote("Flat", amount, flat)
printQuote("Percent", amount, percent, warn)
printQuote("Tiered", amount, tiered)
printQuote("Capped percent", amount, capped(100, percent))
}
}
Expected output:
Transfer of ₦20000
Flat: fee ₦50, total ₦20050
Percent: fee ₦400, total ₦20400
Tiered: fee ₦25, total ₦20025
Capped percent: fee ₦100, total ₦20100
Transfer of ₦100000
Flat: fee ₦50, total ₦100050
Percent: fee ₦2000, total ₦102000
Warning: a fee of ₦2000 is high!
Tiered: fee ₦50, total ₦100050
Capped percent: fee ₦100, total ₦100100
How it works: 20000 * 2 / 100 is 400, which is below the warning threshold of 1000, so no warning appears for the first transfer. For 100000 the percent fee is 2000, so the warning fires after that quote. The for loop over listOf(...) is a small preview of collections, which lesson 14 covers properly. capped returns a brand-new rule that remembers limit and the original rule, a closure at work.
Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.
Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 12 project”.
On github.com click New repository, name it android-lesson-12-function-types-and-lambda-expressions, set it to Public, leave Add a README unticked, and click Create repository.
Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.
Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉
Lesson 12 complete! 🎉
You covered:
- Lesson Introduction
- Prerequisite Concepts
- Conceptual Understanding: Functions as Values
- Worked Examples: Function Types and Lambdas
- Reading Lambdas in Real Compose Code
- Guided Practice Exercises
- Practice: Kotlin Fundamentals
- Common Beginner Mistakes (and How to Fix Them)
- Knowledge Check