Status Update
Comments
jo...@google.com <jo...@google.com> #2
We might just need to implement equals and hashcode for the modifier
mo...@google.com <mo...@google.com> #3
Hi, I can't reproduce this behavior. It is possible that the composable in your code is recomposing because of a different reason. Is the tag you pass for example read from a class variable? That would cause your composable to be recomposed because the compiler cannot infer that all input to the testTag is @Stable.
The test below shows that adding testTag
does not prevent Compose to skip the composable:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.delay
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TestTagTest {
@get:Rule
val rule = createComposeRule()
@Stable
private class RecompositionCounter {
var withoutTag = 0
var withTag = 0
}
@Test
fun test() {
val counter = RecompositionCounter()
rule.setContent {
val state = produceState(initialValue = 0) {
repeat(10) {
delay(500)
value++
}
}
Column {
Text(text = "value: ${state.value}")
MyComponent(modifier = Modifier.width(200.dp), "Data1", counter)
MyComponent(modifier = Modifier.width(200.dp).testTag("Data2"), "Data2", counter)
}
}
repeat(10) {
rule.waitForIdle()
rule.mainClock.advanceTimeBy(500)
}
rule.onNodeWithText("value: 10").assertExists()
assertThat(counter.withoutTag).isEqualTo(1)
assertThat(counter.withTag).isEqualTo(1)
}
@Composable
private fun MyComponent(modifier: Modifier = Modifier, text: String, rc: RecompositionCounter) {
if (text == "Data1") {
rc.withoutTag++
} else if (text == "Data2") {
rc.withTag++
}
Text(modifier = modifier, text = text)
}
}
je...@google.com <je...@google.com> #4
Hi,
I have change the code a bit so I can use it outside a test on a ComponentActivity.
Here the code:
package de.telekom.testapp
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
@Stable
private data class RecompositionCounter(
var withoutTag: Int = 0,
var withTag: Int = 0
)
@Composable
fun TestModifier() {
val counter = remember { RecompositionCounter() }
val state = produceState(initialValue = 0) {
repeat(10) {
delay(500)
value++
}
}
Column {
Text(text = "value: ${state.value} $counter")
MyComponent(modifier = Modifier.width(200.dp), "Data1", counter)
MyComponent(
modifier = Modifier
.width(200.dp)
.testTag("Data2"), "Data2", counter
)
}
}
@Composable
private fun MyComponent(modifier: Modifier = Modifier, text: String, rc: RecompositionCounter) {
if (text == "Data1") {
rc.withoutTag++
} else if (text == "Data2") {
rc.withTag++
}
Text(modifier = modifier, text = text)
}
And the counter "withTag" still increments. Attached you find a screenshot after 10 repeats.
jo...@google.com <jo...@google.com> #5
Additional info
compose version: '1.1.0-beta02' gradle: gradle-7.2-all Emulator: API 26 x86
ap...@google.com <ap...@google.com> #6
Hmm, this is odd. I can reproduce the bug when using the latest snapshot build of Compose from androidx.dev (
ap...@google.com <ap...@google.com> #7
Here is what happened:
The Live Literals feature in Android Studio switches out literals with state values. This will influence static inference by the compiler.
Without Live Literals on, the compiler will infer that Modifier.testTag("tag")
is only using static input and will not even call equals on it to verify if the value has changed (and consequently if parts of the code can be skipped); it knows that it will never change. With Live Literals on however, Modifier.testTag("tag")
is no longer considered static and during composition Modifier.testTag("tag") == Modifier.testTag("tag")
will be false, resulting in not skipping that part of the code.
This is a consequence of the underlying Modifier.composed(..)
, which is not equal to another Modifier.composed(..)
with exactly the same lambda.
For now, you can turn off Live Literals by adding @file:NoLiveLiterals
to the impacted files when benchmarking compositions. Also, in release builds Live Literals will always be turned off.
I filed
je...@google.com <je...@google.com> #8
In release builds or with @file:NoLiveLiterals it works.
Thanks for the details.
Description
The following Experimental APIs have existed for several releases.
Please consider stabilising or removing these APIs: