Pixabay’s picture: https://www.pexels.com/fr-fr/photo/antenne-parabolique-blanche-33153/
Sometimes in our code, we want to use something only in a certain scope, without which it won’t work properly.
We can use context receivers to express constraints.
This article aims to demonstrate one of the many use cases and problems that Kotlin context receivers have come to solve by using a real-life case.
A real-life case with Jetpack Compose
Let’s take an example of Jetpack Compose. We have modifiers that can only be accessed inside a certain scope, like a type of Modifier.align(alignment: Alignment)
which can only be used inside a BoxScope
, the Modifier.align(alignment: Alignment.Horizontal)
for ColumnScope
,etc.
In Kotlin, you can define such a context-restricted declaration using a member extension function
interface BoxScope { fun Modifier.align(alignment: Alignment): Modifier }
This modifier will be accessible only In a BoxScope’s context. In order to achieve that, compose Box use BoxScope
as a receiver of its content, and it can be implemented as :
internal object BoxScopeImpl : BoxScope { override fun Modifier.align(alignment: Alignment): Modifier { /*...*/ } } @Composable fun Box( modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit ) { val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints) Layout( content = { BoxScopeImpl.content() }, measurePolicy = measurePolicy, modifier = modifier ) }
Box content will have access to BoxScope
that means it can use this Modifier.align()
.
Disclaimer : in the code above I just tried to simplify the implementation of the compose box, the goal was not to redo it exactly but rather to show you one of the problems that context receivers come to solve
So what is the problem ?
Our problem concerns the way in which the Modifier is declared.
interface BoxScope { fun Modifier.align(alignment: Alignment): Modifier }
- The key one is that a member extension cannot be declared on a third-party class. So the only way to write another BoxScope’s Modifier is to write it as a member of
BoxScope
(that means it’s impossible) - It limits the ability to decouple, modularize and structure APIs in larger applications. Modifiers don’t have to be declared here, because we can create a file containing the definition of them all.
- Another limitation is that only one receiver can represent a context. It limits composability of various abstractions, as we cannot declare a function that must be called only within two or more scopes present at the same time.
- Etc
Context receivers to the rescue
Context Receivers are actually experimental and not enabled by default. To enable its usage, we need to go to the build.gradle.kts or build.gradle file of our module and add -Xcontext-receivers as a free compiler arg.
In the build.gradle file of an Android module, this looks something like this:
android { ... kotlinOptions { jvmTarget = '1.8' freeCompilerArgs = ["-Xcontext-receivers"] } ... }
Job Offers
Declaring context receivers
Let’s get back to our example of Modifier.align()
and try to introduce context receivers
object BoxScope {} context(BoxScope) fun Modifier.align(alignment: Alignment): Modifier { /*...*/ }
Instead of declaring our Modifier as a member extension function, we have declared it as a top level function and specified the scope in which it should be used.
Now since BoxScope doesn’t contain anything, so there’s no point in implementing it, I prefer to declare it directly as an
object
Our box remains almost the same, except that we call directly BoxScope.content()
@Composable fun Box( modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit ) { val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints) Layout( content = { BoxScope.content() }, measurePolicy = measurePolicy, modifier = modifier ) }
With that, we can, for example, have another scope that behaves like BoxScope, we won’t have to duplicate the code, we will just have to pass this scope as a receiver, we can even have a modifier for ColumnScope
context(BoxScope,AnOtherLikeBoxScope) fun Modifier.align(alignment: Alignment): Modifier { /*...*/ } context(ColumnScope) fun Modifier.align(alignment: Alignment): Modifier { /*...*/ }
Conclusion
Context receivers cover various use cases, there are several articles that give prerequisites with basic examples to help you understand this concept and its use cases. Personally, I was wondering what would be the application of context receivers in one of the codes I’ve already written or seen, and this article is meant to help all those who can find themselves in my shoes.
This article was originally published on proandroiddev.com on January 06, 2023