Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
39: Immutable vs Mutable Collections
Here is a snippet of code I reviewed recently from a junior dev. They were trying to build a simple system to track users currently logged into a chat room. It looks innocent enough, but it won't even compile.
fun main() {
val activeUsers = listOf("Alice", "Bob", "Charlie")
println("Adding a new user...")
addUser(activeUsers, "Dave")
println(activeUsers)
}
fun addUser(users: List<String>, newUser: String) {
users.add(newUser) // ❌ Compilation Error: Unresolved reference: add
}
The "ReadOnly" Wall
If you're coming from Java or Python, this feels wrong. You've created a list, you're passing it to a function, and you're telling that function to add an item. In those languages, lists are generally mutable by default. But in Kotlin, listOf() creates a List, and in Kotlin, the List interface is strictly read-only. It doesn't have an add(), remove(), or clear() method.
The developer here assumed that because the list was stored in a val, it was just the variable that was immutable, not the content. In reality, Kotlin separates the concept of the reference (val vs var) from the concept of the collection's capability (List vs MutableList).
Switching to MutableList for Dynamic Data
To fix this, we need to use mutableListOf(). This returns a MutableList, which extends List but adds all the modification methods we need. We also have to update the function signature so it explicitly accepts a MutableList.
fun main() {
// We use mutableListOf here to allow changes
val activeUsers = mutableListOf("Alice", "Bob", "Charlie")
println("Adding a new user...")
addUser(activeUsers, "Dave")
println(activeUsers) // Output: [Alice, Bob, Charlie, Dave]
}
// The parameter type must be MutableList to call .add()
fun addUser(users: MutableList<String>, newUser: String) {
users.add(newUser)
}
Now the code compiles and works. But here is a professional tip: just because you can make everything mutable doesn't mean you should. If every function in your app can modify every list it touches, you'll spend your weekends chasing down bugs where data "mysteriously" disappeared or changed in a different part of the app.
Protecting Your State with Read-Only Views
I usually follow a pattern where I keep the mutation logic private and expose the data as a read-only List. This prevents external code from accidentally clearing my lists or adding garbage data.
Look at how I'd actually structure that user tracker in a real project:
class UserSessionManager {
// Private mutable list: only this class can change it
private val _users = mutableListOf<String>()
// Public read-only view: others can see the users, but not change them
val users: List<String> get() = _users
fun addUser(user: String) {
if (user.isNotBlank()) {
_users.add(user)
}
}
}
By doing this, I'm using the type system to enforce my business rules. If some other developer tries to call manager.users.add("Hacker"), the compiler will stop them immediately. I've essentially created a "read-only window" into my private mutable data.
📋 Practical Task
Refactoring the Game Inventory System
You have been handed a PlayerInventory class that is causing bugs because any part of the game code can modify the player's items without going through the proper validation logic. Your task is to refactor this class to protect the internal state.
Requirements:
- Change the internal items storage so it is
privateandmutable. - Expose the items to the rest of the game as a read-only
List. - Ensure the
addItemfunction only adds the item if the inventory has fewer than 5 items (the capacity limit).
// CURRENT BROKEN CODE
class PlayerInventory {
val items = mutableListOf<String>()
fun addItem(item: String) {
items.add(item)
}
}
fun main() {
val inventory = PlayerInventory()
inventory.addItem("Iron Sword")
// BUG: External code can bypass the addItem logic and add 100 items!
inventory.items.add("Cheat Item 1")
inventory.items.add("Cheat Item 2")
// ...
}There are no comments for now.