vide
09/26/2023, 5:52 PMequals()
returns true
• compose metrics claim the composable is stable. How can this be?Jussi Malinen
09/26/2023, 6:13 PM{AudioLandingPageKt$AudioLandingPageWrapper$4@36054}
on the first request and {AudioLandingPageKt$AudioLandingPageWrapper$4@36073}
on second. When comparing the values in the function body, the == operation tells they are the same.dewildte
09/26/2023, 9:12 PMvide
09/29/2023, 1:42 PMclass UnstableClass(var publicVar: Int)
class SomeClass(private val unstableClass: UnstableClass? = null) {
fun method() {}
}
class MainActivity2 : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val cl = SomeClass()
setContent {
Root(cl)
}
}
}
@Composable
fun Root(someClass: SomeClass) {
Log.w("TEST", "Recomposing root")
var internalCounter by remember { mutableIntStateOf(0) }
Button(onClick = { internalCounter += 1 }) {
Text("Increment ($internalCounter)")
}
internalCounter // recompose this scope when changed
Skippable(someClass::method)
}
var previous: Any? = null
@Composable
fun Skippable(functionRef: () -> Unit) {
Log.w("TEST", "Recomposing while equals=${functionRef == previous}")
previous = functionRef
}
Skippable
, but adding @Stable
to either UnstableClass or SomeClass will allow skipping Skippable again. (Or removing private val unstableClass: UnstableClass? = null
from SomeClass)dewildte
09/29/2023, 2:03 PMvide
09/29/2023, 2:06 PM1) The result of [equals] will always return the same result for the same two instances.
2) When a public property of the type changes, composition will be notified.
3) All public property types are stable.
dewildte
09/29/2023, 2:07 PMvide
09/29/2023, 2:09 PMrestartable scheme("[androidx.compose.ui.UiComposable]") fun Root(
unstable someClass: SomeClass
)
restartable skippable fun Skippable(
stable functionRef: Function0<Unit>
)
and
unstable class UnstableClass {
stable var publicVar: Int
<runtime stability> = Unstable
}
unstable class SomeClass {
unstable val unstableClass: UnstableClass?
<runtime stability> = Unstable
}
stable class MainActivity2 {
<runtime stability> = Stable
}
seem to be the correct ones, so SomeClass still surprisingly seems to be unstable, but the function reference is inferred to be stablemethod
will never skip recomposition:
class StableClass {
fun method(arg: Int) {}
}
and this will skip recomposition:
class StableClass {
fun method() {}
}
dewildte
09/29/2023, 8:41 PMvide
09/30/2023, 2:18 PMZach Klippenstein (he/him) [MOD]
10/03/2023, 7:30 PMvide
10/04/2023, 6:58 AM===
) instead of .equals()
, which explains the behavior. Normally the compiler generates remember expressions around function references, but this is explicitly disabled for more-than-0 parameters: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]piler/plugins/kotlin/lower/ComposerLambdaMemoization.kt;l=449. I think this is still a bug, let's see if the issue can be reopened or if I should file a new bugAndrew Bailey
10/04/2023, 8:05 AM===
) to check whether a lambda has changed or not. And we infer that all lambdas are stable. We also implicitly memoize lambdas, so you'll get a new instance in composition if the dispatch receiver changes or if you get a new capture scope that requires us to make a new lambda for you. So that would explain how you're seeing an .equals()
lambda that changes during recomposition and is also stable.vide
10/04/2023, 9:27 AMshikasd
10/04/2023, 9:30 AMvide
10/04/2023, 9:34 AMshikasd
10/04/2023, 9:38 AMvalueArgumentCount
is correct is to debug our compiler test in androidx (e.g. LambdaMemoizationTransformTests
), might be a good point to startvide
10/04/2023, 6:47 PM// FunctionReferenceLowering.kt
...
private inner class FunctionReferenceBuilder(...)
...
private fun JvmIrBuilder.generateSignature(...)
...
IrFunctionReferenceImpl(
UNDEFINED_OFFSET, UNDEFINED_OFFSET, irFunctionReference.type, target,
irFunctionReference.typeArgumentsCount, target.owner.valueParameters.size,
irFunctionReference.reflectionTarget, null
)
shikasd
10/04/2023, 6:57 PMvalueArguments
are null then?
I am not sure why it has any, but it kinda makes sense. I still want to guard against remembering unexpected shapes, e.g. in cases where context receivers will be involved (they are represented as regular args)vide
10/05/2023, 6:48 AMgetValueArgument
always returns null for any index regardless of presence of context receivers. Should I just check for expression.symbol.owner.contextReceiverParametersCount == 0
?
We could also potentially check for stability of the context receivers and use those as additional keys to remember? I'm not sure how I would implement that though.shikasd
10/05/2023, 10:03 AMvide
10/05/2023, 10:06 AMshikasd
10/05/2023, 10:11 AMvide
11/08/2023, 8:05 PMshikasd
11/09/2023, 4:11 AMvide
11/13/2023, 8:12 AMshikasd
11/13/2023, 9:37 AMvide
11/13/2023, 9:38 AM