blob: 125ac7d505920768f35c4c2bd6494ccc8b4f8625 [file] [log] [blame]
/*
* Copyright 2021 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.runtime
import androidx.compose.runtime.mock.Linear
import androidx.compose.runtime.mock.Text
import androidx.compose.runtime.mock.compositionTest
import androidx.compose.runtime.mock.expectChanges
import androidx.compose.runtime.mock.expectNoChanges
import androidx.compose.runtime.mock.revalidate
import androidx.compose.runtime.mock.validate
import androidx.compose.runtime.snapshots.Snapshot
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* Tests the interaction between [derivedStateOf] and composition.
*/
@Stable
class CompositionAndDerivedStateTests {
@Test
fun derivedStateOfChangesInvalidate() = compositionTest {
var a by mutableStateOf(31)
var b by mutableStateOf(10)
val answer by derivedStateOf { a + b }
compose {
Text("The answer is $answer")
}
validate {
Text("The answer is ${a + b}")
}
a++
expectChanges()
b++
expectChanges()
revalidate()
}
@Test
fun onlyInvalidatesIfResultIsDifferent() = compositionTest {
var a by mutableStateOf(32)
var b by mutableStateOf(10)
val answer by derivedStateOf { a + b }
compose {
Text("The answer is $answer")
}
validate {
Text("The answer is ${a + b}")
}
// A snapshot is necessary here otherwise the ui thread might see one changed but not
// the other. A snapshot ensures that both modifications will be seen together.
Snapshot.withMutableSnapshot {
a += 1
b -= 1
}
expectNoChanges()
revalidate()
a += 1
// Change just one should reflect a change.
expectChanges()
revalidate()
b -= 1
// Change just one should reflect a change.
expectChanges()
revalidate()
Snapshot.withMutableSnapshot {
a += 1
b -= 1
}
// Again, the change should not cause an invalidate.
expectNoChanges()
revalidate()
}
@Test
fun onlyEvaluateDerivedStatesThatAreLive() = compositionTest {
var a by mutableStateOf(11)
val useNone = 0x00
val useD = 0x01
val useE = 0x02
val useF = 0x04
var use by mutableStateOf(useD)
fun useToString(use: Int): String {
var result = ""
if (use and useD != 0) {
result = "useD"
}
if (use and useE != 0) {
if (result.isNotEmpty()) result += ", "
result += "useE"
}
if (use and useF != 0) {
if (result.isNotEmpty()) result += ", "
result += "useF"
}
return result
}
var dCalculated = 0
val d = "d" to derivedStateOf {
dCalculated++
a
}
var eCalculated = 0
val e = "e" to derivedStateOf {
eCalculated++
a + 100
}
var fCalculated = 0
val f = "f" to derivedStateOf {
fCalculated++
a + 1000
}
var dExpected = 0
var eExpected = 0
var fExpected = 0
fun expect(modified: Int, previous: Int = -1) {
if (modified and useD == useD) dExpected++
if (modified and useE == useE) eExpected++
if (modified and useF == useF) fExpected++
val additionalInfo = if (previous >= 0) {
" switching from ${useToString(previous)} to ${useToString(modified)}"
} else ""
assertEquals(dExpected, dCalculated, "d calculated an unexpected amount$additionalInfo")
assertEquals(eExpected, eCalculated, "e calculated an unexpected amount$additionalInfo")
assertEquals(fExpected, fCalculated, "f calculated an unexpected amount$additionalInfo")
}
// Nothing should be calculated yet.
expect(useNone)
compose {
if (use and useD == useD) {
Display(d)
}
if (use and useE == useE) {
Display(e)
}
if (use and useF == useF) {
Display(f)
}
if ((use and (useD or useE)) == useD or useE) {
Display(d, e)
}
if ((use and (useD or useF)) == useD or useF) {
Display(d, f)
}
if ((use and (useE or useF)) == useE or useF) {
Display(e, f)
}
if ((use and (useD or useE or useF)) == useD or useE or useF) {
Display(d, e, f)
}
}
validate {
if (use and useD != 0) {
Text("d = $a")
}
if (use and useE != 0) {
Text("e = ${a + 100}")
}
if (use and useF != 0) {
Text("f = ${a + 1000}")
}
if ((use and (useD or useE)) == useD or useE) {
Text("d = $a")
Text("e = ${a + 100}")
}
if ((use and (useD or useF)) == useD or useF) {
Text("d = $a")
Text("f = ${a + 1000}")
}
if ((use and (useE or useF)) == useE or useF) {
Text("e = ${a + 100}")
Text("f = ${a + 1000}")
}
if ((use and (useD or useE or useF)) == useD or useE or useF) {
Text("d = $a")
Text("e = ${a + 100}")
Text("f = ${a + 1000}")
}
}
expect(useD)
// Modify A
a++
expectChanges()
revalidate()
expect(useD)
fun switchTo(newUse: Int) {
val previous = use
use = newUse
a++
expectChanges()
revalidate()
expect(newUse, previous)
}
switchTo(useD or useE)
switchTo(useD or useF)
val states = listOf(
useE,
useF,
useD or useE,
useD or useF,
useD or useE or useF,
useE or useF,
useNone
)
for (newUse in states) {
switchTo(newUse)
}
}
@Test
fun ensureCalculateIsNotCalledTooSoon() = compositionTest {
var a by mutableStateOf(11)
var dCalculated = 0
var dChanged = false
val d = "d" to derivedStateOf {
dCalculated++
a + 10
}
compose {
Text("a = $a")
val oldDCalculated = dCalculated
Display(d)
dChanged = oldDCalculated != dCalculated
}
validate {
Text("a = $a")
Text("d = ${a + 10}")
}
assertTrue(dChanged, "Expected d to recalculate")
a++
expectChanges()
revalidate()
assertTrue(dChanged, "Expected d to recalculate")
}
@Test
fun writingToADerviedStateDependencyTriggersAForwardInvalidate() = compositionTest {
var a by mutableStateOf(12)
var b by mutableStateOf(30)
val d = derivedStateOf { a + b }
compose {
DisplayIndirect("d", d)
var c by remember { mutableStateOf(0) }
c = a + b
val e = remember { derivedStateOf { a + b + c } }
DisplayIndirect("e", e)
}
validate {
Text("d = ${a + b}")
Text("e = ${a + b + a + b}")
}
a++
expectChanges()
revalidate()
b--
expectChanges()
revalidate()
Snapshot.withMutableSnapshot {
a += 1
b -= 1
}
advance()
revalidate()
}
@Test // Regression test for 215402574
fun observingBothNormalAndDerivedInSameScope() = compositionTest {
val a = mutableStateOf(0)
val b = derivedStateOf { a.value > 0 }
val c = mutableStateOf(false)
compose {
Linear {
if (b.value) Text("B is true")
if (c.value) Text("C is true")
}
}
validate {
Linear {
if (b.value) Text("B is true")
if (c.value) Text("C is true")
}
}
a.value++
expectChanges()
revalidate()
a.value++
advance()
revalidate()
a.value++
Snapshot.sendApplyNotifications()
c.value = true
advance()
revalidate()
}
@Test
fun changingTheDerivedStateInstanceShouldRelease() = compositionTest {
var reload by mutableStateOf(0)
compose {
val items = remember(reload) {
derivedStateOf {
List(10) { it }
}
}
Text("List of size ${items.value.size}")
}
validate {
Text("List of size 10")
}
repeat(10) {
reload++
advance()
}
revalidate()
// Validate there are only 2 observed objectt which should be `reload` and the last
// created derivedStateOf instance
val observed = (composition as? CompositionImpl)?.observedObjects ?: emptyList()
assertEquals(2, observed.count())
}
@Test
fun observingDerivedStateInMultipleScopes() = compositionTest {
var observeInFirstScope by mutableStateOf(true)
var count by mutableStateOf(0)
compose {
val items by remember {
derivedStateOf {
List(count) { it }
}
}
Linear {
if (observeInFirstScope) {
Text("List of size ${items.size}")
}
}
Linear {
Text("List of size ${items.size}")
}
}
validate {
Linear {
Text("List of size 0")
}
Linear {
Text("List of size 0")
}
}
observeInFirstScope = false
advance()
count++
advance()
validate {
Linear {
}
Linear {
Text("List of size 1")
}
}
}
@Test
fun changingTheDerivedStateInstanceShouldClearDependencies() = compositionTest {
var reload by mutableStateOf(0)
compose {
val itemValue = remember(reload) {
derivedStateOf {
reload
}
}
val items = remember(reload) {
derivedStateOf {
List(10) { itemValue.value }
}
}
Text("List of size ${items.value.size}")
}
validate {
Text("List of size 10")
}
repeat(10) {
reload++
advance()
}
revalidate()
// Validate there are only 2 observed dependencies, one per each derived state
val observed = (composition as? CompositionImpl)?.derivedStateDependencies ?: emptyList()
assertEquals(2, observed.count())
}
@Test
fun changingDerivedStateDependenciesShouldClearThem() = compositionTest {
var reload by mutableStateOf(0)
compose {
val itemValue = remember(reload) {
derivedStateOf { 1 }
}
val intermediateState = rememberUpdatedState(itemValue)
val snapshot = remember {
derivedStateOf {
List(10) { intermediateState.value.value }
}
}
Text("List of size ${snapshot.value.size}")
}
validate {
Text("List of size 10")
}
repeat(10) {
reload++
advance()
}
revalidate()
// Validate there are only 2 observed dependencies, one for intermediateState, one for itemValue
val observed = (composition as? CompositionImpl)?.derivedStateDependencies ?: emptyList()
println(observed)
assertEquals(2, observed.count())
}
}
@Composable
private fun DisplayItem(name: String, state: State<Int>) {
Text("$name = ${state.value}")
}
@Composable
private fun DisplayIndirect(name: String, state: State<Int>) {
DisplayItem(name, state)
}
@Composable
private fun Display(vararg names: Pair<String, State<Int>>) {
for ((name, state) in names) {
DisplayIndirect(name, state)
}
}