Context Receivers in Kotlin: An Example

Context Receivers in Kotlin: An Example

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.

Extension Functions

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.

Limitation: Single 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?

Context Receivers

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.

Best version of our code using Context Receivers

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>, use Flow<T> to use with all Flow derivates such as SharedFlow and StateFlow.


Tip: To use with Activity, replace Fragment with Activity as the receiver in the code, and remove viewLifecycleOwner since it's not necessary with an Activity.


Tip: Compose already has an API for this. See here.

Arrivederci!