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>
, useFlow<T>
to use with allFlow
derivates such asSharedFlow
andStateFlow
.
Tip: To use with
Activity
, replaceFragment
withActivity
as the receiver in the code, and removeviewLifecycleOwner
since it's not necessary with anActivity
.
Tip: Compose already has an API for this. See here.
Arrivederci!