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
200: Context Receivers (Experimental Feature)
I've been spending the morning refactoring a piece of a project where I'm handling complex data transformations. I keep running into the same annoying problem: my functions need a "dependency" (like a logger or a configuration object), but they also need to operate on a specific object. In Kotlin, we usually solve this with extension functions, but extension functions have a strict limit—they only give you one receiver.
The Extension Function Wall
Let's look at what I'm trying to do. I have a User profile, and I want to validate it. To do that, I need a ValidationRules object to tell me what's legal, and a Logger to record any failures. My first instinct was to make it an extension function on User:
class User(val name: String, val age: Int)
class ValidationRules { fun isValidName(name: String) = name.isNotEmpty() }
class Logger { fun log(msg: String) = println("LOG: $msg") }
fun User.validate(rules: ValidationRules, logger: Logger) {
if (!rules.isValidName(this.name)) {
logger.log("Invalid name for user $name")
}
}
This works, sure. But as my project grows, I find myself passing rules and logger into every single function in this layer. It's "parameter drilling." My function signatures are becoming 70% plumbing and 30% actual logic. I wanted to make it an extension of ValidationRules instead, but then I can't easily make it an extension of User. I can't do fun ValidationRules.User.validate(). Kotlin just doesn't allow multiple extension receivers.
Fighting the Boilerplate
I tried to wrap things in a class to hold the state, but that just adds another layer of nesting and makes the functions harder to move around. I started wondering: why can't I just tell the compiler, "This function requires these three things to be present in the environment to run," without forcing them to be explicit arguments every single time?
That's where I stumbled into Context Receivers. Now, keep in mind, this is an experimental feature, so you'll need to enable it in your Gradle settings (-Xcontext-receivers), but the power it gives you is exactly what I'm looking for.
Breaking the One-Receiver Limit
Instead of passing the dependencies as arguments, I can define them as contexts. Watch what happens when I rewrite that validate function:
context(ValidationRules, Logger)
fun User.validate() {
// I have access to 'this' (the User)
// AND I have access to ValidationRules and Logger as if they were 'this'
if (!isValidName(this.name)) {
log("Invalid name for user $name")
}
}
This is wild. Inside the function, I can call isValidName() (from ValidationRules) and log() (from Logger) directly. I don't need to reference a variable name because the compiler knows that for this function to even be callable, those two types must be available in the surrounding scope.
But here is the catch: if you try to call user.validate() in a standard main function now, the code won't compile. The compiler will complain that ValidationRules and Logger are missing. It's essentially enforcing a requirement at the type level.
Actually Making It Run
To make this work, I have to provide the context. The most common way to do this is using with() blocks, nesting them to provide the required environments. I tried it like this:
fun main() {
val user = User("Alice", 30)
val rules = ValidationRules()
val logger = Logger()
with(rules) {
with(logger) {
user.validate() // Now it compiles!
}
}
}
I'll be honest: nesting with blocks feels a bit clunky. But imagine this in a real-world architecture. You could have a base service class that provides these contexts, or a higher-level coordinator that wraps a whole block of business logic in the necessary contexts. Suddenly, your internal domain functions are clean, focused on the logic, and free of the "plumbing" arguments that usually clutter up the code.
It turns the dependency from something you pass into something you require. It's a subtle shift, but it makes the intent of the function much clearer: "I am a User validation logic, and I can only exist in a world where rules and logging are available."
📋 Practical Task
Implementing a Transaction-Aware Order Service
You are building a checkout system. You have a Database class and a Transaction class. The Order class should have a saveToDb() method, but this method must only be callable when both a Database connection and an active Transaction are present in the context.
Requirements:
- Create a
Databaseclass with a methodconnect(). - Create a
Transactionclass with a methodcommit(). - Create an
Orderclass. - Write an extension function for
OrdercalledsaveToDb()that uses context receivers to require bothDatabaseandTransaction. - Inside
saveToDb(), callconnect()andcommit()to simulate the saving process. - In your
mainfunction, instantiate the objects and usewith()blocks to provide the necessary context so thatsaveToDb()can be executed.
There are no comments for now.