Context Receivers
Tags: kotlin, receiver, context, android
Published: 2022-07-26 09:55:00

On this third part we take a look at Kotlin Context Receivers and how it can help us, before we start putting every together in [part 4](https://dev.to/lex_fury/draw-the-labeled-range-slider-1771) and finish up our Labeled Range Slider in [part 5](https://dev.to/lex_fury/make-the-labeled-range-slider-interactive-5gf).

First let's see what a Context Receiver is, before we take a look at how it can help us make our component easier to use.

> **Important:** Kotlin Context Receiver is an experimental API, this means it could still be subject to change in the future, before a final release. Also the IDE support is currently limited as well.

## Enabling Context Receivers
Because of the experimental nature of Context Receivers, they are 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:

```Groovy
android {
    ...
    kotlinOptions {
        jvmTarget = '1.8'
        freeCompilerArgs = ["-Xcontext-receivers"]
    }
}

```

## How it works
With Context Receivers we can add one or more contexts to our functions or methods. This provides the functionality of the declared context within our function, like having another **this** in that function. We are then able to call methods of that context as if our function is part of the object.

At a first glance this sounds a little bit like extension functions, similar to

```Kotlin
fun String.toast(activity: Activity, duration: Int = Toast.LENGTH_LONG) {
	Toast.makeText(activity, this, duration).show()
}
```

What you can see in this function is, that we also need an Android Activity to call the **Toast.makeText** function, therefore we need to call the function like this:

```Kotlin
"Hello Toast".toast(this@MainActivity)
```

We know we only want to call this function from an Android Activity, since this shows an UI element. What we can do with Context Receiver, we can declare our **toast** function to be callable within the **Scope** of an Android Activity.

```Kotlin
context(Activity)
fun String.toast(duration: Int = Toast.LENGTH_LONG) {
	Toast.makeText(this@Activity, this, duration).show()
}

```

As you can see, we removed the parameter Activity and instead added a Context Receiver, saying this function is valid within the **Scope** of an Activity. This allows us to call **toast** without passing an Activity directly, as long as we are within the **Scope** of one.

```Kotlin
"Hello Toast".toast()
```

On top of that our function now communicates to our team members, that it is only allowed to call this function in an Activity and not e.g. in a background service.

One question that comes up is, what happens when we are not within the **Scope** of an object needed as Context Receiver? 
Just as an example, lets change our **toast** function to have an Activity and a String as Context Receivers

```Kotlin
context(Activity, String)
fun toast(duration: Int = Toast.LENGTH_LONG) {
	Toast.makeText(this@Activity, this@String, duration).show()
}
```

Now we need our String also to be in **Scope**. We can achieve this by using Kotlin's **with** function.

```Kotlin
with("Hello Context") {
    toast()
}
```

Sure, this is a little bit of an odd example, but it demonstrates how we can create a **Scope** and also how to use multiple Context Receivers :-).


## How does this help us?
For our Labeled Range Slider, Context Receivers can help us makes our API cleaner and simpler to use for the caller and for us.

To start simple, we want to draw a text on a canvas and give the user the ability to configure the text size, as well as the horizontal and vertical translation of the positioning. In our Composable we need pixel values to position and draw the text. But we don't want the callers of our Composable to have to calculate pixel values by themselves and offer them an API which can be used with **Dp** and/or **Sp** values.
For that we create a configuration class **TextConfig**, holding the configurable properties and give them some defaults.

```Kotlin
class TextConfig(
...
	private val verticalTranslation: Dp = 0.dp,
	private val horizontalTranslation: Dp = 0.dp,
	private val size: TextUnit = 8.sp,
...
)
```

To be able to convert **Dp** or **Sp** values into pixels, we can use **toPx**, as we already saw in [part two](https://dev.to/lex_fury/touch-interactions-in-jetpack-compose-5be9) of this series. The function **toPx** is only available while we are within the **DrawScope** of a canvas. Therefore we could either call **toPx** manually every time in our canvas or we can use Context Receivers and create a nice computed properties, making it easier to use.

```Kotlin
class TextConfig(
...
	private val verticalTranslation: Dp = 0.dp,
	private val horizontalTranslation: Dp = 0.dp,
	private val size: TextUnit = 8.sp,
...
) {
	context(DrawScope)
	val sizePx: Float
		get() = size.toPx()

	context(DrawScope)
	val verticalTranslationPx: Float
		get() = verticalTranslation.toPx()

	context(DrawScope)
	val horizontalTranslationPx: Float
		get() = horizontalTranslation.toPx()
}
```

With that we can implement our Composable as follows:

```Kotlin
@Composable
fun DrawText(
    textConfig: TextConfig
) {

    var enteredText by remember { mutableStateOf("") }

    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        TextField(
            value = enteredText,
            onValueChange = { enteredText = it }
        )
        Canvas(
            modifier = Modifier
                         .fillMaxSize()
                         .background(color = Color.DarkGray)
        ) {
            val paint = Paint().apply {
                textSize = textConfig.sizePx
            }
            val x = (size.width / 2f) - (enteredText.length / 4f * textConfig.sizePx) + textConfig.horizontalTranslationPx
            val y = size.height / 2f + textConfig.sizePx / 2f + textConfig.verticalTranslationPx			

            drawIntoCanvas {	
                it.nativeCanvas.drawText(
                    enteredText,
                    x,
                    y,
                    paint
                )
            }
		}
	}
}
```
We can see, that as long as we are in the **Scope** of the Canvas we can call our properties, bounds to the **DrawScope**, because the lambda of the Canvas provides the **DrawScope** as an extension function parameter and there as **this**.

The user of the Composable can provide their configuration with device independent values.

```Kotlin
val config = TextConfig(
    size = 32.sp,
    verticalTranslation = 16.dp,
    horizontalTranslation = 32.dp,
)
DrawText(
    textConfig = config
) 
```

## Conclusion
With the Context Receivers we can provide a nice API to our users, while also making our lives easier for the implementation of our Composable.

All the code shown here and a little bit more can be found [on GitHub](https://github.com/a-frank/composable-and-context-receivers).

Now that we know how to draw elements on a canvas, interactive with them using gesture detection and also are able to create a nice simple API, we got all parts ready to implement our Labeled Range Slider. 

[Go to the fourth part right away](https://dev.to/lex_fury/draw-the-labeled-range-slider-1771)