You have learned a great deal of Kotlin. Variables, types, control flow, loops, functions, the whole family of collections, null safety, lambdas, classes, inheritance, interfaces, and threading—that is a genuinely solid command of the language, and you have the games to prove it.
But Kotlin is bigger than one book, and we made deliberate choices about what to leave out so we could keep moving and keep building. This chapter is a guided tour of the most useful corners we skipped. The goal is not to teach each one in depth—that would be another book—but to make sure that when you meet these features in real code, or in something an AI assistant writes for you in the next chapters, you recognize them and know where to look to learn more. Think of it as a map of the territory beyond the edge of what we have explored.
In this chapter, we will:
- Meet coroutines, Kotlin's headline feature for background work.
- See how data classes give you tidy data containers for free.
- Use enums and sealed classes to model fixed sets of options safely.
- Add methods to existing types with extension functions.
- Meet the scope functions (
let,apply,run,also,with). - Learn about
lateinitand lazy properties. - Understand what the angle brackets in
List<T>really mean (generics). - Glance at the huge slice of Android we deliberately skipped.
- Point you toward where to learn more.
Let's explore the wilderness.
Coroutines and Asynchronous Code
If there is one famous Kotlin feature this book did not use, it is coroutines. They are Kotlin's modern answer to doing work in the background—downloading a file, reading a database, calling a web service—without freezing the screen.
We met this problem back in Chapter 20: the UI thread must stay free, so slow work has to happen elsewhere. Our answer was a raw Thread. That is perfect for a game loop that runs forever, but it is clumsy for one-off background tasks. Coroutines are a far smoother tool for those. They let you write code that looks sequential—do this, then when it's done do that—while it actually runs without blocking anything:
// A sketch — this needs the coroutines library and a scope to run in
launch {
val data = loadFromNetwork() // suspends here, doesn't block the screen
showResult(data) // runs when the data is ready
}
The keyword suspend marks a function that can pause and resume, and launch starts a coroutine. The result reads like ordinary top-to-bottom code, but the waiting happens off the UI thread. Coroutines are the standard way to handle networking, databases, and any long task in modern Android. We did not need them for a self-contained game loop, but they are essential the moment your app talks to the outside world. They are a deep topic—the official Kotlin coroutines guide is the place to start.
Data Classes
Many times in this book we wrote small classes whose only job was to hold a few values. Kotlin has a shortcut for exactly that: the data class. Add the word data in front of class, and Kotlin writes a pile of useful boilerplate for you:
data class Score(val name: String, val points: Int)
That one line gives you a class that, for free, can compare two scores for equality, print itself sensibly, and copy itself with tweaks:
val a = Score("Alex", 100)
val b = Score("Alex", 100)
println(a == b) // true — data classes compare by their contents
println(a) // Score(name=Alex, points=100) — a readable printout
val c = a.copy(points = 150) // a new Score, same name, different points
Without data, comparing two objects checks whether they are the same object in memory, and printing one gives an ugly code. A data class fixes both, plus throws in copy. Whenever you have a class that is really just a bundle of values—a high score, a save record, a settings entry—reach for data class.
Enums and Sealed Classes
Often a value should be one of a small, fixed set of options. A direction is up, down, left, or right. A game is in the menu, playing, or over. For these, Kotlin gives us enums:
enum class GameState {
MENU, PLAYING, GAME_OVER
}
var state = GameState.PLAYING
An enum defines a type whose only possible values are the ones you list. It pairs beautifully with the when expression from Chapter 4, because Kotlin can check you have handled every case:
when (state) {
GameState.MENU -> drawMenu()
GameState.PLAYING -> drawGame()
GameState.GAME_OVER -> drawGameOver()
}
If you later add a fourth state and forget to handle it, Kotlin warns you—a real safety net. (We managed our game states with simple Boolean flags like isGameOver in this book; an enum is the tidier choice as games grow more states.)
Sealed classes are enums' more powerful cousin. They also describe a fixed set of options, but each option can carry its own data. A network result might be a success carrying data, or a failure carrying an error message:
sealed class Result
data class Success(val data: String) : Result()
data class Failure(val error: String) : Result()
Like enums, sealed classes let when check you have covered every case. They are the idiomatic Kotlin way to model "one of these specific shapes, each with its own details."
Extension Functions
Here is a genuinely delightful Kotlin feature. You can add a new method to a type you don't own—even a built-in one like Canvas or Float—without subclassing it. These are extension functions:
fun Canvas.drawCenteredText(text: String, paint: Paint) {
drawText(text, width / 2f, height / 2f, paint)
}
The Canvas. in front of the function name means "this is a new method on Canvas." Inside it, width and drawText refer to the canvas it is called on. Now, anywhere in your code, you can write:
canvas.drawCenteredText("Game Over", paint)
as though drawCenteredText had been part of Canvas all along. Extension functions are how Kotlin keeps code readable—you add the convenience methods you wish a class had, right where you need them. You will see them constantly in real Kotlin, and they are a tidy way to organize helper code.
Scope Functions
Kotlin has a small family of helpers called scope functions—let, apply, run, also, and with—that run a block of code "on" an object. They show up everywhere in idiomatic Kotlin (and in AI-generated code), so they are worth recognizing. The most useful for us would have been apply, which configures an object and hands it back:
val paint = Paint().apply {
color = Color.RED
textSize = 60f
isAntiAlias = true
}
Inside the apply { } block, you can set properties directly without repeating paint. each time—it is "applied" to the new Paint. We wrote those settings the long way throughout the book for clarity, but apply is the concise idiom. The other scope functions do variations on the theme: let is handy for running code only when a nullable value isn't null (name?.let { ... }), and the rest tune the details. They take a little getting used to; the official Kotlin docs have a clear guide to choosing between them.
lateinit and Lazy Properties
Back in Chapter 21, when we wired up the game loop, we stored our GameView in a nullable property and reached it with safe calls—gameView?.resume(). I mentioned then that there was a tidier way, saved for this chapter. Here it is: lateinit.
lateinit lets you declare a non-null property that you promise to set up shortly after the object is created, rather than right away:
private lateinit var gameView: GameView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
gameView = GameView(this) // set it up here
setContentView(gameView)
}
override fun onResume() {
super.onResume()
gameView.resume() // no safe call needed — we promised it's set
}
We wrote lateinit var gameView: GameView (no ?, no null), and from then on used gameView directly, with no ?.. The catch is that the promise is on you: if you use it before setting it, the app crashes. Used correctly, it removes a lot of needless null-handling for properties you know will be ready in a moment. It is extremely common in real Android code, which is exactly why our Chapter 21 example would more typically be written this way.
A close relative is by lazy, which sets a property up automatically the first time it is used:
private val atlas by lazy { Atlas(context) }
atlas is not created until the first time something reads it, and then it is remembered. Both lateinit and by lazy are about when a property gets its value—handy tools once you know they exist.
Generics (Briefly)
You have used generics in every project since Act 2, perhaps without naming them. Every time you wrote MutableList<Particle> or Map<String, Int>, those angle brackets were generics at work. They let a single class—MutableList—work with any type, while still keeping things type-safe: a MutableList<Particle> holds particles and nothing else, and the compiler enforces it.
You can write your own generic code, too. The <T> is a stand-in for "some type, decided when you use it":
fun <T> firstOrNull(list: List<T>): T? {
if (list.isEmpty()) return null
return list[0]
}
Here <T> means this function works on a list of anything, returning that same type (or null). Call it with a List<Particle> and you get a Particle? back; call it with a List<String> and you get a String?. Most of the time you will use generics (through the collections) far more than you write them, but knowing what the brackets mean demystifies a lot of Kotlin code.
The Android You Haven't Seen
Finally, an honest admission about Android itself. This book took a very particular path—straight to the Canvas, drawing everything ourselves—precisely so we could learn real programming without getting lost in Android's enormous standard toolkit. But that toolkit is most of what professional Android development actually is, and it is worth knowing what is out there:
- Jetpack Compose and XML layouts—the two standard ways to build normal app interfaces (buttons, lists, text fields, navigation). We skipped both entirely. If you want to build a typical app rather than a game, this is where you would go next.
- The Activity and Fragment lifecycle—we touched only
onCreate,onResume, andonPause. There is a whole, detailed lifecycle for managing screens as the user navigates and the system reclaims memory. - ViewModels and app architecture—the recommended patterns for organizing a real app's data and logic.
- Networking, databases (Room), and storage—talking to web servers and saving data properly (we only ever drew to the screen).
- Permissions, notifications, sensors, and the rest—the device features a full app reaches for.
None of this was needed to learn Kotlin or to build games the way we did, and skipping it is exactly why this book could move as fast as it did. But it is the natural next direction if you want to broaden from games into Android apps in general.
Further Reading and Resources
To keep going, a few solid starting points. The official Kotlin documentation (kotlinlang.org) is excellent and free, with dedicated guides to coroutines, generics, and the scope functions. The book Kotlin in Action is a well-regarded deeper dive into the language. For Android itself, the official Android developer guides (developer.android.com) cover Compose, lifecycle, and everything else mentioned above. And for game-specific questions, the patterns in Game Programming Patterns by Robert Nystrom (free online) will sharpen the architecture instincts you started building in Act 3.
Beyond books and docs, you now have the single most powerful learning tool there is: an AI assistant, and the judgment to use it well. Which is exactly where we are headed next.
Summary
This was a flyover of the Kotlin and Android territory beyond this book: coroutines for background work, data classes for tidy value holders, enums and sealed classes for fixed sets of options, extension functions for adding methods to existing types, the scope functions for concise configuration, lateinit and by lazy for controlling when properties are set, generics for type-safe reuse, and the large slice of standard Android we deliberately bypassed. You do not need any of these to have built what you built—but you will meet all of them in the wild, and now you will know them when you do.
The rest of the book is about acceleration. You have the fundamentals; now we add a partner. The next chapter is about using AI as a coding assistant—what it is good at, where it lies to you, and how to direct it well—and then we put it to work building two new games, faster and richer than we could alone, with you firmly in command.
AI Exercise (Optional)
Fittingly, let's use an AI to explore one of the features this chapter only sketched. Pick whichever interests you most and paste a prompt like this (here, data classes):
"I am learning Kotlin. I have built games using regular classes with properties and methods, but I have NOT used data classes. Please show me how to rewrite a simple
Scoreclass (holding a player name and a number of points) as adata class, and demonstrate the three things a data class gives me for free: comparing two scores with ==, printing one readably, and making a modified copy. Explain what would be different if I used a regular class instead. Keep it beginner-friendly."
This asks the AI to show one wilderness feature concretely, against the kind of class you already know how to write. Comparing the data class version to a plain class is what makes the benefit click.
When the answer comes back, check it against this chapter: does the data class really compare by contents, print nicely, and offer copy? Does the AI correctly explain that a regular class would compare by identity and print an unreadable code instead? If you are curious, follow up with a different feature—"now show me the same Score used as the value in a Map," or "show me an enum for game states with a when"—and keep exploring. You are using AI exactly as the next chapters will teach: to learn, to compare, and to extend what you already understand.