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 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
}
vide
09/29/2023, 1:44 PMSkippable
, but adding @Stable
to either UnstableClass or SomeClass will allow skipping Skippable again. (Or removing private val unstableClass: UnstableClass? = null
from SomeClass)vide
09/29/2023, 1:45 PMvide
09/29/2023, 1:45 PMdewildte
09/29/2023, 2:03 PMdewildte
09/29/2023, 2:05 PMvide
09/29/2023, 2:06 PMvide
09/29/2023, 2:07 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 PMdewildte
09/29/2023, 2:09 PMvide
09/29/2023, 2:09 PMvide
09/29/2023, 2:16 PMvide
09/29/2023, 2:17 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 stablevide
09/29/2023, 2:27 PMvide
09/29/2023, 7:06 PMmethod
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 PMvide
10/03/2023, 12:02 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 AMshikasd
10/04/2023, 9:32 AMvide
10/04/2023, 9:34 AMvide
10/04/2023, 9:35 AMshikasd
10/04/2023, 9:38 AMshikasd
10/04/2023, 9:44 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.vide
10/05/2023, 7:37 AMvide
10/05/2023, 8:01 AMshikasd
10/05/2023, 10:03 AMvide
10/05/2023, 10:06 AMshikasd
10/05/2023, 10:11 AMvide
11/08/2023, 8:05 PMvide
11/08/2023, 8:06 PMshikasd
11/09/2023, 4:11 AMvide
11/13/2023, 8:12 AMshikasd
11/13/2023, 9:37 AMshikasd
11/13/2023, 9:38 AMvide
11/13/2023, 9:38 AM