Welcome to one of the most important lessons in the whole course. Up to now your programs have been one long list of statements inside main. That works for ten lines. It does not work for ten thousand. Real apps are built from many small pieces, each doing one job and each having a name. Those pieces are functions.
Picture a busy kitchen at a wedding. The head cook does not shout out every tiny step each time ("pick up the knife, peel the onion, slice the onion..."). She says "chop onions" and a trained helper knows what to do. A function is that helper: a named recipe you write once and ask for as many times as you like, with different ingredients each time.
By the end of this lesson you will be able to:
- Explain why functions exist and when to create one.
- Write functions with parameters and call them with arguments.
- Return a value, and understand what
Unitmeans when you return nothing. - Use default parameter values and named arguments.
- Write short functions in single-expression form.
- Return more than one value using
Pair. - Understand scope: which variables can be seen where.
- Use local functions and
varargparameters. - Solve a set of practice problems in the style of the official Practice: Kotlin Basics material.
How this fits the bigger picture: This lesson covers the Create and use functions in Kotlin codelab and the Practice: Kotlin Basics problems from Unit 1, Pathway 1 of Android Basics with Compose. In Jetpack Compose every piece of screen you build is itself a function, so everything here carries straight into lesson 15.
1. main is already a function
You have written fun main() { ... } since lesson 06. main is just a function with a special job: it is where Kotlin starts. Today you learn to write your own.
2. Variables and types (lesson 07)
You need val, var, Int, Double, String and Boolean, plus string templates like "Total: ${a + b}". If any of these feel shaky, revisit lesson 07 first.
3. Statements versus expressions
A statement does something (println("hi")). An expression produces a value (3 + 4 produces 7). A great deal of Kotlin is expressions, including function calls that return values. Keep this idea in mind. It makes single-expression functions natural.
4. The idea of "don't repeat yourself"
If you copy and paste the same five lines in three places, and later find a bug, you must fix it in three places. Programmers call this the DRY principle: Don't Repeat Yourself. Functions are the main tool for obeying it.
A function has a name, optional inputs (called parameters), a body of statements, and optionally an output (the return value).
inputs the function output
(arguments) (return value)
+---------+ +---------------------------+ +-------------+
| 1200, 3 | ---> | lineTotal(price, qty) | ---> | 3600 |
+---------+ | multiplies the two | +-------------+
+---------------------------+
Think of a vending machine: you put in money and press a button (inputs), the machine does its work (body), and a drink drops out (output). You do not need to know how the machine works inside. You only need to know what it expects and what it gives back. That separation is what makes big programs manageable.
The anatomy of a function
fun addNumbers(first: Int, second: Int): Int {
val sum = first + second
return sum
}
| Piece | What it is |
|---|---|
fun |
Keyword meaning "define a function". |
addNumbers |
The name. Use camelCase, and start with a verb: calculateTotal, printReceipt, isValid. |
(first: Int, second: Int) |
The parameter list. Each parameter has a name, a colon and a type. Separate parameters with commas. |
: Int |
The return type: what kind of value comes out. |
{ ... } |
The body. |
return sum |
Sends the value back to whoever called the function and ends the function. |
Parameters versus arguments
These two words get mixed up constantly:
- A parameter is the label in the definition:
firstandsecondabove. - An argument is the actual value you pass when calling: in
addNumbers(5, 7)the arguments are5and7.
Inside the function, every parameter behaves like a val. You cannot reassign it. If you need a changed copy, make a new local variable.
Defining versus calling
Writing fun addNumbers... only defines the recipe. Nothing happens until you call it by writing its name with parentheses:
fun addNumbers(first: Int, second: Int): Int {
return first + second
}
fun main() {
val result = addNumbers(5, 7)
println("5 + 7 = $result")
println("Direct call: ${addNumbers(20, 22)}")
}
5 + 7 = 12
Direct call: 42
The order of definition does not matter in Kotlin. main can sit above the functions it calls.
Run each example in the Playground. For each one, predict the output before you press Run.
Example 1: A function with no parameters and no result
fun printWelcome() {
println("Welcome to Techbase Academy")
println("Let's learn Android")
}
fun main() {
printWelcome()
println("---")
printWelcome()
}
Welcome to Techbase Academy
Let's learn Android
Welcome to Techbase Academy
Let's learn Android
We wrote two lines once and used them twice. If we change the greeting, both places update.
Example 2: A function with a parameter
fun greet(name: String) {
println("Hello, $name! Good to see you.")
}
fun main() {
greet("Ada")
greet("Tunde")
greet("Chioma")
}
Hello, Ada! Good to see you.
Hello, Tunde! Good to see you.
Hello, Chioma! Good to see you.
Example 3: Returning a value
The return type goes after the parameter list. If a function declares a return type, every path through it must return a value of that type.
fun lineTotal(price: Int, quantity: Int): Int {
return price * quantity
}
fun main() {
val rice = lineTotal(1200, 3)
val beans = lineTotal(800, 2)
println("Rice: ₦$rice")
println("Beans: ₦$beans")
println("Together: ₦${rice + beans}")
}
Rice: ₦3600
Beans: ₦1600
Together: ₦5200
Notice how a returned value can be stored, printed, or used straight inside another expression.
Example 4: What Unit means
printWelcome and greet did not declare a return type. In Kotlin that means they return Unit, a special type meaning "nothing useful". Writing : Unit is allowed but nobody does it. You can see it if you capture the result:
fun greet(name: String) {
println("Hi $name")
}
fun main() {
val result = greet("Bola")
println(result)
}
Hi Bola
kotlin.Unit
You will almost never do this. The point is that every function returns something, and Unit is the "nothing interesting" something. Other languages call this void.
Example 5: Single-expression functions
When the body is just one expression, you can skip the braces and return and write = instead:
fun lineTotal(price: Int, quantity: Int): Int = price * quantity
fun square(n: Int) = n * n
fun isEven(n: Int) = n % 2 == 0
fun main() {
println(lineTotal(500, 4))
println(square(9))
println(isEven(10))
println(isEven(7))
}
2000
81
true
false
With =, Kotlin can often work out the return type, so you may leave it off (square and isEven above). Keep the explicit type when it helps readers, especially in code others will use. You will see this single-expression style all over Compose projects.
Example 6: Default parameter values
A parameter can have a default value, used when the caller leaves the argument out:
fun calculateTip(amount: Double, tipPercent: Double = 15.0): Double {
return amount * tipPercent / 100
}
fun main() {
println(calculateTip(20000.0)) // uses the default 15.0
println(calculateTip(8000.0)) // default again
println(calculateTip(8000.0, 10.0)) // overrides the default
}
Reasoning: 20000.0 times 15.0 is 300000.0, divided by 100 is 3000.0. Next, 8000.0 times 15.0 is 120000.0 over 100 gives 1200.0. With 10.0, 80000.0 over 100 gives 800.0.
3000.0
1200.0
800.0
Defaults replace the old habit of writing several nearly identical functions. Parameters with defaults are conventionally placed after those without.
Example 7: Named arguments
When a function has several parameters, a bare list like describe("Ada", 19, "Ibadan") is hard to read. Named arguments put the parameter name beside the value. You can even change the order:
fun describe(name: String, age: Int, city: String = "Ibadan"): String {
return "$name is $age and lives in $city"
}
fun main() {
println(describe("Ada", 19))
println(describe(name = "Tunde", age = 24, city = "Lagos"))
println(describe(age = 30, name = "Chioma"))
}
Ada is 19 and lives in Ibadan
Tunde is 24 and lives in Lagos
Chioma is 30 and lives in Ibadan
Named arguments appear everywhere in Jetpack Compose. A line like Text(text = "Hi", fontSize = 24.sp) is just a function call with named arguments. Mastering them now makes lesson 15 feel familiar.
Example 8: Returning more than one value with Pair
A function returns one value, but that value can be a bundle. Pair holds two values, which you can split apart with destructuring:
fun splitMinutes(totalMinutes: Int): Pair<Int, Int> {
val hours = totalMinutes / 60
val minutes = totalMinutes % 60
return Pair(hours, minutes)
}
fun main() {
val (h, m) = splitMinutes(400)
println("$h hours and $m minutes")
val whole = splitMinutes(95)
println("first = ${whole.first}, second = ${whole.second}")
}
6 hours and 40 minutes
first = 1, second = 35
Pair<Int, Int> means "a pair of two Ints". When you need to return many named things, a data class is better (lesson 11).
Example 9: Variable number of arguments with vararg
Sometimes you want to accept any number of values. Mark the parameter vararg:
fun sumAll(vararg numbers: Int): Int {
var total = 0
for (n in numbers) {
total += n
}
return total
}
fun main() {
println(sumAll(1, 2, 3))
println(sumAll(10, 20, 30, 40, 50))
println(sumAll())
}
Inside the function, numbers behaves like a collection of Int. We used a for loop here as a sneak preview. Lesson 09 teaches loops fully. An empty call gives 0 because the loop does nothing.
6
150
0
Scope is the area of code where a name can be used. A variable created inside a function lives only inside that function. When the function ends, the variable disappears.
fun calculateTotal(): Int {
val subtotal = 5000 // local to calculateTotal
return subtotal + 250
}
fun main() {
println(calculateTotal())
// println(subtotal) // ERROR: unresolved reference, subtotal is not visible here
}
5250
Think of each function as a closed room. What happens inside the room stays inside the room, unless it is passed out through return. This is a feature: two functions can both use a variable called total without interfering.
A variable declared at the top level of the file (outside every function) can be seen by all functions in that file. Use that sparingly. Prefer passing values in as parameters and getting results back with return. Functions that depend only on their inputs are far easier to test and understand.
Local functions
You can define a function inside another function. It can only be called from there:
fun printReceiptLine(item: String, price: Int, quantity: Int) {
fun formatNaira(amount: Int) = "₦$amount" // local helper
val total = price * quantity
println("$item x$quantity = ${formatNaira(total)}")
}
fun main() {
printReceiptLine("Rice", 1200, 3)
}
Rice x3 = ₦3600
Local functions are helpful for tiny helpers that nobody else needs.
When one function calls another, the computer remembers where to come back to. Follow this program line by line:
fun double(n: Int) = n * 2
fun addDoubles(a: Int, b: Int): Int {
return double(a) + double(b)
}
fun main() {
println(addDoubles(3, 4))
}
main() starts
calls addDoubles(3, 4)
calls double(3) -> returns 6
calls double(4) -> returns 8
6 + 8 = 14 -> addDoubles returns 14
println prints 14
main() ends
14
Functions calling functions is how large programs are assembled. Each function stays small and has one clear job. A good guideline: if you cannot describe what a function does in one short sentence, it probably does too much.
Functions in an Android app look exactly like this. Here is a tiny preview of what Jetpack Compose will look like in lesson 15, with a function that returns nothing (Unit) and takes a named parameter:
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Composable is an annotation that tells the Compose tooling "this function describes a piece of screen". Composable functions by convention start with a capital letter, an exception to the camelCase rule. Do not worry about the details now. Notice only that it is a normal function with a normal parameter.
To practise pure Kotlin functions in Android Studio, use a scratch file (File > New > Scratch File > Kotlin) or the Kotlin library module from lesson 06. Menu names move a little between versions, so look for the nearest equivalent if yours differs.
The official course ends this pathway with a set of practice problems. Here are original problems at the same level, so you can test yourself. Try each one before opening the answer. Every problem has a complete worked solution.
Problem 1: Rectangle toolkit
Write two single-expression functions, area(width: Int, height: Int) and perimeter(width: Int, height: Int). In main, print the area and perimeter of a rectangle that is 8 by 5.
fun area(width: Int, height: Int) = width * height
fun perimeter(width: Int, height: Int) = 2 * (width + height)
fun main() {
println("Area: ${area(8, 5)}")
println("Perimeter: ${perimeter(8, 5)}")
}
Area: 40
Perimeter: 26
The area is 8 times 5, and the perimeter is twice the sum of the two sides, 2 times 13.
Problem 2: Temperature both ways
Write celsiusToFahrenheit(c: Double): Double and fahrenheitToCelsius(f: Double): Double. Use them to print: 25 degrees Celsius in Fahrenheit, -40 degrees Celsius in Fahrenheit, and 212 degrees Fahrenheit in Celsius.
Formulas: F = C x 9 / 5 + 32 and C = (F - 32) x 5 / 9.
fun celsiusToFahrenheit(c: Double): Double = c * 9 / 5 + 32
fun fahrenheitToCelsius(f: Double): Double = (f - 32) * 5 / 9
fun main() {
println("25 C = ${celsiusToFahrenheit(25.0)} F")
println("-40 C = ${celsiusToFahrenheit(-40.0)} F")
println("212 F = ${fahrenheitToCelsius(212.0)} C")
}
Working: 25.0 times 9 is 225.0, over 5 is 45.0, plus 32 is 77.0. Then -40.0 times 9 is -360.0, over 5 is -72.0, plus 32 is -40.0 (the famous point where both scales agree). Finally 212 minus 32 is 180.0, times 5 is 900.0, over 9 is 100.0.
25 C = 77.0 F
-40 C = -40.0 F
212 F = 100.0 C
Problem 3: Average of three scores
Write average(a: Int, b: Int, c: Int): Double. It must give a decimal answer, not a rounded-down whole number. Print the average of 80, 90 and 85, and of 70, 71 and 72, and of 1, 2 and 2.
fun average(a: Int, b: Int, c: Int): Double = (a + b + c) / 3.0
fun main() {
println(average(80, 90, 85))
println(average(70, 71, 72))
println(average(1, 2, 2))
}
The trick is dividing by 3.0, a Double, so Kotlin does not use integer division. The sums are 255, 213 and 5, so the averages are 85.0, 71.0 and about 1.6666666666666667.
85.0
71.0
1.6666666666666667
Problem 4: A report card line (default and named arguments)
Write reportLine(student: String, subject: String, score: Int, outOf: Int = 100): String that returns text like Ada | Maths | 78/100. Call it once using the default, once with outOf = 50, and once with all arguments named in a different order.
fun reportLine(student: String, subject: String, score: Int, outOf: Int = 100): String {
return "$student | $subject | $score/$outOf"
}
fun main() {
println(reportLine("Ada", "Maths", 78))
println(reportLine("Tunde", "English", 41, 50))
println(reportLine(subject = "Biology", score = 66, student = "Chioma", outOf = 80))
}
Ada | Maths | 78/100
Tunde | English | 41/50
Chioma | Biology | 66/80
Problem 5: Fix the broken functions
Each function below has a mistake. Find and fix it.
fun add(a: Int, b: Int) {
return a + b
}
fun greet(name: String = "friend", age: Int) {
println("Hello $name, you are age")
}
fun main() {
println(add(2))
greet(age = 20)
}
There are three real problems and one style point:
addhas a block body that usesreturn a + b, but no return type is declared, so it returnsUnit. Add: Int.println(add(2))supplies only one of the two required arguments. Pass both:add(2, 3).- The template
"... you are age"is missing the$, so it prints the wordage. Write$age. - Style point:
greetitself is legal: sinceageis given by name, the default fornamecan be used. (Putting a defaulted parameter first is legal, but when you call it positionally you must name the later argument, so most people put defaults last.)
fun add(a: Int, b: Int): Int {
return a + b
}
fun greet(age: Int, name: String = "friend") {
println("Hello $name, you are $age")
}
fun main() {
println(add(2, 3))
greet(age = 20)
}
5
Hello friend, you are 20
Scenario: You are helping at a school canteen and want small reusable functions for prices.
Exercise 1: Canteen helpers
Steps:
- Write
fun totalPrice(unitPrice: Int, count: Int): Intas a single-expression function. - Write
fun applyDiscount(amount: Int, percent: Int): Intthat returns the amount after the percentage discount (use integer arithmetic:amount * (100 - percent) / 100). - Write
fun printLine(label: String, amount: Int)that printslabel: ₦amount. - In
main, buy 5 meat pies at ₦350 each, apply a 10 percent discount, and print both the full price and the final price.
fun totalPrice(unitPrice: Int, count: Int): Int = unitPrice * count
fun applyDiscount(amount: Int, percent: Int): Int = amount * (100 - percent) / 100
fun printLine(label: String, amount: Int) {
println("$label: ₦$amount")
}
fun main() {
val full = totalPrice(350, 5)
val discounted = applyDiscount(full, 10)
printLine("Full price", full)
printLine("After discount", discounted)
}
Working: 350 times 5 is 1750. Then 1750 times 90 is 157500, divided by 100 is 1575.
Full price: ₦1750
After discount: ₦1575
Exercise 2: Returning two values
Write divideWithRemainder(a: Int, b: Int): Pair<Int, Int> and use destructuring to print 17 divided by 5 is 3 remainder 2. Then use the same function to work out how many full buses of 14 passengers are needed for 100 people, and how many are left standing.
fun divideWithRemainder(a: Int, b: Int): Pair<Int, Int> = Pair(a / b, a % b)
fun main() {
val (q, r) = divideWithRemainder(17, 5)
println("17 divided by 5 is $q remainder $r")
val (buses, standing) = divideWithRemainder(100, 14)
println("$buses full buses, $standing people left over")
}
Working: 100 divided by 14 is 7 (7 times 14 is 98), leaving 2.
17 divided by 5 is 3 remainder 2
7 full buses, 2 people left over
Choose three lines you have repeated in earlier lessons' practice (a border line, a total calculation, a greeting). Turn each into a function and call it twice with different arguments. Then write a one-sentence description of each function. If you cannot do it in one sentence, split the function.
Mistake #1: Declaring a return type but forgetting return. The wrong code: fun double(n: Int): Int { n * 2 }. The error: a return is missing, so the compiler complains. The fix: write return n * 2, or use the single-expression form fun double(n: Int) = n * 2.
Mistake #2: Using return in a block body without a return type. The wrong code: fun add(a: Int, b: Int) { return a + b }. The problem: with a block body, no declared return type means Unit. The fix: add : Int. (Only single-expression functions with = can infer the type.)
Mistake #3: Forgetting to call the function. The wrong code: printWelcome without parentheses. The problem: nothing prints, or you get an error. The fix: a call needs parentheses: printWelcome().
Mistake #4: Ignoring the returned value. The wrong code: lineTotal(1200, 3) on a line by itself. The problem: it computes 3600 and throws it away. The fix: store it (val t = lineTotal(1200, 3)) or print it.
Mistake #5: Reassigning a parameter. The wrong code: fun addVat(price: Int): Int { price = price + 75; return price }. The error: Val cannot be reassigned. The fix: val withVat = price + 75 and return that.
Mistake #6: Using a local variable outside its function. The wrong code: creating val total inside calculate() and printing total in main. The error: unresolved reference. The fix: return the value from the function and store it in main.
Mistake #7: Wrong argument order. The wrong code: describe(19, "Ada") for describe(name: String, age: Int). The error: type mismatch. The fix: match the order, or use named arguments, which make order irrelevant and the code self-explaining.
Mistake #8: Functions that do too much. The wrong code: one function that reads data, calculates, formats and prints. The fix: split it. A calculating function should return a value, and a separate function should print it. This makes each easy to test and reuse.
Question 1. What is the difference between a parameter and an argument?
A parameter is the named placeholder in the function definition (fun greet(name: String)). An argument is the actual value supplied in a call (greet("Ada")).
Question 2. What does a function with no declared return type return?
It returns Unit, Kotlin's way of saying "no meaningful value". It is similar to void in other languages, but Unit is a real type with exactly one value.
Question 3. Rewrite this as a single-expression function: fun triple(n: Int): Int { return n * 3 }
fun triple(n: Int) = n * 3 (or with the explicit type: fun triple(n: Int): Int = n * 3).
Question 4. Given fun order(item: String, qty: Int = 1, note: String = ""), which calls are valid: order("Rice"), order("Rice", 3), order(item = "Rice", note = "no pepper"), order(3, "Rice")?
The first three are valid. The first uses both defaults, the second overrides qty, and the third uses named arguments to skip qty and set note. The fourth is invalid because the arguments are in the wrong order and types.
Question 5. Why can main not read a variable that was declared inside another function?
Because of scope. A local variable exists only inside the function (or block) where it was declared. To share a result, the function must return it and the caller stores it.
Go deeper: official resources
- Android Basics with Compose, Unit 1, Pathway 1: the official pathway containing the functions codelab and practice.
- Kotlin docs: Functions: default arguments, named arguments, varargs, local functions and more.
- Kotlin Playground: run every example in the browser.
Project goal: Build a tidy receipt printer made of small functions, each with one job. This models how real apps are organised.
Requirements:
- Write
lineTotal(price: Int, quantity: Int): Intas a single-expression function. - Write
applyDiscount(total: Int, percent: Int = 0): Intreturning the total after discount. The default must mean no discount. - Write
formatNaira(amount: Int): Stringreturning text like₦5200. - Write
printItem(name: String, price: Int, quantity: Int)that prints one line:Rice x3 @ ₦1200 = ₦3600. - In
main, record two items (rice at ₦1200 x3, beans at ₦800 x2), print both lines, then print the subtotal, a 10 percent discount using a named argument, and the final total. - Use
valthroughout.
Stretch goal: Write splitBill(total: Int, people: Int): Pair<Int, Int> that returns the share each person pays and the leftover naira, and print the result for 4 people.
fun lineTotal(price: Int, quantity: Int): Int = price * quantity
fun applyDiscount(total: Int, percent: Int = 0): Int = total * (100 - percent) / 100
fun formatNaira(amount: Int): String = "₦$amount"
fun printItem(name: String, price: Int, quantity: Int) {
val total = lineTotal(price, quantity)
println("$name x$quantity @ ${formatNaira(price)} = ${formatNaira(total)}")
}
fun splitBill(total: Int, people: Int): Pair<Int, Int> = Pair(total / people, total % people)
fun main() {
println("===== RECEIPT =====")
printItem("Rice", 1200, 3)
printItem("Beans", 800, 2)
val subtotal = lineTotal(1200, 3) + lineTotal(800, 2)
val finalTotal = applyDiscount(subtotal, percent = 10)
println("-------------------")
println("Subtotal: ${formatNaira(subtotal)}")
println("Total after 10% discount: ${formatNaira(finalTotal)}")
val (share, leftover) = splitBill(finalTotal, 4)
println("Split 4 ways: ${formatNaira(share)} each, ${formatNaira(leftover)} left over")
}
Reasoning through the output: rice is 1200 times 3, which is 3600. Beans is 800 times 2, which is 1600. The subtotal is 5200. With 10 percent off, 5200 times 90 is 468000, divided by 100 is 4680. Splitting 4680 between 4 people gives 1170 each with nothing left over.
Expected output:
===== RECEIPT =====
Rice x3 @ ₦1200 = ₦3600
Beans x2 @ ₦800 = ₦1600
Subtotal: ₦5200
Total after 10% discount: ₦4680
Split 4 ways: ₦1170 each, ₦0 left over
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 08 project”.
On github.com click New repository, name it android-lesson-08-create-and-use-functions-in-kotlin, 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 08 complete! 🎉
You covered:
- Lesson Introduction
- Prerequisite Concepts
- Conceptual Understanding: What a Function Is
- Worked Examples
- Scope: Who Can See What
- Seeing the Call Stack
- Functions in Android Studio
- Practice: Kotlin Basics (Functions Set)
- Guided Practice Exercises
- Common Beginner Mistakes (and How to Fix Them)
- Knowledge Check