I'm creating a Row of Text Composables, imagine it like a table row.
Image: https://i.sstatic.net/jR3oB.png
If you care for the backstory:
The amount of "cells" of my custom Row Composable is dynamic and I use Box Composables to wrap the Text in the "cells". I need to apply different Modifier parameters to each Box, which I first tried to achieve via creating a Modifier extension.
Code of my Modifier extension:
@SuppressLint("ComposableModifierFactory", "ModifierFactoryExtensionFunction") // I wish I could. The RowScope requirement makes doing it the easy way impossible
@Composable
fun RowScope.rowOfTextCellsBoxModifier(
index: Int,
columnWidths: ArrayList<Dp?>,
columnWeights: ArrayList<Float?>
): Modifier {
@Suppress("RedundantExplicitType") // bull**** (compose kotlinCompilerExtensionVersion 1.3.2)
var returnModifier: Modifier = Modifier
if (index != columnWidths.size - 1)
returnModifier = returnModifier.then(Modifier.verticalEndLine())
returnModifier = if (columnWidths[index] != null)
returnModifier.then(Modifier.width(columnWidths[index]!!))
else {
val weight = try {
columnWeights[index]
} catch (ignored: Throwable) {
1f
}
returnModifier.then(Modifier.weight(weight!!))
}
return returnModifier
}
I moved on to just generating the entire Box in a @Composable function, but that's beside the point ;)
As you can see, I want to add a Modifier.weight() at one point.
However, .weight() is a Modifier function that is only applicable in certain Scopes, like in RowScope. Which means that my "Modifier extension" needs to apply to RowScope, which turned out impossible to find instructions on.
So, imagine I hadn't found a better solution:
What would be the proper way of extending the RowScope Interface's Modifier?
Usually, when you need to create a Composable that is applicable to certain scopes, you write something like:
RowScope.YourComposable() {...}
And when you extend Modifier, you write:
Modifier.yourExtension() {...}
But that does not work for Scoped Modifiers:
RowScope.Modifier.YourModifierExtension() {...}
is invalid code.
A viable experimental solution at the time of writing is to use the experimental Context Receiver to make the Modifier applicable to a specific Scope. Thanks to user @gpunto for the hint.
See this Github page for further details on the Context Receiver.
You can implement the experimental feature by adding "-Xcontext-receivers"
to the Kotlin compiler options. In an android project using Gradle (Kotlin DSL), this is done by adding it to the kotlinOptions "freeCompilerArgs" list:
kotlinOptions {
freeCompilerArgs = listOf("-Xcontext-receivers")
}
Usage for my example in the question is as follows:
context(RowScope) private fun Modifier.rowOfTextCellsBoxModifier(
index: Int,
columnWidths: ArrayList<Dp?>,
columnWeights: ArrayList<Float?>,
columTextsSize: Int
): Modifier {
return this
.then(if (index != columTextsSize - 1) Modifier.verticalEndLine() else Modifier)
.then(
if (columnWidths[index] != null)
Modifier.width(columnWidths[index]!!)
else {
val weight = try {
columnWeights[index]
} catch (ignored: Throwable) {
1f
}
Modifier.weight(weight!!)
}
)
}
What the context receiver does:
It allows the use of the .weight
modifier while the extension functions still targets the Modifier
interface as recommended.
This would also allow you to use the = composed {}
functionalities.
This solution results in the cleanest, least hacky code with zero lint complaints. Whether to use this in production is up to your judgement however.
Differences to the code example in my question:
This now uses Compose compiler version 1.4.3.
Instead of allocating a new instance of a Modifier, adding to it with .then()
and returning that allocated instance, the code now returns the Modifier receiver (this
) with conditionally applied Modifiers in .then()
calls.