When using `@Composable operator fun invoke` with ...
# compose
b
When using
@Composable operator fun invoke
with interfaces - default arguments doesn't work. Is this a bug? Example in thread
Copy code
interface A {
    object B : A

    operator fun invoke(truth: Boolean = true, data: String = "Data") {
        assert(truth)
        assert(data == "Data")
    }
}

interface C {
    object D : C

    @Composable
    operator fun invoke(truth: Boolean = true, data: String = "Data") {
        assert(truth)
        assert(data == "Data")
    }
}


@Preview
@Composable
fun Test() {
    // doesn't crash
    A.B.invoke()
    // doesn't crash
    A.B()

    // doesn't crash
    C.D.invoke()
    // crashes with assertion error
    C.D()
}
z
What's the exception?
b
assertions fail,
truth
is false,
data
is null
z
But only D crashes, not B?
b
Yeah
z
And they're the same, right? I am not seeing a difference
b
Shoot, yeah. B shouldn't have
@Composable
z
Ah
Is Show Kotlin bytecode working for this? What's it showing for the body of Test?
z
There was a similar bug previously where default arguments in interfaces werent applied, e.g. a non nullable value with some relevant default would be null and crash when running the code. Its been fixed since, but perhaps still relevant for why this fails?
b
Copy code
LINENUMBER 382 L0
    GETSTATIC app/A$B.INSTANCE : Lapp/A$B;
    CHECKCAST app/A
    ICONST_0
    ACONST_NULL
    ICONST_3
    ACONST_NULL
    INVOKESTATIC app/A$DefaultImpls.invoke$default (Lapp/A;ZLjava/lang/String;ILjava/lang/Object;)V
   L1
    LINENUMBER 384 L1
    GETSTATIC app/A$B.INSTANCE : Lapp/A$B;
    CHECKCAST app/A
    ICONST_0
    ACONST_NULL
    ICONST_3
    ACONST_NULL
    INVOKESTATIC app/A$DefaultImpls.invoke$default (Lapp/A;ZLjava/lang/String;ILjava/lang/Object;)V
   L2
    LINENUMBER 387 L2
    GETSTATIC app/C$D.INSTANCE : Lapp/C$D;
    CHECKCAST app/C
    ICONST_0
    ACONST_NULL
    ICONST_3
    ACONST_NULL
    INVOKESTATIC app/C$DefaultImpls.invoke$default (Lapp/C;ZLjava/lang/String;ILjava/lang/Object;)V
   L3
    LINENUMBER 389 L3
    GETSTATIC app/C$D.INSTANCE : Lapp/C$D;
    CHECKCAST app/C
    ICONST_0
    ACONST_NULL
    ICONST_3
    ACONST_NULL
    INVOKESTATIC app/C$DefaultImpls.invoke$default (Lapp/C;ZLjava/lang/String;ILjava/lang/Object;)V
   L4
Body of Test
z
They look like the same call to me, weird. I'd say file a bug, prolly Leland or Chuck need to take a look
b
Seems it's like it's not related to interfaces. It fails for other things too (object, class, enum class)
Found existing issue here https://issuetracker.google.com/issues/232422226 @Leland Richardson [G]
s
l
yeah its a bug. looks like @Ben Trengrove [G] put up a fix for this this morning! https://android-review.googlesource.com/c/platform/frameworks/support/+/2107668
🎉 1
b
Changing
fun invoke
to
fun C.invoke
and adding more arguments that are
@Composable
lambdas also seems to work somehow.. While only adding
@Composable
lambdas result in runtime error (non-nullable param is null)
l
if you do
C.invoke(…)
the compiler looks at it like a normal function call. The
C()
pathway is an “operator call” and the code in the compose compiler was incorrectly assuming that in the latter pathway it would never have any default params
b
When I add
C.
to
@Composable operator fun invoke
to make it
@Composable operator fun C.invoke
, and call it like
C.D()
(which seems to be still an "operator call"?), it feels like it shouldn't be different calls?
l
you’re right, but i think that the extension receiver makes it go down yet another code path where the bug does not exist 🙂
b
And merged! Should be in the next release
🙏 3
b
I think I found a similar bug, but could need separate fix:
Copy code
sealed interface A {
    object B : A

    sealed interface E : A {
        object F : E
    }

    @Composable
    operator fun invoke(truth: Boolean = true, data: String = "Data") {
        assert(truth)
        assert(data == "Data")
    }
}

@Preview
@Composable
fun Test() {
    // doesn't crash
    A.B.invoke()

    // crashes with: checkNotNullParameter, parameter data
    A.E.F.invoke()
    A.E.F()

    // Crashes with already fixed assertion error (wrong defaults)
    A.B()
}
I see some another issue when I added a
minSize: Dp = Defaults.minSize()
field to
invoke
with totally different stacktrace:
Copy code
java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object kotlin.jvm.functions.Function3.invoke(java.lang.Object, java.lang.Object, java.lang.Object)' on a null object reference
Class structure is same as above but invoke has a receiver :
@Composable operator invoke A.invoke
b
I'm pretty sure it's the same bug even though it shows a different stack trace. Basically the compose compiler got the parameters wrong on invoke functions so you could get any number of exceptions from passing in incorrect parameters
Also, I just realised it didn't make the cutoff for this release so it will be in the one after next sorry
b
Gotcha. Hopefully all of them gets resolved with the fix.
@Ben Trengrove [G] looks like the fix wasn't included in compose compiler 1.2.0, right? I'm still seeing the issue in 1.2.0
l
Ben's change is in 1.2.0. can you provide a minimal repro?
đź‘€ 1
b
Copy code
sealed interface Button {
    object Primary : Button
    object Secondary : Button

    sealed interface Outlined : Button {
        object Primary : Outlined
        object Secondary : Outlined
    }

    @Composable
    operator fun invoke(
        onClick: () -> Unit,
        text: String,
        modifier: Modifier = Modifier,
    ) {
        androidx.compose.material3.Button(
            onClick = onClick,
            modifier = modifier,
        ) {
            Text(text = text)
        }
    }
}

@Composable
@Preview
fun ButtonsPreview() {
    Column {
        // doesn't crash
        Button.Primary(text = "Primary", onClick = {})
        Button.Secondary(text = "Secondary", onClick = {})
        // crashes
        Button.Outlined.Primary(text = "Outlined Primary", onClick = {})
        Button.Outlined.Secondary(text = "Outlined Secondary", onClick = {})
    }
}
^ It's crashing for
interface Outlined : Button
, but not
object Primary: Button
b
Whats the exception this time?
b
Modifier is null
đź‘Ť 1
b
I can reproduce it, it's very weird though. Both outlined and non-outlined resolve to the same invoke so it's weird one would crash and one wouldn't
l
this is also known issue but different. this is defaults in overridden methods. the fact that the method is invoke is irrelevent here
b
Changing the Outlined interface to a sealed class works as well
b
@Leland Richardson [G] "this is defaults in overridden methods." could you elaborate?
@Ben Trengrove [G] I think this is about being an interface, since it crashes with
interface
too and doesn't with object, abstract/sealed
class
b
Looks like this issue
l
yeah interface methods with default argument expressions dont work correctly right now. its actually a compiler error in many cases but looks like this case is not getting marked for aome reason? unless im misremembering