'How to make kotlin operator a Composable with parameters having default value
I'm trying to make Kotlin's invoke
operator a @Composable
, everything works fine, until I add a parameter to it, which should have a default value. See the code below:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent{
Button()
}
}
}
object Button{
@Composable
operator fun invoke(text: String = "SomeText"){
println(text) // prints: null
}
}
When the operator is not annotated as @Composable
the output is SomeText
, as it should be.
Is this some error in Jetpack Compose, or am I missing something?
The behavior is the same on the latest stable Compose v 1.1.1 and on 1.2.0-beta01. Kotlin 1.6.21
Solution 1:[1]
Based on the info provided in the comments, I decided to answer:
I'll maybe think of something better, but off the top of my head, this is what you can do for now
enum class ButtonType {
Primary,
Secondary,
Tertiary
}
Return the correct type of Button
@Composable
fun MasterButton(type: ButtonType) {
when(type) {
primary -> PrimaryButton()
secondary -> SecondaryButton()
else -> TertiaryButton() // Must provide an 'else' branch
}
}
This will do the job for you.
CORRECT APPROACH I:
I just got the correct one the moment I started typing the first approach.
@Composable
fun ( @Composable () -> Unit ).Primary(...) {
PrimaryButton()
}
Make copies for every other button.
STRONG NOTICE: This is a RIDICULOUS way of "cleaning" up the code. Nobody should ever use anything remotely resembling this ever, but since that is just what the question is about, this is how you go about doing it. Know that this will attach an extension function called Primary(...)
to every single @Composable
function, and that cannot change. You can't apply it to select Composable(s) only, since this is basically just an extension function that I have applied on a general labmda
, since 'extension functions for extension functions' are not something that exist as of now.
Solution 2:[2]
I am going to take this as your question (even though it is in the comments) and try to answer the way I achieve this.
What I'm trying to achieve is a way to clean up the namespace, so that not all Composables are available as a top-level function. The general idea is to group all flavors of let's say Buttons (Primary, Secondary, Tertiary) to be Composables declared as a function of object Button. But I would like to be able to use also this Button object as a default Button (let it be Primary) in a Compose way, so just by using it as it would be a function, thus invoke() operator. I would have Button.Primary(), Button.Secondary() and Button() which would be an "alias" for Button.Primary().
My implementation is quite simple,
- Expose only one top-level Composable function to have a cleaner namespace.
- Pass an argument that denotes the type of the required Composable, using a sealed class.
Button Type
sealed class MyIconButtonType(
open val typeName: String,
) {
data class Default(
override val typeName: String = "default",
) : MyIconButtonType(
typeName = typeName,
)
data class BorderIconButton(
override val typeName: String = "border",
// The variant specific attributes can be added here
val borderWidth: Int,
) : MyIconButtonType(
typeName = typeName,
)
}
Button (The only composable exposed to other files)
@Composable
fun MyTestIconButton(
onClickLabel: String,
modifier: Modifier = Modifier,
data: MyIconButtonType = MyIconButtonType.Default(),
onClick: () -> Unit,
content: @Composable () -> Unit,
) {
when (data) {
is MyIconButtonType.Default -> {
// This composable should be private
MyTestIconDefaultButton(
// parameter as required
)
}
is MyIconButtonType.BorderIconButton -> {
// This composable should be private
MyTestIconDefaultButton(
// parameter as required, also make sure to pass variant specific attributes here
)
}
}
}
Usage
// For default impl
MyTestIconButton(
// default parameters
) {
}
// For specific variants
MyTestIconButton(
// default parameters
data = MyIconButtonType.BorderIconButton(
borderWidth = 10,
),
) {
}
Note:
- Data class requires at least one attribute. Use object if no attributes like the
typeName
are required.
Like this,
sealed class MyIconButtonType {
object Default : MyIconButtonType()
data class BorderIconButton(
val borderWidth: Int,
) : MyIconButtonType()
}
Kotlin concepts that are used for reference,
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Richard Onslow Roper |
Solution 2 | Richard Onslow Roper |