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
23: Visibility Modifiers
If you're coming from Java, you probably have a mental habit of thinking about "package-private" visibility. You might assume that if you don't provide a modifier, or if you use internal, you're restricting access to just the files in that specific folder. I see this mistake constantly in code reviews.
The "Internal equals Package-Private" Myth
In Java, if you omit a modifier, the class or member is visible to everything in the same package. It's a common mistake to assume Kotlin's internal modifier does the same thing. Let's look at why that's wrong.
// File: com.app.payments.PaymentProcessor.kt
internal class PaymentGateway {
fun processPayment() {
println("Processing...")
}
}
// File: com.app.utils.NetworkHelper.kt (Different package!)
fun setup() {
val gateway = PaymentGateway() // This WORKS in Kotlin
gateway.processPayment()
}
In the example above, PaymentGateway is in the payments package, but NetworkHelper is in the utils package. If this were Java's default visibility, this would throw a compiler error. But in Kotlin, it works perfectly. Why? Because internal doesn't care about packages; it cares about modules.
Thinking in Modules, not Folders
A module is a set of Kotlin files compiled together—think of a Gradle source set, a Maven project, or an IntelliJ module. When you mark something as internal, you're telling Kotlin: "Anyone inside this specific build artifact can use this, but if this code is ever imported as a library by another project, keep it hidden."
I personally love internal because it allows me to organize my code into clean packages without worrying about visibility barriers, while still ensuring that the "public API" I expose to the rest of the company (or the world) stays lean and intentional.
Tightening the Screws with Private and Protected
While internal handles the module boundary, you still need the finer tools for class-level encapsulation. Remember that in Kotlin, the default visibility is public. If you don't type anything, the whole world can see it.
- private: This is the strictest. If you mark a function
privateinside a class, it's invisible even to other classes in the same file. If you mark a propertyprivateat the top level of a file, it's invisible to every other file in the module. - protected: This is strictly for inheritance. A
protectedmember is visible to the class it's defined in and any subclasses. You won't findprotectedon top-level functions because, well, you can't inherit from a function.
Let's look at a more realistic scenario where we mix these to protect a sensitive operation:
open class BaseTransaction {
// Only subclasses can access the raw transaction ID
protected val transactionId = "TXN-12345"
// Only this specific class can handle the encryption key
private fun encryptPayload(data: String) = "encrypted_$data"
// Anything in the same module can trigger a sync
internal fun syncWithServer() {
println("Syncing $transactionId...")
}
// Anyone using this library can execute the payment
fun execute() {
val secret = encryptPayload("amount=100")
println("Executing payment with $secret")
}
}
Notice the hierarchy of trust here. I trust the execute() method to be public, I trust my subclasses to see the transactionId, and I trust my module to handle syncWithServer(). But I trust absolutely no one with the encryptPayload logic except the class itself.
📋 Practical Task
Hardening the Banking Transaction API
You are building a banking module. Currently, the BankVault class is too permissive, exposing sensitive internal logic to the entire application. Your task is to apply the correct visibility modifiers to secure the class based on the following requirements:
- The
vaultKeyshould be completely hidden from everything except theBankVaultclass. - The
auditLog()method should be accessible to any class within the same Gradle module, but hidden if this module is imported as a library. - The
calculateInterest()method should be accessible to theBankVaultclass and any classes that inherit from it (e.g.,SavingsVault). - The
deposit()method should remain accessible to the entire application.
class BankVault {
val vaultKey = "SECRET_123"
fun auditLog() {
println("Logging transaction...")
}
fun calculateInterest() {
println("Calculating interest...")
}
fun deposit(amount: Double) {
println("Depositing $$amount")
}
}
Task: Rewrite the BankVault class applying private, internal, and protected modifiers to meet the requirements above. (Note: You will need to make the class open to allow for the protected modifier to make sense for future subclasses).
There are no comments for now.