Context Receivers in Kotlin: An Example

I break (and occasionally build) apps for Android. I sometimes work on backend as well. When I'm not coding, I can most likely be found watching a TV Show, a movie, playing video games or reading a book.
Search for a command to run...

I break (and occasionally build) apps for Android. I sometimes work on backend as well. When I'm not coding, I can most likely be found watching a TV Show, a movie, playing video games or reading a book.
No comments yet. Be the first to comment.
In long agent conversations, Developers often lose track of how much context has been used. Tools like Claude Code or Codex have auto-compaction built in, but they compact too late. Opus 4.8 has a mil

Bottom Sheets have become ubiquitous in Mobile design across Android and iOS. If you’re using Jetpack Compose, you’ll likely implement them using either the ModalBottomSheet or the BottomSheetScaffold. The former is more used in my experience, so fur...

A task I completed that I wouldn't have (or at least, not without putting several hours into it) without Generative AI: I use an Arch-based Linux distro called Endeavour OS. It's a great OS, but lately, I've been having this issue where my computer w...
Integrating Google maps into Android apps has been somewhat of a tough nut to crack for developers. With the advent of Jetpack Compose for building native UI on Android, the Google Maps team announced a Map SDK for Compose, available here. However, i...

Context Receivers are a new feature introduced in Kotlin 1.6.20. They're useful for receiving the context from the call-site when a function is called. To understand it and its utility, let's first take a look at a similar feature available in the language.
In Kotlin, Extension functions have proven to be an extremely useful feature. They can be used to extend the functionality of a class without inheriting it, and prove extremely useful when you have to define utility methods.
Say you wanted to write a utility method that reverses an integer. You could write something like this:
fun reverse(number: Int): Int {
var sum = 0
var num = number
while (num != 0) {
sum = (sum * 10) + (num % 10)
num /= 10
}
return sum
}
println(reverse(12345))
With extension functions, you can define an additional method on the Int the class itself, and call that on any integer you'd like:
fun Int.reverse(): Int {
var sum = 0
var num = this
while (num != 0) {
sum = (sum * 10) + (num % 10)
num /= 10
}
return sum
}
println(12345.reverse())
When we're using Extension Functions, the object that it is being called upon is called the receiver.
With extension functions, we can only use a single receiver at a time. There are some use cases that warrant the usage of 2 or more receivers.
As an example, consider the case where we're using StateFlow instead of LiveData to make the UI reactive to state change. The correct method to collect a StateFlow in a Fragment looks like this:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect {
binding.toolbar.text = it.titleText
// ...
}
}
}
This is rather verbose, and if the pattern is being used in several places, you'd obviously like to convert this to a utility function that'd make things simpler. A very simple definition is presented below:
fun Fragment.collectStateFlow(body: suspend CoroutineScope.() -> Unit) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
body()
}
}
}
// In Fragment
collectStateFlow {
viewModel.state.collect {
binding.toolbar.text = it.titleText
// ...
}
}
What if we wanted to take this a step further, and eliminate the additional line to collect the StateFlow too? We can define an extension function on Flow or StateFlow that can collect emissions while handling the lifecycle under the hood. Here's where we run into a problem. If we define an extension function on StateFlow, it can only receive the StateFlow. In other words, only methods that can be called on a StateFlow will be inside the function, since it'll have the context of the StateFlow it has been called on. Hence, we'll have to pass the Fragment as an argument to the function:
fun <T> StateFlow<T>.collectWithLifecycle(fragment: Fragment, block: (T) -> Unit) {
fragment.viewLifecycleOwner.lifecycleScope.launch {
fragment.repeatOnLifecycle(Lifecycle.State.CREATED) {
this@collectWithLifecycle.collect {
block(it)
}
}
}
}
// In Fragment
viewModel.state.collectWithLifecycle(this) {
binding.toolbar.text = it.titleText
// ...
}
Now that's neat. Looks a lot like the observe method of a LiveData right?
Being programmers, we have a bad habit of trying to write clever code, so your mind naturally wonders: What if we could have the Fragment context just be implicitly available in the function body? That'd be great. And that's exactly where Context Receivers help us. For a good explanation of how they work, see this. In short, using context receiver syntax, we can have as many receivers for our function as we'd like, meaning we can get any scope we want. Consider this example:
class A {
val x = 10
}
class B {
val y = 20
}
context(A, B)
fun printValues() {
println(x)
println(y)
}
fun main() {
A().apply {
with(B()) {
printValues()
}
}
}
No need to pass arguments. We can access x and y inside printValues since when we call it, contexts of both classes A, and B are available, and that's seemingly transferred to our method.
To use Context Receivers on Android, add this to the kotlinOptions block in your app-level build.gradle file:
freeCompilerArgs = ["-Xcontext-receivers"]
// Overall
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs = ["-Xcontext-receivers"]
}
Additionally, you need to be using Kotlin 1.6.20 or higher.
Once we've enabled the feature, we can write a utility method like this:
context(Fragment)
fun <T> StateFlow<T>.collectWithLifecycle(block: (T) -> Unit) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
this@collectWithLifecycle.collect {
block(it)
}
}
}
}
// In Fragment
viewModel.state.collectWithLifecycle {
binding.toolbar.text = it.titleText
// ...
}
This eliminates all boilerplate we need to write while collecting a StateFlow.
Tip: Instead of
StateFlow<T>, useFlow<T>to use with allFlowderivates such asSharedFlowandStateFlow.
Tip: To use with
Activity, replaceFragmentwithActivityas the receiver in the code, and removeviewLifecycleOwnersince it's not necessary with anActivity.
Tip: Compose already has an API for this. See here.
Arrivederci!