cashapp / molecule

Build a StateFlow stream using Jetpack Compose
https://cashapp.github.io/molecule/docs/1.x/
Apache License 2.0
1.87k stars 81 forks source link

Method Resolution failing across modules when @Composable fun has a @Composable lambda and called within launchMolecule #91

Open steve-the-edwards opened 2 years ago

steve-the-edwards commented 2 years ago

Reproducing project is here: https://github.com/steve-the-edwards/reproductions/tree/main/overridetest

abstract class AndroidLibAbstractClass<I1, I2, T> {
  @Composable
  abstract fun AComposableWithLambda(
    input1: I1,
    input2: I2,
    hoistState: @Composable (T) -> Unit
  ): Unit

  @Composable
  abstract fun AComposableWithoutLambda(
    input1: I1,
    input2: I2,
  ): Unit
}

@Composable fun <I1, I2, T> AndroidLibComposableWithLambda(
  i1: I1,
  i2: I2,
  objectWithComposables: AndroidLibAbstractClass<I1, I2, T>
): T? {
  val payload: MutableState<T?> = remember { mutableStateOf(null) }
  objectWithComposables.AComposableWithLambda(i1, i2) @Composable {
    payload.value = it
  }
  return payload.value
}

@Composable fun <I1, I2, T> AndroidLibComposableWithoutLambda(
  i1: I1,
  i2: I2,
  objectWithComposables: AndroidLibAbstractClass<I1, I2, T>
): Unit {
  objectWithComposables.AComposableWithoutLambda(i1, i2)
}

Are defined in android-lib-module.

In the app module, a concrete child class of AndroidLibAbstractClass can be used successfully in a Compose UI composition.

However, in the launchMolecule composition (see MethodResolutionTest) this fails on AndroidLibComposableWithLambda:

  private class AndroidAppConcreteTestClass(
    private val payload: String
  ) : AndroidLibAbstractClass<Unit, String, String>() {
    @Composable
    public override fun AComposableWithLambda(
      input1: Unit,
      input2: String,
      hoistState: @Composable (s: String) -> Unit
    ) {
      println("Can you hear me now? $payload")
      hoistState(payload + input2)
    }

    @Composable
    override fun AComposableWithoutLambda(
      input1: Unit,
      input2: String
    ) {
    }
  }

  @Test fun testMethodResolution() {
    val objectUnderTest = AndroidAppConcreteTestClass("a test")
    val broadcastFrameClock = BroadcastFrameClock {}
    val testScope = CoroutineScope(broadcastFrameClock)

    val testFlow = testScope.launchMolecule {
      AndroidLibComposableWithoutLambda(Unit, " again", objectUnderTest)
      AndroidLibComposableWithLambda(Unit, " again", objectUnderTest)
    }

    assert(testFlow.value.contentEquals("a test again"))
  }

because it gets an AbstractMethodError as it cannot resolve the concrete class's override at runtime?

Originally I thought it might be because I was using the 0.3.0-SNAPSHOT and the common KMP artifacts so I include a module with that example as well, but I was able to reproduce it with just 0.2.0 while all in Android/JVM.

JakeWharton commented 2 years ago

Surely this bug lies in the Compose compiler. I cannot see any way in which the code from Molecule could cause this. We're not involved in the compiler at all beyond telling it to apply the normal AndroidX Compose compiler plugin.

I don't have time to dig into it today, though.

steve-the-edwards commented 2 years ago

I thought so too but could not reproduce in stock Compose.

JakeWharton commented 2 years ago

Yeah I mean your repro shows that it's something to do with our setup, but I have a hard time envisioning what it is intuitively.