[go: nahoru, domu]

blob: f3a63ae74606df2f24359083a6a9173d3a04c79b [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.layout
import android.annotation.SuppressLint
import android.os.Build
import android.widget.FrameLayout
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ReusableContent
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.background
import androidx.compose.ui.composed
import androidx.compose.ui.draw.assertColor
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@MediumTest
@RunWith(AndroidJUnit4::class)
class SubcomposeLayoutTest {
@get:Rule
val rule = createAndroidComposeRule<TestActivity>()
@get:Rule
val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
@Test
fun useSizeOfTheFirstItemInSecondSubcomposition() {
val firstTag = "first"
val secondTag = "second"
rule.setContent {
SubcomposeLayout { constraints ->
val first = subcompose(0) {
Spacer(Modifier.requiredSize(50.dp).testTag(firstTag))
}.first().measure(constraints)
// it is an input for the second subcomposition
val halfFirstSize = (first.width / 2).toDp()
val second = subcompose(1) {
Spacer(Modifier.requiredSize(halfFirstSize).testTag(secondTag))
}.first().measure(constraints)
layout(first.width, first.height) {
first.place(0, 0)
second.place(first.width - second.width, first.height - second.height)
}
}
}
rule.onNodeWithTag(firstTag)
.assertPositionInRootIsEqualTo(0.dp, 0.dp)
.assertWidthIsEqualTo(50.dp)
.assertHeightIsEqualTo(50.dp)
rule.onNodeWithTag(secondTag)
.assertPositionInRootIsEqualTo(25.dp, 25.dp)
.assertWidthIsEqualTo(25.dp)
.assertHeightIsEqualTo(25.dp)
}
@Test
fun subcomposeMultipleLayoutsInOneSlot() {
val firstTag = "first"
val secondTag = "second"
val layoutTag = "layout"
rule.setContent {
SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
val placeables = subcompose(Unit) {
Spacer(Modifier.requiredSize(50.dp).testTag(firstTag))
Spacer(Modifier.requiredSize(30.dp).testTag(secondTag))
}.map {
it.measure(constraints)
}
val maxWidth = placeables.maxByOrNull { it.width }!!.width
val height = placeables.sumOf { it.height }
layout(maxWidth, height) {
placeables.fold(0) { top, placeable ->
placeable.place(0, top)
top + placeable.height
}
}
}
}
rule.onNodeWithTag(firstTag)
.assertPositionInRootIsEqualTo(0.dp, 0.dp)
.assertWidthIsEqualTo(50.dp)
.assertHeightIsEqualTo(50.dp)
rule.onNodeWithTag(secondTag)
.assertPositionInRootIsEqualTo(0.dp, 50.dp)
.assertWidthIsEqualTo(30.dp)
.assertHeightIsEqualTo(30.dp)
rule.onNodeWithTag(layoutTag)
.assertWidthIsEqualTo(50.dp)
.assertHeightIsEqualTo(80.dp)
}
@Test
fun recompositionDeepInsideTheSlotDoesntRecomposeUnaffectedLayerOrRemeasure() {
val model = mutableStateOf(0)
var measuresCount = 0
var recompositionsCount1 = 0
var recompositionsCount2 = 0
rule.setContent {
SubcomposeLayout { constraints ->
measuresCount++
val placeable = subcompose(Unit) {
recompositionsCount1++
NonInlineBox(Modifier.requiredSize(20.dp)) {
model.value // model read
recompositionsCount2++
}
}.first().measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
rule.runOnIdle { model.value++ }
rule.runOnIdle {
assertEquals(1, measuresCount)
assertEquals(1, recompositionsCount1)
assertEquals(2, recompositionsCount2)
}
}
@Composable
private fun NonInlineBox(modifier: Modifier, content: @Composable () -> Unit) {
Box(modifier = modifier) { content() }
}
@Test
fun recompositionOfTheFirstSlotDoestAffectTheSecond() {
val model = mutableStateOf(0)
var recompositionsCount1 = 0
var recompositionsCount2 = 0
rule.setContent {
SubcomposeLayout {
subcompose(1) {
recompositionsCount1++
model.value // model read
}
subcompose(2) {
recompositionsCount2++
}
layout(100, 100) {
}
}
}
rule.runOnIdle { model.value++ }
rule.runOnIdle {
assertEquals(2, recompositionsCount1)
assertEquals(1, recompositionsCount2)
}
}
@Test
fun addLayoutOnlyAfterRecomposition() {
val addChild = mutableStateOf(false)
val childTag = "child"
val layoutTag = "layout"
rule.setContent {
SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
val placeables = subcompose(Unit) {
if (addChild.value) {
Spacer(Modifier.requiredSize(20.dp).testTag(childTag))
}
}.map { it.measure(constraints) }
val size = placeables.firstOrNull()?.width ?: 0
layout(size, size) {
placeables.forEach { it.place(0, 0) }
}
}
}
rule.onNodeWithTag(layoutTag)
.assertWidthIsEqualTo(0.dp)
.assertHeightIsEqualTo(0.dp)
rule.onNodeWithTag(childTag)
.assertDoesNotExist()
rule.runOnIdle {
addChild.value = true
}
rule.onNodeWithTag(layoutTag)
.assertWidthIsEqualTo(20.dp)
.assertHeightIsEqualTo(20.dp)
rule.onNodeWithTag(childTag)
.assertWidthIsEqualTo(20.dp)
.assertHeightIsEqualTo(20.dp)
}
@Test
fun providingNewLambdaCausingRecomposition() {
val content = mutableStateOf<@Composable () -> Unit>({
Spacer(Modifier.requiredSize(10.dp))
})
rule.setContent {
MySubcomposeLayout(content.value)
}
val updatedTag = "updated"
rule.runOnIdle {
content.value = {
Spacer(Modifier.requiredSize(10.dp).testTag(updatedTag))
}
}
rule.onNodeWithTag(updatedTag)
.assertIsDisplayed()
}
@Composable
private fun MySubcomposeLayout(content: @Composable () -> Unit) {
SubcomposeLayout { constraints ->
val placeables = subcompose(Unit, content).map { it.measure(constraints) }
val maxWidth = placeables.maxByOrNull { it.width }!!.width
val height = placeables.sumOf { it.height }
layout(maxWidth, height) {
placeables.forEach { it.place(0, 0) }
}
}
}
@Test
fun notSubcomposedSlotIsDisposed() {
val addSlot = mutableStateOf(true)
var composed = false
var disposed = false
rule.setContent {
SubcomposeLayout {
if (addSlot.value) {
subcompose(Unit) {
DisposableEffect(Unit) {
composed = true
onDispose { }
}
DisposableEffect(Unit) {
onDispose {
disposed = true
}
}
}
}
layout(10, 10) {}
}
}
rule.runOnIdle {
assertThat(composed).isTrue()
assertThat(disposed).isFalse()
addSlot.value = false
}
rule.runOnIdle {
assertThat(disposed).isTrue()
}
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun slotsAreDrawnInTheOrderTheyComposed() {
val layoutTag = "layout"
rule.setContent {
SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
val first = subcompose(Color.Red) {
Spacer(Modifier.requiredSize(10.dp).background(Color.Red))
}.first().measure(constraints)
val second = subcompose(Color.Green) {
Spacer(Modifier.requiredSize(10.dp).background(Color.Green))
}.first().measure(constraints)
layout(first.width, first.height) {
first.place(0, 0)
second.place(0, 0)
}
}
}
rule.waitForIdle()
rule.onNodeWithTag(layoutTag)
.captureToImage()
.assertCenterPixelColor(Color.Green)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun slotsCouldBeReordered() {
val layoutTag = "layout"
val firstSlotIsRed = mutableStateOf(true)
rule.setContent {
SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
val firstColor = if (firstSlotIsRed.value) Color.Red else Color.Green
val secondColor = if (firstSlotIsRed.value) Color.Green else Color.Red
val first = subcompose(firstColor) {
Spacer(Modifier.requiredSize(10.dp).background(firstColor))
}.first().measure(constraints)
val second = subcompose(secondColor) {
Spacer(Modifier.requiredSize(10.dp).background(secondColor))
}.first().measure(constraints)
layout(first.width, first.height) {
first.place(0, 0)
second.place(0, 0)
}
}
}
rule.onNodeWithTag(layoutTag)
.captureToImage()
.assertCenterPixelColor(Color.Green)
rule.runOnIdle {
firstSlotIsRed.value = false
}
rule.onNodeWithTag(layoutTag)
.captureToImage()
.assertCenterPixelColor(Color.Red)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun drawingOrderCouldBeChangedUsingZIndex() {
val layoutTag = "layout"
rule.setContent {
SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
val first = subcompose(Color.Red) {
Spacer(Modifier.requiredSize(10.dp).background(Color.Red).zIndex(1f))
}.first().measure(constraints)
val second = subcompose(Color.Green) {
Spacer(Modifier.requiredSize(10.dp).background(Color.Green))
}.first().measure(constraints)
layout(first.width, first.height) {
first.place(0, 0)
second.place(0, 0)
}
}
}
rule.onNodeWithTag(layoutTag)
.captureToImage()
.assertCenterPixelColor(Color.Red)
}
@Test
fun slotsAreDisposedWhenLayoutIsDisposed() {
val addLayout = mutableStateOf(true)
var firstDisposed = false
var secondDisposed = false
rule.setContent {
if (addLayout.value) {
SubcomposeLayout {
subcompose(0) {
DisposableEffect(Unit) {
onDispose {
firstDisposed = true
}
}
}
subcompose(1) {
DisposableEffect(Unit) {
onDispose {
secondDisposed = true
}
}
}
layout(10, 10) {}
}
}
}
rule.runOnIdle {
assertThat(firstDisposed).isFalse()
assertThat(secondDisposed).isFalse()
addLayout.value = false
}
rule.runOnIdle {
assertThat(firstDisposed).isTrue()
assertThat(secondDisposed).isTrue()
}
}
@Test
fun propagatesDensity() {
rule.setContent {
val size = 50.dp
val density = Density(3f)
val sizeIpx = with(density) { size.roundToPx() }
CompositionLocalProvider(LocalDensity provides density) {
SubcomposeLayout(
Modifier.requiredSize(size).onGloballyPositioned {
assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
}
) { constraints ->
layout(constraints.maxWidth, constraints.maxHeight) {}
}
}
}
rule.waitForIdle()
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun drawingOrderIsControlledByPlaceCalls() {
val layoutTag = "layout"
rule.setContent {
SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
val first = subcompose(Color.Red) {
Spacer(Modifier.requiredSize(10.dp).background(Color.Red))
}.first().measure(constraints)
val second = subcompose(Color.Green) {
Spacer(Modifier.requiredSize(10.dp).background(Color.Green))
}.first().measure(constraints)
layout(first.width, first.height) {
second.place(0, 0)
first.place(0, 0)
}
}
}
rule.waitForIdle()
rule.onNodeWithTag(layoutTag)
.captureToImage()
.assertCenterPixelColor(Color.Red)
}
@Test
@LargeTest
fun viewWithSubcomposeLayoutCanBeDetached() {
// verifies that the View with composed SubcomposeLayout can be detached at any point of
// time without runtime crashes and once the view will be attached again the change will
// be applied
val scenario = rule.activityRule.scenario
lateinit var container1: FrameLayout
lateinit var container2: ComposeView
val state = mutableStateOf(10.dp)
var stateUsedLatch = CountDownLatch(1)
scenario.onActivity {
container1 = FrameLayout(it)
container2 = ComposeView(it)
it.setContentView(container1)
container1.addView(container2)
container2.setContent {
SubcomposeLayout { constraints ->
val first = subcompose(Unit) {
stateUsedLatch.countDown()
Box(Modifier.requiredSize(state.value))
}.first().measure(constraints)
layout(first.width, first.height) {
first.place(0, 0)
}
}
}
}
assertTrue("state was used in setup", stateUsedLatch.await(1, TimeUnit.SECONDS))
stateUsedLatch = CountDownLatch(1)
scenario.onActivity {
state.value = 15.dp
container1.removeView(container2)
}
// The subcomposition is allowed to be active while the View is detached,
// but it isn't required
rule.waitForIdle()
scenario.onActivity {
container1.addView(container2)
}
assertTrue(
"state was used after reattaching view",
stateUsedLatch.await(1, TimeUnit.SECONDS)
)
}
@Test
fun precompose() {
val addSlot = mutableStateOf(false)
var composingCounter = 0
var composedDuringMeasure = false
val state = SubcomposeLayoutState()
val content: @Composable () -> Unit = {
composingCounter++
}
rule.setContent {
SubcomposeLayout(state) {
if (addSlot.value) {
composedDuringMeasure = true
subcompose(Unit, content)
}
layout(10, 10) {}
}
}
rule.runOnIdle {
assertThat(composingCounter).isEqualTo(0)
state.precompose(Unit, content)
}
rule.runOnIdle {
assertThat(composingCounter).isEqualTo(1)
assertThat(composedDuringMeasure).isFalse()
addSlot.value = true
}
rule.runOnIdle {
assertThat(composedDuringMeasure).isTrue()
assertThat(composingCounter).isEqualTo(1)
}
}
@Test
fun disposePrecomposedItem() {
var composed = false
var disposed = false
val state = SubcomposeLayoutState()
rule.setContent {
SubcomposeLayout(state) {
layout(10, 10) {}
}
}
val slot = rule.runOnIdle {
state.precompose(Unit) {
DisposableEffect(Unit) {
composed = true
onDispose {
disposed = true
}
}
}
}
rule.runOnIdle {
assertThat(composed).isTrue()
assertThat(disposed).isFalse()
slot.dispose()
}
rule.runOnIdle {
assertThat(disposed).isTrue()
}
}
@Test
fun composeItemRegularlyAfterDisposingPrecomposedItem() {
val addSlot = mutableStateOf(false)
var composingCounter = 0
var enterCounter = 0
var exitCounter = 0
val state = SubcomposeLayoutState()
val content: @Composable () -> Unit = @Composable {
composingCounter++
DisposableEffect(Unit) {
enterCounter++
onDispose {
exitCounter++
}
}
}
rule.setContent {
SubcomposeLayout(state) {
if (addSlot.value) {
subcompose(Unit, content)
}
layout(10, 10) {}
}
}
val slot = rule.runOnIdle {
state.precompose(Unit, content)
}
rule.runOnIdle {
slot.dispose()
}
rule.runOnIdle {
assertThat(composingCounter).isEqualTo(1)
assertThat(enterCounter).isEqualTo(1)
assertThat(exitCounter).isEqualTo(1)
addSlot.value = true
}
rule.runOnIdle {
assertThat(composingCounter).isEqualTo(2)
assertThat(enterCounter).isEqualTo(2)
assertThat(exitCounter).isEqualTo(1)
}
}
@Test
fun precomposeTwoItems() {
val addSlots = mutableStateOf(false)
var composing1Counter = 0
var composing2Counter = 0
val state = SubcomposeLayoutState()
val content1: @Composable () -> Unit = {
composing1Counter++
}
val content2: @Composable () -> Unit = {
composing2Counter++
}
rule.setContent {
SubcomposeLayout(state) {
subcompose(0) { }
if (addSlots.value) {
subcompose(1, content1)
subcompose(2, content2)
}
subcompose(3) { }
layout(10, 10) {}
}
}
rule.runOnIdle {
assertThat(composing1Counter).isEqualTo(0)
assertThat(composing2Counter).isEqualTo(0)
state.precompose(1, content1)
state.precompose(2, content2)
}
rule.runOnIdle {
assertThat(composing1Counter).isEqualTo(1)
assertThat(composing2Counter).isEqualTo(1)
addSlots.value = true
}
rule.runOnIdle {
assertThat(composing1Counter).isEqualTo(1)
assertThat(composing2Counter).isEqualTo(1)
}
}
@Test
fun precomposedItemDisposedWhenSubcomposeLayoutIsDisposed() {
val emitLayout = mutableStateOf(true)
var enterCounter = 0
var exitCounter = 0
val state = SubcomposeLayoutState()
val content: @Composable () -> Unit = @Composable {
DisposableEffect(Unit) {
enterCounter++
onDispose {
exitCounter++
}
}
}
rule.setContent {
if (emitLayout.value) {
SubcomposeLayout(state) {
layout(10, 10) {}
}
}
}
rule.runOnIdle {
state.precompose(Unit, content)
}
rule.runOnIdle {
assertThat(enterCounter).isEqualTo(1)
assertThat(exitCounter).isEqualTo(0)
emitLayout.value = false
}
rule.runOnIdle {
assertThat(exitCounter).isEqualTo(1)
}
}
@Test
fun precomposeIsNotTriggeringParentRemeasure() {
val state = SubcomposeLayoutState()
var measureCount = 0
var layoutCount = 0
rule.setContent {
SubcomposeLayout(state) {
measureCount++
layout(10, 10) {
layoutCount++
}
}
}
rule.runOnIdle {
assertThat(measureCount).isEqualTo(1)
assertThat(layoutCount).isEqualTo(1)
state.precompose(Unit) {
Box(Modifier.fillMaxSize())
}
}
rule.runOnIdle {
assertThat(measureCount).isEqualTo(1)
assertThat(layoutCount).isEqualTo(1)
}
}
@Test
fun precomposedItemDisposalIsNotTriggeringParentRemeasure() {
val state = SubcomposeLayoutState()
var measureCount = 0
var layoutCount = 0
rule.setContent {
SubcomposeLayout(state) {
measureCount++
layout(10, 10) {
layoutCount++
}
}
}
rule.runOnIdle {
assertThat(measureCount).isEqualTo(1)
assertThat(layoutCount).isEqualTo(1)
val handle = state.precompose(Unit) {
Box(Modifier.fillMaxSize())
}
handle.dispose()
}
rule.runOnIdle {
assertThat(measureCount).isEqualTo(1)
assertThat(layoutCount).isEqualTo(1)
}
}
@Test
fun slotsKeptForReuse() {
val items = mutableStateOf(listOf(0, 1, 2, 3, 4))
val state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(2))
composeItems(state, items)
rule.runOnIdle {
items.value = listOf(2, 3)
}
assertNodes(
exists = /*active*/ listOf(2, 3) + /*reusable*/ listOf(0, 1),
doesNotExist = /*disposed*/ listOf(4)
)
}
@Test
fun newSlotIsUsingReusedSlot() {
val items = mutableStateOf(listOf(0, 1, 2, 3, 4))
val state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(2))
composeItems(state, items)
rule.runOnIdle {
items.value = listOf(2, 3)
// 0 and 1 are now in reusable buffer
}
rule.runOnIdle {
items.value = listOf(2, 3, 5)
// the last reusable slot (1) will be used for composing 5
}
assertNodes(
exists = /*active*/ listOf(2, 3, 5) + /*reusable*/ listOf(0),
doesNotExist = /*disposed*/ listOf(1, 4)
)
}
@Test
fun theSameSlotIsUsedWhileItIsInReusableList() {
val items = mutableStateOf(listOf(0, 1, 2, 3, 4))
val state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(2))
composeItems(state, items)
rule.runOnIdle {
items.value = listOf(2, 3)
// 0 and 1 are now in reusable buffer
}
rule.runOnIdle {
items.value = listOf(2, 3, 1)
// slot 1 should be taken back from reusable
}
assertNodes(
exists = /*active*/ listOf(2, 3, 1) + /*reusable*/ listOf(0)
)
}
@Test
fun prefetchIsUsingReusableNodes() {
val items = mutableStateOf(listOf(0, 1, 2, 3, 4))
val state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(2))
composeItems(state, items)
rule.runOnIdle {
items.value = listOf(2, 3)
// 0 and 1 are now in reusable buffer
}
rule.runOnIdle {
state.precompose(5) {
ItemContent(5)
}
// prefetch should take slot 1 from reuse
}
assertNodes(
exists = /*active*/ listOf(2, 3) + /*prefetch*/ listOf(5) + /*reusable*/ listOf(0)
)
}
@Test
fun prefetchSlotWhichIsInReusableList() {
val items = mutableStateOf(listOf(0, 1, 2, 3, 4))
val state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(3))
composeItems(state, items)
rule.runOnIdle {
items.value = listOf(2)
// 0, 1, 3 are now in reusable buffer
}
rule.runOnIdle {
state.precompose(3) {
ItemContent(3)
}
// prefetch should take slot 3 from reuse
}
assertNodes(
exists = /*active*/ listOf(2) + /*prefetch*/ listOf(3) + /*reusable*/ listOf(0, 1),
doesNotExist = listOf(4)
)
}
@Test
fun nothingIsReusedWhenMaxSlotsAre0() {
val items = mutableStateOf(listOf(0, 1, 2, 3, 4))
val state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(0))
composeItems(state, items)
rule.runOnIdle {
items.value = listOf(2, 4)
}
assertNodes(
exists = listOf(2, 4),
doesNotExist = listOf(0, 1, 3)
)
}
@Test
fun reuse1Node() {
val items = mutableStateOf(listOf(0, 1, 2, 3))
val state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(1))
composeItems(state, items)
rule.runOnIdle {
items.value = listOf(0, 1)
}
assertNodes(
exists = /*active*/ listOf(0, 1) + /*reusable*/ listOf(2),
doesNotExist = /*disposed*/ listOf(3)
)
}
@SuppressLint("RememberReturnType")
@Test
fun reusedCompositionResetsRememberedObject() {
val slotState = mutableStateOf(0)
var lastRememberedSlot: Any? = null
var lastRememberedComposedModifierSlot: Any? = null
rule.setContent {
SubcomposeLayout(remember { SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)) }) {
val slot = slotState.value
subcompose(slot) {
ReusableContent(slot) {
remember {
lastRememberedSlot = slot
}
Box(
Modifier.composed {
remember {
lastRememberedComposedModifierSlot = slot
}
Modifier
}
)
}
}
layout(10, 10) {}
}
}
rule.runOnIdle {
assertThat(lastRememberedSlot).isEqualTo(0)
assertThat(lastRememberedComposedModifierSlot).isEqualTo(0)
slotState.value = 1
}
rule.runOnIdle {
assertThat(lastRememberedSlot).isEqualTo(1)
assertThat(lastRememberedComposedModifierSlot).isEqualTo(1)
slotState.value = 2
}
rule.runOnIdle {
assertThat(lastRememberedSlot).isEqualTo(2)
assertThat(lastRememberedComposedModifierSlot).isEqualTo(2)
}
}
@Test
fun subcomposeLayoutInsideLayoutUsingAlignmentsIsNotCrashing() {
// fix for regression from b/189965769
val emit = mutableStateOf(false)
rule.setContent {
LayoutUsingAlignments {
Box {
if (emit.value) {
SubcomposeLayout {
subcompose(Unit) {}
layout(10, 10) {}
}
}
}
}
}
rule.runOnIdle {
emit.value = true
}
// awaits that the change is applied and no crash happened
rule.runOnIdle { }
}
@Test
fun compositionLocalChangeInMainCompositionRecomposesSubcomposition() {
var flag by mutableStateOf(true)
val compositionLocal = compositionLocalOf<Boolean> { error("") }
var subcomposionValue: Boolean? = null
val subcomposeLambda = @Composable {
// makes sure the recomposition happens only once after the change
assertThat(compositionLocal.current).isNotEqualTo(subcomposionValue)
subcomposionValue = compositionLocal.current
}
rule.setContent {
CompositionLocalProvider(compositionLocal provides flag) {
val mainMovableValue = flag
SubcomposeLayout(
Modifier.drawBehind {
// makes sure we never draw inconsistent states
assertThat(subcomposionValue).isEqualTo(mainMovableValue)
}
) {
subcompose(Unit, subcomposeLambda)
layout(100, 100) {}
}
}
}
rule.runOnIdle {
assertThat(subcomposionValue).isTrue()
flag = false
}
rule.runOnIdle {
assertThat(subcomposionValue).isFalse()
}
}
@Test
fun compositionLocalChangeInMainCompositionRecomposesSubcomposition_noRemeasure() {
var flag by mutableStateOf(true)
val compositionLocal = compositionLocalOf<Boolean> { error("") }
var subcomposionValue: Boolean? = null
val subcomposeLambda = @Composable {
// makes sure the recomposition happens only once after the change
assertThat(compositionLocal.current).isNotEqualTo(subcomposionValue)
subcomposionValue = compositionLocal.current
}
val measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult = {
subcompose(Unit, subcomposeLambda)
layout(100, 100) {}
}
rule.setContent {
CompositionLocalProvider(compositionLocal provides flag) {
SubcomposeLayout(measurePolicy = measurePolicy)
}
}
rule.runOnIdle {
assertThat(subcomposionValue).isTrue()
flag = false
}
rule.runOnIdle {
assertThat(subcomposionValue).isFalse()
}
}
@Test
fun staticCompositionLocalChangeInMainCompositionRecomposesSubcomposition() {
var flag by mutableStateOf(true)
val compositionLocal = staticCompositionLocalOf<Boolean> { error("") }
var subcomposionValue: Boolean? = null
val subcomposeLambda = @Composable {
// makes sure the recomposition happens only once after the change
assertThat(compositionLocal.current).isNotEqualTo(subcomposionValue)
subcomposionValue = compositionLocal.current
}
val measureBlock: SubcomposeMeasureScope.(Constraints) -> MeasureResult = {
subcompose(Unit, subcomposeLambda)
layout(100, 100) {}
}
rule.setContent {
CompositionLocalProvider(compositionLocal provides flag) {
val mainCompositionValue = flag
SubcomposeLayout(
Modifier.drawBehind {
// makes sure we never draw inconsistent states
assertThat(subcomposionValue).isEqualTo(mainCompositionValue)
},
measureBlock
)
}
}
rule.runOnIdle {
assertThat(subcomposionValue).isTrue()
flag = false
}
rule.runOnIdle {
assertThat(subcomposionValue).isFalse()
}
}
@Test
fun staticCompositionLocalChangeInMainCompositionRecomposesSubcomposition_noRemeasure() {
var flag by mutableStateOf(true)
val compositionLocal = staticCompositionLocalOf<Boolean> { error("") }
var subcomposionValue: Boolean? = null
val subcomposeLambda = @Composable {
// makes sure the recomposition happens only once after the change
assertThat(compositionLocal.current).isNotEqualTo(subcomposionValue)
subcomposionValue = compositionLocal.current
}
val measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult = {
subcompose(Unit, subcomposeLambda)
layout(100, 100) {}
}
rule.setContent {
CompositionLocalProvider(compositionLocal provides flag) {
SubcomposeLayout(measurePolicy = measurePolicy)
}
}
rule.runOnIdle {
assertThat(subcomposionValue).isTrue()
flag = false
}
rule.runOnIdle {
assertThat(subcomposionValue).isFalse()
}
}
@Test
fun staticCompositionLocalChangeInMainComposition_withNonStaticLocal_invalidatesComposition() {
var isDark by mutableStateOf(false)
val staticLocal = staticCompositionLocalOf<Boolean> { error("Not defined") }
val local = compositionLocalOf<Boolean> { error("Not defined") }
val innerLocal = staticCompositionLocalOf<Unit> { error("\not defined") }
val content = @Composable {
CompositionLocalProvider(innerLocal provides Unit) {
val value1 = staticLocal.current
val value2 = local.current
Box(
Modifier
.testTag(if (value1) "dark" else "light")
.requiredSize(if (value2) 50.dp else 100.dp)
)
}
}
rule.setContent {
CompositionLocalProvider(
staticLocal provides isDark,
) {
CompositionLocalProvider(
local provides staticLocal.current
) {
SubcomposeLayout { constraints ->
val measurables = subcompose(Unit, content)
val placeables = measurables.map {
it.measure(constraints)
}
layout(100, 100) {
placeables.forEach { it.place(IntOffset.Zero) }
}
}
}
}
}
rule.onNodeWithTag("light")
.assertWidthIsEqualTo(100.dp)
isDark = true
rule.waitForIdle()
rule.onNodeWithTag("dark")
.assertWidthIsEqualTo(50.dp)
}
@Test
fun derivedStateChangeInMainCompositionRecomposesSubcomposition() {
var flag by mutableStateOf(true)
var subcomposionValue: Boolean? = null
rule.setContent {
val updatedState = rememberUpdatedState(flag)
val derivedState = remember { derivedStateOf { updatedState.value } }
val subcomposeLambda = remember<@Composable () -> Unit> {
{
// makes sure the recomposition happens only once after the change
assertThat(derivedState.value).isNotEqualTo(subcomposionValue)
subcomposionValue = derivedState.value
}
}
SubcomposeLayout(
Modifier.drawBehind {
// makes sure we never draw inconsistent states
assertThat(subcomposionValue).isEqualTo(updatedState.value)
}
) {
subcompose(Unit, subcomposeLambda)
layout(100, 100) {}
}
}
rule.runOnIdle {
assertThat(subcomposionValue).isTrue()
flag = false
}
rule.runOnIdle {
assertThat(subcomposionValue).isFalse()
}
}
@Test
fun updatingStateWorks() {
val tagState = mutableStateOf("box1")
rule.setContent {
val tag = tagState.value
val state = remember(tag) { SubcomposeLayoutState() }
SubcomposeLayout(state = state) {
val placeable = subcompose(Unit) {
Box(Modifier.size(10.dp).testTag(tag))
}.first().measure(Constraints())
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
rule.onNodeWithTag("box1").assertIsDisplayed()
rule.runOnIdle { tagState.value = "box2" }
rule.onNodeWithTag("box2").assertIsDisplayed()
}
@Test
fun nodesKeptAsReusableAreReusedWhenTheStateObjectChanges() {
val slotState = mutableStateOf(0)
var remeasuresCount = 0
val measureModifier = Modifier.layout { _, _ ->
remeasuresCount++
layout(10, 10) {}
}
val layoutState = mutableStateOf(SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)))
rule.setContent {
val slot = slotState.value
SubcomposeLayout(layoutState.value) {
val placeable = subcompose(slot) {
ReusableContent(slot) {
Box(measureModifier)
}
}.first().measure(Constraints())
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
rule.runOnIdle {
slotState.value = 1
// slot 0 is kept for reuse
}
rule.runOnIdle {
assertThat(remeasuresCount).isEqualTo(2)
remeasuresCount = 0
slotState.value = 2 // slot 0 should be reused
layoutState.value = SubcomposeLayoutState(SubcomposeSlotReusePolicy(1))
}
rule.runOnIdle {
// there is no remeasure as the node was reused and the modifier didn't change
assertThat(remeasuresCount).isEqualTo(0)
}
}
@Test
fun previouslyActiveNodesAreReusedWhenTheStateObjectChanges() {
val slotState = mutableStateOf(0)
var remeasuresCount = 0
val measureModifier = Modifier.layout { _, _ ->
remeasuresCount++
layout(10, 10) {}
}
val layoutState = mutableStateOf(SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)))
rule.setContent {
val slot = slotState.value
SubcomposeLayout(layoutState.value) { _ ->
val placeable = subcompose(slot) {
ReusableContent(slot) {
Box(measureModifier)
}
}.first().measure(Constraints())
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
rule.runOnIdle {
assertThat(remeasuresCount).isEqualTo(1)
remeasuresCount = 0
slotState.value = 1 // slot 0 should be reused
layoutState.value = SubcomposeLayoutState(SubcomposeSlotReusePolicy(1))
}
rule.runOnIdle {
// there is no remeasure as the node was reused and the modifier didn't change
assertThat(remeasuresCount).isEqualTo(0)
}
}
@Test
fun reusableNodeIsKeptAsReusableAfterStateUpdate() {
val layoutState = mutableStateOf(SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)))
val needChild = mutableStateOf(true)
var coordinates: LayoutCoordinates? = null
rule.setContent {
SubcomposeLayout(state = layoutState.value) { constraints ->
val node = if (needChild.value) {
subcompose(Unit) {
Box(Modifier.onGloballyPositioned { coordinates = it })
}.first().measure(constraints)
} else {
null
}
layout(10, 10) {
node?.place(0, 0)
}
}
}
rule.runOnIdle {
assertThat(coordinates!!.isAttached).isTrue()
needChild.value = false
}
rule.runOnIdle {
// the modifier is still attached
assertThat(coordinates!!.isAttached).isTrue()
layoutState.value = SubcomposeLayoutState(SubcomposeSlotReusePolicy(1))
}
rule.runOnIdle {
// the modifier is still attached
assertThat(coordinates!!.isAttached).isTrue()
}
}
@Test
fun passingSmallerMaxSlotsToRetainForReuse() {
val layoutState = mutableStateOf(SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)))
val needChild = mutableStateOf(true)
var coordinates: LayoutCoordinates? = null
rule.setContent {
SubcomposeLayout(state = layoutState.value) { constraints ->
val node = if (needChild.value) {
subcompose(Unit) {
Box(Modifier.onGloballyPositioned { coordinates = it })
}.first().measure(constraints)
} else {
null
}
layout(10, 10) {
node?.place(0, 0)
}
}
}
rule.runOnIdle { needChild.value = false }
rule.runOnIdle {
// the node in the reusable pool is still attached
assertThat(coordinates!!.isAttached).isTrue()
layoutState.value = SubcomposeLayoutState(SubcomposeSlotReusePolicy(0))
}
rule.runOnIdle {
// detached as the new state has 0 as maxSlotsToRetainForReuse
assertThat(coordinates!!.isAttached).isFalse()
}
}
@Test
fun compositionKeptForReuseIsDisposed() {
val needChild = mutableStateOf(true)
var disposed = false
rule.setContent {
SubcomposeLayout(
state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(1))
) {
if (needChild.value) {
subcompose(Unit) {
DisposableEffect(Unit) {
onDispose {
disposed = true
}
}
}
}
layout(10, 10) {}
}
}
rule.runOnIdle { needChild.value = false }
rule.runOnIdle {
// the composition in the reusable pool is disposed
assertThat(disposed).isTrue()
}
}
@Test
fun composedModifierOnReusableNodeIsDisposedButAttached() {
val layoutState = SubcomposeLayoutState(SubcomposeSlotReusePolicy(1))
val needChild = mutableStateOf(true)
var composed = false
var coordinates: LayoutCoordinates? = null
rule.setContent {
SubcomposeLayout(state = layoutState) { constraints ->
val node = if (needChild.value) {
subcompose(Unit) {
Box(Modifier.composed {
DisposableEffect(Unit) {
composed = true
onDispose {
composed = false
}
}
onGloballyPositioned {
coordinates = it
}
})
}.first().measure(constraints)
} else {
null
}
layout(10, 10) {
node?.place(0, 0)
}
}
}
rule.runOnIdle {
assertThat(composed).isTrue()
assertThat(coordinates!!.isAttached).isTrue()
needChild.value = false
}
rule.runOnIdle {
assertThat(composed).isFalse()
assertThat(coordinates!!.isAttached).isTrue()
needChild.value = true
}
rule.runOnIdle {
assertThat(composed).isTrue()
assertThat(coordinates!!.isAttached).isTrue()
}
}
@Test
fun customPolicy_retainingExactItem() {
val items = mutableStateOf(listOf(0, 1, 2, 3, 4))
val policy = object : SubcomposeSlotReusePolicy {
override fun getSlotsToRetain(slotIds: SubcomposeSlotReusePolicy.SlotIdsSet) {
assertThat(slotIds).containsExactly(1, 2, 4).inOrder()
slotIds.remove(1)
slotIds.remove(4)
}
override fun areCompatible(slotId: Any?, reusableSlotId: Any?): Boolean {
assertThat(reusableSlotId).isEqualTo(2)
return true
}
}
val state = SubcomposeLayoutState(policy)
composeItems(state, items)
rule.runOnIdle {
items.value = listOf(0, 3)
}
assertNodes(
exists = /*active*/ listOf(0, 3) + /*reusable*/ listOf(2),
doesNotExist = /*disposed*/ listOf(1, 4)
)
rule.runOnIdle {
items.value = listOf(0, 3, 5)
}
assertNodes(
exists = /*active*/ listOf(0, 3, 5) + /*reusable*/ emptyList(),
doesNotExist = /*disposed*/ listOf(1, 2, 4)
)
}
@Test
fun customPolicy_lastUsedItemsAreFirstInSet() {
val items = mutableStateOf(listOf(0, 1, 2, 3, 4))
var expectedReusable = arrayOf<Int>()
val policy = object : SubcomposeSlotReusePolicy {
override fun getSlotsToRetain(slotIds: SubcomposeSlotReusePolicy.SlotIdsSet) {
assertThat(slotIds).containsExactly(*expectedReusable).inOrder()
}
override fun areCompatible(slotId: Any?, reusableSlotId: Any?) = true
}
val state = SubcomposeLayoutState(policy)
composeItems(state, items)
rule.runOnIdle {
expectedReusable = arrayOf(1, 2, 3, 4)
items.value = listOf(0)
}
rule.runOnIdle {
expectedReusable = arrayOf(1, 2, 3)
items.value = listOf(0, 4)
}
rule.runOnIdle {
expectedReusable = arrayOf(4, 1, 2, 3)
items.value = listOf(0)
}
}
@Test
fun customPolicy_disposedPrefetchedItemIsFirstInSet() {
val items = mutableStateOf(listOf(0, 1, 2, 3, 4))
var expectedReusable = arrayOf<Int>()
var callbackCalled = false
var expectedSlotId: Any? = null
var expectedreusableSlotId: Any? = null
val policy = object : SubcomposeSlotReusePolicy {
override fun getSlotsToRetain(slotIds: SubcomposeSlotReusePolicy.SlotIdsSet) {
callbackCalled = true
assertThat(slotIds).containsExactly(*expectedReusable).inOrder()
}
override fun areCompatible(slotId: Any?, reusableSlotId: Any?): Boolean {
assertThat(slotId).isEqualTo(expectedSlotId)
assertThat(reusableSlotId).isEqualTo(expectedreusableSlotId)
return true
}
}
val state = SubcomposeLayoutState(policy)
composeItems(state, items)
rule.runOnIdle {
expectedReusable = arrayOf(1, 2, 3, 4)
items.value = listOf(0)
}
rule.runOnIdle {
assertThat(callbackCalled).isTrue()
callbackCalled = false
expectedSlotId = 5
expectedreusableSlotId = 4
val handle = state.precompose(5, {}) // it should reuse slot 4
expectedReusable = arrayOf(5, 1, 2, 3)
handle.dispose()
assertThat(callbackCalled).isTrue()
}
}
@Test
fun customPolicy_retainingOddNumbers() {
fun isOdd(number: Any?): Boolean {
return (number as Int) % 2 == 1
}
val items = mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6))
val policy = object : SubcomposeSlotReusePolicy {
override fun getSlotsToRetain(slotIds: SubcomposeSlotReusePolicy.SlotIdsSet) {
slotIds.removeAll { !isOdd(it) }
}
override fun areCompatible(slotId: Any?, reusableSlotId: Any?): Boolean {
return isOdd(slotId) && isOdd(reusableSlotId)
}
}
val state = SubcomposeLayoutState(policy)
composeItems(state, items)
rule.runOnIdle {
items.value = listOf()
}
assertNodes(
exists = /*active*/ emptyList<Int>() + /*reusable*/ listOf(1, 3, 5),
doesNotExist = /*disposed*/ listOf(0, 2, 4, 6)
)
rule.runOnIdle {
items.value = listOf(8, 9, 10)
// new slots composed for 8 and 10
// 5 is reused for 9
}
assertNodes(
exists = /*active*/ listOf(8, 9, 10) + /*reusable*/ listOf(1, 3),
doesNotExist = /*disposed*/ listOf(5)
)
}
@Test
fun customPolicy_reusingSecondSlotFromTheEnd() {
fun isOdd(number: Any?): Boolean {
return (number as Int) % 2 == 1
}
val items = mutableStateOf(listOf(0, 1, 2, 3))
val policy = object : SubcomposeSlotReusePolicy {
override fun getSlotsToRetain(slotIds: SubcomposeSlotReusePolicy.SlotIdsSet) {}
override fun areCompatible(slotId: Any?, reusableSlotId: Any?): Boolean {
return isOdd(slotId) == isOdd(reusableSlotId)
}
}
val state = SubcomposeLayoutState(policy)
composeItems(state, items)
rule.runOnIdle {
items.value = listOf()
}
assertNodes(exists = /*active*/ emptyList<Int>() + /*reusable*/ listOf(0, 1, 2, 3))
rule.runOnIdle {
items.value = listOf(10) // slot 2 should be reused
}
assertNodes(
exists = /*active*/ listOf(10) + /*reusable*/ listOf(0, 1, 3),
doesNotExist = /*disposed*/ listOf(2)
)
}
@Test
fun premeasuringAllowsToSkipMeasureOnceTheSlotIsComposed() {
val state = SubcomposeLayoutState()
var remeasuresCount = 0
var relayoutCount = 0
var subcomposeLayoutRemeasures = 0
val modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
remeasuresCount++
layout(placeable.width, placeable.height) {
relayoutCount++
placeable.place(0, 0)
}
}.fillMaxSize()
val content = @Composable { Box(modifier) }
val constraints = Constraints(maxWidth = 100, minWidth = 100)
var needContent by mutableStateOf(false)
rule.setContent {
SubcomposeLayout(state) {
subcomposeLayoutRemeasures++
val placeable = if (needContent) {
subcompose(Unit, content).first().measure(constraints)
} else {
null
}
layout(10, 10) {
placeable?.place(0, 0)
}
}
}
rule.runOnIdle {
assertThat(remeasuresCount).isEqualTo(0)
subcomposeLayoutRemeasures = 0
val handle = state.precompose(Unit, content)
assertThat(remeasuresCount).isEqualTo(0)
assertThat(handle.placeablesCount).isEqualTo(1)
handle.premeasure(0, constraints)
assertThat(remeasuresCount).isEqualTo(1)
assertThat(relayoutCount).isEqualTo(0)
assertThat(subcomposeLayoutRemeasures).isEqualTo(0)
remeasuresCount = 0
needContent = true
}
rule.runOnIdle {
assertThat(remeasuresCount).isEqualTo(0)
assertThat(relayoutCount).isEqualTo(1)
}
}
@Test
fun premeasuringTwoPlaceables() {
val state = SubcomposeLayoutState()
var remeasuresCount = 0
val modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
remeasuresCount++
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}.fillMaxSize()
val content = @Composable {
Box(modifier)
Box(modifier)
}
val constraints0 = Constraints(maxWidth = 100, minWidth = 100)
val constraints1 = Constraints(maxWidth = 200, minWidth = 200)
var needContent by mutableStateOf(false)
rule.setContent {
SubcomposeLayout(state) {
val placeables = if (needContent) {
val measurables = subcompose(Unit, content)
assertThat(measurables.size).isEqualTo(2)
measurables.mapIndexed { index, measurable ->
measurable.measure(if (index == 0) constraints0 else constraints1)
}
} else {
emptyList()
}
layout(10, 10) {
placeables.forEach { it.place(0, 0) }
}
}
}
rule.runOnIdle {
assertThat(remeasuresCount).isEqualTo(0)
val handle = state.precompose(Unit, content)
assertThat(remeasuresCount).isEqualTo(0)
assertThat(handle.placeablesCount).isEqualTo(2)
handle.premeasure(0, constraints0)
assertThat(remeasuresCount).isEqualTo(1)
handle.premeasure(1, constraints1)
assertThat(remeasuresCount).isEqualTo(2)
remeasuresCount = 0
needContent = true
}
rule.runOnIdle {
assertThat(remeasuresCount).isEqualTo(0)
}
}
@Test
fun premeasuringIncorrectIndexesCrashes() {
val state = SubcomposeLayoutState()
val content = @Composable {
Box(Modifier.size(10.dp))
Box(Modifier.size(10.dp))
}
rule.setContent {
SubcomposeLayout(state) {
layout(10, 10) {}
}
}
rule.runOnIdle {
val handle = state.precompose(Unit, content)
assertThrows(IndexOutOfBoundsException::class.java) {
handle.premeasure(-1, Constraints())
}
assertThrows(IndexOutOfBoundsException::class.java) {
handle.premeasure(2, Constraints())
}
}
}
@Test
fun ifSlotWasUsedDuringMeasurePassHandleHasZeroPlaceables() {
val state = SubcomposeLayoutState()
val content = @Composable {
Box(Modifier.size(10.dp))
}
var needContent by mutableStateOf(false)
rule.setContent {
SubcomposeLayout(state) {
val placeable = if (needContent) {
subcompose(Unit, content).first().measure(Constraints())
} else {
null
}
layout(10, 10) {
placeable?.place(0, 0)
}
}
}
lateinit var handle: SubcomposeLayoutState.PrecomposedSlotHandle
rule.runOnIdle {
handle = state.precompose(Unit, content)
handle.premeasure(0, Constraints())
needContent = true
}
rule.runOnIdle {
assertThat(handle.placeablesCount).isEqualTo(0)
// we also make sure that calling dispose on such handle is safe
handle.dispose()
}
}
@Test
fun stateIsRestoredWhenGoBackToScreen1WithSubcomposition() {
val restorationTester = StateRestorationTester(rule)
var increment = 0
var screen by mutableStateOf(Screens.Screen1)
var restorableNumberOnScreen1 = -1
restorationTester.setContent {
val holder = rememberSaveableStateHolder()
holder.SaveableStateProvider(screen) {
if (screen == Screens.Screen1) {
SubcomposeLayout {
subcompose(Unit) {
restorableNumberOnScreen1 = rememberSaveable { increment++ }
}
layout(10, 10) {}
}
}
}
}
rule.runOnIdle {
assertThat(restorableNumberOnScreen1).isEqualTo(0)
screen = Screens.Screen2
}
// wait for the screen switch to apply
rule.runOnIdle {
restorableNumberOnScreen1 = -1
// switch back to screen1
screen = Screens.Screen1
}
rule.runOnIdle {
assertThat(restorableNumberOnScreen1).isEqualTo(0)
}
}
@Test
fun disposeSecondPrecomposedItem() {
// it is a regression from b/218668336. the assertion was incorrectly checking
// for the ranges so disposing the second active precomposed node was crashing.
val state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(0))
composeItems(state, mutableStateOf(emptyList()))
rule.runOnIdle {
state.precompose(0) { ItemContent(0) }
val handle = state.precompose(1) { ItemContent(1) }
handle.dispose()
}
assertNodes(
exists = /*prefetch*/ listOf(0),
doesNotExist = /*disposed*/ listOf(1)
)
}
@Test
fun reusingWithNestedSubcomposeLayoutInside() {
val slotState = mutableStateOf(0)
rule.setContent {
SubcomposeLayout(
remember { SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)) }
) { constraints ->
val slot = slotState.value
val child = subcompose(slot) {
ReusableContent(slot) {
Box {
SubcomposeLayout(Modifier.testTag("$slot")) { constraints ->
val placeable = subcompose(0) {
Box(modifier = Modifier.size(10.dp))
}.first().measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
}
}.first().measure(constraints)
layout(child.width, child.height) {
child.place(0, 0)
}
}
}
rule.runOnIdle {
slotState.value = 1
}
rule.runOnIdle {
slotState.value = 2
}
rule.onNodeWithTag("2").assertIsDisplayed()
rule.onNodeWithTag("1").assertIsNotDisplayed()
rule.onNodeWithTag("0").assertDoesNotExist()
}
@Test
fun disposingPrecomposedItemInTheNestedSubcomposeLayout() {
var needSlot by mutableStateOf(true)
val state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(1))
rule.setContent {
SubcomposeLayout(
remember { SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)) }
) { constraints ->
val child = if (needSlot) {
subcompose(0) {
Box {
SubcomposeLayout(state = state, Modifier.testTag("0")) { constraints ->
if (needSlot) {
val placeable = subcompose(0) {
Box(modifier = Modifier.size(10.dp))
}.first().measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
} else {
layout(100, 100) { }
}
}
}
}.first().measure(constraints)
} else {
null
}
layout(100, 100) {
child?.place(0, 0)
}
}
}
val handle = rule.runOnIdle {
state.precompose(1) {
Box(modifier = Modifier.size(10.dp).testTag("1"))
}
}
rule.runOnIdle {
needSlot = false
}
rule.runOnIdle {
handle.dispose()
}
rule.onNodeWithTag("1").assertDoesNotExist()
rule.onNodeWithTag("0").assertIsNotDisplayed()
}
@Test
fun getAlignmentsOnSubcomposeLayoutContent() {
var baseline: Int? = null
rule.setContent {
Layout(
{
SubcomposeLayout { constraints ->
val placeable = subcompose("0") {
Layout(content = {}) { _, _ ->
layout(10, 10, mapOf(FirstBaseline to 100)) {}
}
}.first().measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
) { measurables, constraints ->
val titlePlaceable = measurables.first().measure(
constraints.copy(minHeight = 0)
)
baseline = titlePlaceable[FirstBaseline]
layout(titlePlaceable.width, titlePlaceable.height) {
titlePlaceable.place(0, 0)
}
}
}
rule.runOnIdle {
assertThat(baseline).isEqualTo(100)
}
}
@Test
fun noNotMeasuredWrappersInTheHierarchy() {
lateinit var coordinates: LayoutCoordinates
var size: IntSize? = null
rule.setContent {
Box {
SubcomposeLayout { constraints ->
val placeable = subcompose("0") {
Box(Modifier
.fillMaxSize()
.onGloballyPositioned {
coordinates = it
}
.onSizeChanged {
size = it
}
)
}.first().measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
}
rule.runOnIdle {
var current: LayoutCoordinates? = coordinates
while (current != null) {
assertThat(current.isAttached)
assertThat(current.size).isEqualTo(size)
current = current.parentCoordinates
}
}
}
@Test
@Ignore("b/188320755")
fun forceMeasureOfInactiveElementFromLaunchedEffect() {
var isActive by mutableStateOf(true)
var forceMeasureFromLaunchedEffect by mutableStateOf(false)
var remeasurer: Remeasurement? = null
rule.setContent {
SubcomposeLayout(
modifier = Modifier.then(object : RemeasurementModifier {
override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
remeasurer = remeasurement
}
}),
state = remember {
SubcomposeLayoutState(SubcomposeSlotReusePolicy(1))
},
) { constraints ->
val placeable = if (isActive) {
val measureables = subcompose(null) {
Box(Modifier)
}
measureables.map { it.measure(constraints) }
} else {
forceMeasureFromLaunchedEffect = true
emptyList()
}
layout(0, 0) {
placeable.forEach { it.place(0, 0) }
}
}
if (forceMeasureFromLaunchedEffect) {
LaunchedEffect(Unit) {
isActive = true
remeasurer?.forceRemeasure()
}
}
}
rule.runOnIdle {
isActive = false
}
rule.waitUntil { isActive }
}
private fun composeItems(
state: SubcomposeLayoutState,
items: MutableState<List<Int>>
) {
rule.setContent {
SubcomposeLayout(state) { constraints ->
items.value.forEach {
subcompose(it) {
ItemContent(it)
}.forEach {
it.measure(constraints)
}
}
layout(10, 10) {}
}
}
}
@Composable
private fun ItemContent(index: Int) {
Box(Modifier.fillMaxSize().testTag("$index"))
}
private fun assertNodes(exists: List<Int>, doesNotExist: List<Int> = emptyList()) {
exists.forEach {
rule.onNodeWithTag("$it")
.assertExists()
}
doesNotExist.forEach {
rule.onNodeWithTag("$it")
.assertDoesNotExist()
}
}
}
fun ImageBitmap.assertCenterPixelColor(expectedColor: Color) {
asAndroidBitmap().assertColor(expectedColor, width / 2, height / 2)
}
@Composable
private fun LayoutUsingAlignments(content: @Composable () -> Unit) {
Layout(content) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
placeable[FirstBaseline]
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
private enum class Screens {
Screen1,
Screen2,
}