Blog Infos
Author
Published
Topics
Published
Topics

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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

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.

Sometimes in our code, we want to use something only in a certain scope, without which it won’t work properly.

This article was originally published on proandroiddev.com on January 06, 2023

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu