Kotlin Context Receivers — misunderstood and underestimated feature

Kacper Wojciechowski
5 min readAug 31, 2023

--

Photo by Belinda Fewings on Unsplash

Kotlin Context Receivers were added to Kotlin in 1.6.20. Ever since I’ve never seen them actually in use. I feel like this is the most misunderstood and underestimated Kotlin feature. Most articles cover the usual approach — here you have a class of type Animal, and to create an extension for animals to eat grass, you need access to the Grass. So you might pass it as a parameter — or as Kotlin Context Receiver to make it even more cohesive and easy to use. Honestly, I hate those imaginary examples. The best way to explain something is to actually show it in real-life usage.

Recently, I’ve had those “aha!” moments where the usage of Context Receiver was just perfect. I’ve decided to show those examples in this article — so maybe you will also have those “aha!” moments that will make your life easier.

How to enable Kotlin Context Receivers?

Context Receivers is still an experimental feature, even tho we’ve had 3 major Kotlin releases since they were added. In order to enable them we need to add a compiler argument:

tasks.withType(KotlinCompile::class.java) {
kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
}

So what are Kotlin Context Receivers?

In order to answer this question we first need to explain what Context is in Kotlin world. Let’s look at those 2 code snippets:

When writing the code in place of “…” we are in the scope of the Logger class. We have access to its functions and properties without object specification (or with this keyword). The code is in the Context of the Logger object.

Context Receiver is a feature that lets us mark that this function can be used only in the Context (one or multiple ones) of the specified type.

Here, for example, we have a function that extends String and can be used only in the scope of Logger. This extension function uses the log(message: String) function of Logger, as we are in its Context.

The real-world usages

So now that we have established what Context and Context Receiver are, let’s talk about some real-world usages.

Compose Density extensions

Let’s say we are writing a custom component with the use of the Canvas. Our Composable function accepts the offset of some internal element in Dp. We can use DpOffset as it is a really cool wrapper for such a case.

But drawing functions inside Canvas accept only pixel values. For such purpose, Canvas’es DrawScope implements Density which is an API for Dp to pixel conversion. So let’s convert DpOffset to Offset which will contain pixel values. Since DpOffset does not have a built-in extension to do so, we need to build it ourselves!

And here’s the problem, How do we access those conversion functions that Density provides? We could pass the Density object as a parameter and then somehow use it here, but it feels just … not Kotlinish. We can’t write an extension function that extends multiple types. Or can we?

Context Receiver to the rescue!

We’ve specified that this extension can be used only in the Context of Density. This way we are able to access those conversion functions inside a function that extends a totally different object. Now we can convert DpOffset to Offset in a nice and cohesive way!

Analytics parameters

Let’s say we have an analytics logger that accepts the event name and some parameters.

Now let’s say we have a screen with multiple events and the same params for all of the events.

We need to pass those parameters every time. Let’s say we have like 20 of those events. We need to pass analyticsParams to every single line of code. Can we do something about that? Maybe we can create some extension function so it will add this parameter for us. But in order to do so we would need to specify this extension function for every screen as every screen has a different property of those parameters. That’s a lot of redundant code!

Let’s use Context Receivers!

We can create a Context that will provide those parameters for us with one extension function to rule them all.

First, let’s create an interface for the Context that will provide our parameters:

Now let’s create an extension function, that will extend AnalyticsLogger and will be usable in the context of AnalyticsParamsProvider:

Now we have an extension function that will add those parameters for us if we are in the Context of AnalyticsParamsProvider. So, how we can be in the Context of this provider? By implementing this interface in our ViewModel class:

Look how clean it is now!

Context Receivers under the hood

For a better understanding of how Kotlin actually works, I recommend sometimes sitting down and decompiling some Kotlin bytecode to Java (f.e. with this plugin). We can learn some interesting stuff about Kotlin in general. For example, did you know that Extension functions are actually just functions that accept an extended object as a parameter? Exactly the same story is for Context Receivers, just some more parameters named a bit differently.

public static final void logWithParams(@NotNull AnalyticsParamsProvider $context_receiver_0, @NotNull AnalyticsLogger $this$logWithParams, @NotNull String name) {
Intrinsics.checkNotNullParameter($this$logWithParams, "<this>");
Intrinsics.checkNotNullParameter($context_receiver_0, "$context_receiver_0");
Intrinsics.checkNotNullParameter(name, "name");
$this$logWithParams.log(name, $context_receiver_0.getAnalyticsParams());
}

Conclusion

Context Receivers is a powerful Kotlin feature — if you understand which issues it resolves. I hope that I’ve shed some light on how we can utilize them in real-world code. Feel free to share your thoughts and share your use cases in the comments!

--

--