https://kotlinlang.org logo
#compose
Title
# compose
b

Berkeli Alashov

05/27/2022, 4:35 AM
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

Zach Klippenstein (he/him) [MOD]

05/27/2022, 4:38 AM
What's the exception?
b

Berkeli Alashov

05/27/2022, 4:39 AM
assertions fail,
truth
is false,
data
is null
z

Zach Klippenstein (he/him) [MOD]

05/27/2022, 4:43 AM
But only D crashes, not B?
b

Berkeli Alashov

05/27/2022, 4:44 AM
Yeah
z

Zach Klippenstein (he/him) [MOD]

05/27/2022, 4:45 AM
And they're the same, right? I am not seeing a difference
b

Berkeli Alashov

05/27/2022, 4:46 AM
Shoot, yeah. B shouldn't have
@Composable
z

Zach Klippenstein (he/him) [MOD]

05/27/2022, 4:46 AM
Ah
Is Show Kotlin bytecode working for this? What's it showing for the body of Test?
z

Zoltan Demant

05/27/2022, 5:10 AM
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

Berkeli Alashov

05/27/2022, 5:12 AM
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

Zach Klippenstein (he/him) [MOD]

05/27/2022, 5:16 AM
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

Berkeli Alashov

05/27/2022, 5:23 AM
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

ste

05/27/2022, 8:17 AM
l

Leland Richardson [G]

05/27/2022, 3:46 PM
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

Berkeli Alashov

05/27/2022, 6:39 PM
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

Leland Richardson [G]

05/27/2022, 7:12 PM
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

Berkeli Alashov

05/27/2022, 8:34 PM
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

Leland Richardson [G]

05/27/2022, 8:45 PM
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

Ben Trengrove [G]

05/27/2022, 11:42 PM
And merged! Should be in the next release
🙏 3
b

Berkeli Alashov

05/28/2022, 12:44 AM
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

Ben Trengrove [G]

05/31/2022, 12:34 AM
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

Berkeli Alashov

05/31/2022, 12:37 AM
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

Leland Richardson [G]

07/02/2022, 2:22 AM
Ben's change is in 1.2.0. can you provide a minimal repro?
👀 1
b

Berkeli Alashov

07/02/2022, 2:50 AM
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

Ben Trengrove [G]

07/03/2022, 10:18 PM
Whats the exception this time?
b

Berkeli Alashov

07/03/2022, 10:19 PM
Modifier is null
👍 1
b

Ben Trengrove [G]

07/03/2022, 10:40 PM
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

Leland Richardson [G]

07/03/2022, 10:41 PM
this is also known issue but different. this is defaults in overridden methods. the fact that the method is invoke is irrelevent here
b

Ben Trengrove [G]

07/03/2022, 10:42 PM
Changing the Outlined interface to a sealed class works as well
b

Berkeli Alashov

07/03/2022, 10:44 PM
@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

Ben Trengrove [G]

07/03/2022, 11:02 PM
Looks like this issue
l

Leland Richardson [G]

07/03/2022, 11:14 PM
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
58 Views