Look at the onDraw function in our last few projects. It is starting to do rather a lot. It clears the screen, updates positions, checks for collisions, chooses colors, and draws everything — all crammed into one long block of code. It works, but it is getting hard to read. If you wanted to find the line that handles bouncing, you would have to scan through everything else to get to it.
Real programs solve this with functions. A function is a named, reusable block of code that does one job. Instead of one enormous onDraw, we can have a tidy onDraw that simply says "update the game, then draw the game" — and the messy details live in separate functions called update and draw. Our code becomes a set of well-labeled drawers instead of one overflowing box.
You have actually been working with functions since page one. onCreate, onDraw, and onTouchEvent are all functions. The difference is that those belong to Android, and Android calls them for us. In this chapter you will learn to write and call your own.
In this chapter, we will:
- Understand what a function is and the problem it solves.
- Write our own functions with the
funkeyword. - Pass information into a function using parameters.
- Get information back out of a function using return values.
- Meet two Kotlin conveniences: single-expression functions and default arguments.
- Understand scope — where a variable lives and where it doesn't.
- See how functions let us tidy up a crowded
onDraw. - Try an optional AI exercise on functions.
Let's begin with the functions you already know.
Functions You Have Already Met
Every project in this book has started the same way:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// ...our drawing code...
}
That is a function. The fun keyword announces it. The name is onDraw. The thing in parentheses, canvas: Canvas, is information being handed to the function. And the code between the curly braces is the function's body — the work it does when it runs.
We never call onDraw ourselves; Android does, every time the screen needs repainting. The override keyword, which we met in Chapter 1, means we are replacing Android's empty default version with our own. So you already understand the shape of a function. Now let's build one from scratch — one that nobody calls but us.
Anatomy of a Function
Here is about the simplest function you can write:
fun sayHello() {
println("Hello!")
}
In the preceding code, fun tells Kotlin "a function is being defined here." sayHello is the name we chose — functions follow the same camelCase convention as variables, and a function name is usually a verb or an action, because a function does something. The empty parentheses () mean this function needs no information to do its job. And the body, between the braces, is the single line it runs.
Defining a function does not run it. The code inside sayHello sits there, dormant, until we call it. Calling a function means writing its name followed by parentheses:
sayHello() // This runs the function. "Hello!" is printed.
sayHello() // Call it again — "Hello!" is printed a second time.
That is the whole point. Define the work once, then trigger it as many times as you like, from wherever you like, just by naming it. If you want to change what "saying hello" means, you change it in one place and every call gets the new behavior.
One reassuring note: in Kotlin you can define your functions in any order. A function near the top of your class can happily call a function defined further down. Kotlin reads the whole class before it runs anything, so it always knows what is available. You never have to worry about arranging functions in a particular sequence.
Parameters: Giving a Function Inputs
A function that always does exactly the same thing is useful, but limited. Most of the time we want a function whose behavior depends on some information we hand it. That information is passed through parameters.
A parameter is a variable, listed in the function's parentheses, that holds a value given to it when the function is called. Here is a function that greets a particular player:
fun greet(playerName: String) {
println("Welcome, $playerName!")
}
In the preceding code, playerName: String is a parameter. It says "this function expects to be given one piece of text, and inside the function we will call it playerName." Notice it is written exactly like a variable declaration: a name, a colon, and a type.
When we call the function, we supply the actual value, which is called an argument:
greet("Alex") // Prints: Welcome, Alex!
greet("Sam") // Prints: Welcome, Sam!
The terms are worth keeping straight: the parameter is the placeholder in the function definition (playerName), and the argument is the real value you pass in when you call it ("Alex"). Same idea, viewed from two ends.
A function can take as many parameters as it needs, separated by commas:
fun describeEnemy(name: String, health: Int) {
println("$name has $health health remaining.")
}
describeEnemy("Goblin", 30) // Prints: Goblin has 30 health remaining.
When you call a function with several parameters, the arguments line up with the parameters in order — the first argument fills the first parameter, the second fills the second, and so on. This is exactly how canvas.drawRect(left, top, right, bottom, paint) has worked all along: each number you pass slots into the matching parameter of the function.
Return Values: Getting Something Back
So far our functions have done things — printed a message, drawn a shape. But often we want a function to work something out and hand the answer back to us. That answer is the return value.
Here is a function that adds two numbers and returns the result:
fun add(a: Int, b: Int): Int {
return a + b
}
Two things are new here. First, look at the end of the first line: : Int. After the parentheses, a colon and a type declare what kind of value this function gives back. This one returns an Int. Second, inside the body, the return keyword does the handing back. return a + b calculates the sum and sends it out of the function.
We capture that returned value just like any other:
val total = add(5, 3) // total is now 8
println(total)
When Kotlin reaches add(5, 3), it runs the function, gets 8 back, and that 8 takes the place of the call — so val total = add(5, 3) becomes val total = 8. You can use a function's return value anywhere you could use a plain value, including right inside an if:
if (add(player1Score, player2Score) > 100) {
println("You beat the team target!")
}
A return value that is a Boolean is especially handy in games. A function called isGameOver() that returns true or false reads beautifully at the call site: if (isGameOver()) { ... }. We will write exactly this kind of function in the next chapter.
Functions That Return Nothing
What about our earlier functions, like sayHello, that just do something and return nothing? Those have a return type too — it is simply a special type called Unit, meaning "no useful value." Because functions that return nothing are so common, Kotlin lets you leave Unit out entirely. That is why fun sayHello() { ... } has no : Type after its parentheses — Kotlin fills in Unit for you. So every function returns something; it is just that sometimes that something is "nothing in particular."
Single-Expression Functions
When a function is short enough to be a single expression, Kotlin offers a lovely shorthand. Our add function can be written like this:
fun add(a: Int, b: Int) = a + b
The braces, the return, and even the : Int return type are gone. The = says "this function simply is the value of the expression on the right." Kotlin works out that the result is an Int on its own, using the type inference we met in Chapter 2.
This single-expression function form is purely a convenience for short functions, and it is extremely common in real Kotlin code. Use it when a function boils down to one clean line, and stick with the full braces-and-return form when there is more going on.
Default Argument Values
Kotlin lets you give a parameter a fallback value, used when the caller does not supply one. You write it with an = in the parameter list:
fun spawnEnemy(health: Int = 100, isBoss: Boolean = false) {
println("Spawned an enemy with $health health. Boss: $isBoss")
}
Now the same function can be called in several ways:
spawnEnemy() // Uses both defaults: 100 health, not a boss
spawnEnemy(50) // 50 health, not a boss
spawnEnemy(500, true) // 500 health, and it's a boss
Default arguments save you from writing several near-identical versions of the same function. They are a small feature, but once you have them, you reach for them constantly. We will not lean on them heavily in Act 1, but it is good to recognize them when an AI assistant hands you code that uses them.
Scope: Where a Variable Lives
Now that we are creating functions, a question arises. If we declare a variable inside a function, can the rest of the program see it?
The answer is no, and the rule is called scope. A variable declared inside a function exists only inside that function. The moment the function finishes, the variable is gone. We say the variable is local to the function.
fun calculateBonus() {
val bonus = 50 // 'bonus' is local to this function
println(bonus) // Fine — we are inside the function
}
// Out here, 'bonus' does not exist. Trying to use it is an error.
This is not a limitation to fight against; it is a feature that keeps your code sane. Because bonus cannot be touched from outside, you never have to worry about some distant piece of code accidentally changing it. Each function gets its own private workspace.
So where do variables like ballX and score live — the ones we want every function in our View to see? Those are declared at the top of the class, outside any single function. We have been writing them there since Chapter 3. A variable declared at the class level is called a property, and every function in the class can read and change it. That is exactly why our drawing functions and our update functions can all share the same score or paddleX.
The short version: a variable declared inside a function is private to that function; a variable declared at the top of the class is shared by all the functions in it. There is more to say about scope, and we will return to it when we cover classes properly in Chapter 16. For now, this distinction is all you need.
Taming onDraw: Refactoring with Functions
Let's bring this back to where the chapter started. Here is the shape of a typical onDraw from our recent projects — everything piled into one block:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(Color.BLACK)
// ...update positions...
// ...check collisions...
// ...draw the player...
// ...draw the enemies...
// ...draw the score...
invalidate()
}
It works, but it is a wall of code with several unrelated jobs mixed together. Refactoring means restructuring code to make it cleaner without changing what it does. With functions, we can refactor that block into something far more readable:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(Color.BLACK)
update() // Move everything and check for collisions
drawPlayer(canvas)
drawEnemies(canvas)
drawScore(canvas)
invalidate()
}
Now onDraw reads almost like a description of what happens each frame: update, draw the player, draw the enemies, draw the score. The details of how each step works are tucked away in their own functions, where you can find them instantly and change them without disturbing anything else. Notice too that the drawing functions take canvas as a parameter — they need the canvas to draw on, so we pass it in, exactly like the parameters we met earlier.
This pattern — a slim onDraw that calls a handful of well-named functions — is how every serious game is structured, and it is precisely what we are going to build in the next chapter.
Summary
Functions are how we organize code into named, reusable pieces, each doing one clear job. You learned to define a function with the fun keyword and call it by name, to feed it information through parameters, and to get answers back through return values — including the Boolean-returning functions that read so naturally inside an if. You met Kotlin's single-expression shorthand and default arguments, and you learned the rule of scope: variables inside a function are local and private, while variables at the top of the class are properties shared by every function. Finally, you saw how functions let us refactor a bloated onDraw into a clean, readable sequence of steps.
This is the last piece of Act 1. You now have variables, types, flow control, loops, and functions — the complete toolkit of fundamental programming, with not a single class of our own or a fancy library in sight. In the next chapter we put the whole lot together and build the Act 1 capstone: a real, playable game, structured entirely with the functions you just learned. After that, Act 2 opens the door to collections, and our games start managing not one falling apple, but hundreds of things at once.
AI Exercise (Optional)
Functions are a perfect topic to explore with an AI assistant, because organizing code well is as much a matter of taste and judgment as of rules. As always, this is optional — skip it freely.
Open your AI chatbot and paste in this prompt:
"I am a beginner learning Kotlin. I have just learned how to write functions: the
funkeyword, parameters, return values, and return types. I know variables, if/when, and loops, but I have NOT learned about classes (beyond using one), lists, or arrays. Please write three small, separate Kotlin functions for a game: one that takes a score as a parameter and returns a Boolean saying whether it beats a high score of 1000; one that takes a player's health and returns a String describing their condition ('healthy', 'hurt', or 'critical'); and one that takes no parameters and prints a welcome message. After each function, show me one example of calling it. Use only the features I listed."
Notice how the prompt asks for one function of each kind we covered: one that returns a Boolean, one that returns a String (and will need an if or when inside it), and one that returns nothing. That is a deliberate way to check your own understanding against three different shapes of function at once.
When the answer comes back, read each function and ask yourself: what is its return type, and where does the return happen? For the health function, did the AI use if/else if or a when? Could you have written it the other way? If anything is unclear, ask the AI to rewrite one of the functions in the other style so you can compare them side by side. Comparing two correct solutions to the same problem is one of the fastest ways to deepen your understanding.