blob: 64c54a57dd0d8616c0ada9def987fe7006ddddbe [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.external.kotlinx.collections.immutable.persistentHashMapOf
import androidx.compose.runtime.mock.EmptyApplier
import androidx.compose.runtime.mock.TestMonotonicFrameClock
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.validate
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlin.test.assertEquals
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
// Create a normal (dynamic) CompositionLocal with a string value
val LocalSomeTextComposition = compositionLocalOf { "Default" }
// Create a normal (dynamic) CompositionLocal with an int value
val LocalSomeIntComposition = compositionLocalOf { 1 }
// Create a non-overridable CompositionLocal provider key
private val LocalSomeOtherIntProvider = compositionLocalOf { 1 }
// Make public the consumer key.
val LocalSomeOtherIntComposition: CompositionLocal<Int> = LocalSomeOtherIntProvider
// Create a static CompositionLocal with an int value
val LocalSomeStaticInt = staticCompositionLocalOf { 40 }
class CompositionLocalTests {
@Composable
fun ReadStringCompositionLocal(compositionLocal: CompositionLocal<String>) {
Text(value = compositionLocal.current)
}
@Test
fun testCompositionLocalApi() = compositionTest {
compose {
assertEquals("Default", LocalSomeTextComposition.current)
assertEquals(1, LocalSomeIntComposition.current)
CompositionLocalProvider(
LocalSomeTextComposition provides "Test1",
LocalSomeIntComposition provides 12,
LocalSomeOtherIntProvider provides 42,
LocalSomeStaticInt provides 50
) {
assertEquals(
"Test1",
LocalSomeTextComposition.current
)
assertEquals(12, LocalSomeIntComposition.current)
assertEquals(
42,
LocalSomeOtherIntComposition.current
)
assertEquals(50, LocalSomeStaticInt.current)
CompositionLocalProvider(
LocalSomeTextComposition provides "Test2",
LocalSomeStaticInt provides 60
) {
assertEquals(
"Test2",
LocalSomeTextComposition.current
)
assertEquals(
12,
LocalSomeIntComposition.current
)
assertEquals(
42,
LocalSomeOtherIntComposition.current
)
assertEquals(60, LocalSomeStaticInt.current)
}
assertEquals(
"Test1",
LocalSomeTextComposition.current
)
assertEquals(12, LocalSomeIntComposition.current)
assertEquals(
42,
LocalSomeOtherIntComposition.current
)
assertEquals(50, LocalSomeStaticInt.current)
}
assertEquals("Default", LocalSomeTextComposition.current)
assertEquals(1, LocalSomeIntComposition.current)
}
}
@Test
fun recompose_Dynamic() = compositionTest {
val invalidates = mutableListOf<RecomposeScope>()
fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
var someText = "Unmodified"
compose {
invalidates.add(currentRecomposeScope)
CompositionLocalProvider(
LocalSomeTextComposition provides someText
) {
ReadStringCompositionLocal(
compositionLocal = LocalSomeTextComposition
)
}
}
fun validate() {
validate {
Text(someText)
}
}
someText = "Modified"
doInvalidate()
expectChanges()
validate()
}
@Test
fun recompose_Static() = compositionTest {
val invalidates = mutableListOf<RecomposeScope>()
fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
val staticStringCompositionLocal = staticCompositionLocalOf { "Default" }
var someText = "Unmodified"
compose {
invalidates.add(currentRecomposeScope)
CompositionLocalProvider(
staticStringCompositionLocal provides someText
) {
ReadStringCompositionLocal(
compositionLocal = staticStringCompositionLocal
)
}
}
fun validate() {
validate {
Text(someText)
}
}
validate()
someText = "Modified"
doInvalidate()
expectChanges()
validate()
}
@Test
fun subCompose_Dynamic() = compositionTest {
val invalidates = mutableListOf<RecomposeScope>()
fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
var someText = "Unmodified"
compose {
invalidates.add(currentRecomposeScope)
CompositionLocalProvider(
LocalSomeTextComposition provides someText,
LocalSomeIntComposition provides 0
) {
ReadStringCompositionLocal(compositionLocal = LocalSomeTextComposition)
TestSubcomposition {
assertEquals(
someText,
LocalSomeTextComposition.current
)
assertEquals(0, LocalSomeIntComposition.current)
CompositionLocalProvider(
LocalSomeIntComposition provides 42
) {
assertEquals(
someText,
LocalSomeTextComposition.current
)
assertEquals(
42,
LocalSomeIntComposition.current
)
}
}
}
}
fun validate() {
validate {
Text(someText)
}
}
someText = "Modified"
doInvalidate()
expectChanges()
validate()
}
@Test
fun subCompose_Static() = compositionTest {
val invalidates = mutableListOf<RecomposeScope>()
fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
val staticSomeTextCompositionLocal = staticCompositionLocalOf { "Default" }
val staticSomeIntCompositionLocal = staticCompositionLocalOf { -1 }
var someText = "Unmodified"
compose {
invalidates.add(currentRecomposeScope)
CompositionLocalProvider(
staticSomeTextCompositionLocal provides someText,
staticSomeIntCompositionLocal provides 0
) {
assertEquals(someText, staticSomeTextCompositionLocal.current)
assertEquals(0, staticSomeIntCompositionLocal.current)
ReadStringCompositionLocal(
compositionLocal = staticSomeTextCompositionLocal,
)
TestSubcomposition {
assertEquals(someText, staticSomeTextCompositionLocal.current)
assertEquals(0, staticSomeIntCompositionLocal.current)
CompositionLocalProvider(
staticSomeIntCompositionLocal provides 42
) {
assertEquals(someText, staticSomeTextCompositionLocal.current)
assertEquals(42, staticSomeIntCompositionLocal.current)
}
}
}
}
fun validate() {
validate {
Text(someText)
}
}
someText = "Modified"
doInvalidate()
expectChanges()
validate()
}
@Test
fun deferredSubCompose_Dynamic() = compositionTest {
val invalidates = mutableListOf<RecomposeScope>()
fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
var someText = "Unmodified"
var doSubCompose: () -> Unit = { error("Sub-compose callback not set") }
compose {
invalidates.add(currentRecomposeScope)
CompositionLocalProvider(
LocalSomeTextComposition provides someText,
LocalSomeIntComposition provides 0
) {
ReadStringCompositionLocal(
compositionLocal = LocalSomeTextComposition,
)
doSubCompose = testDeferredSubcomposition {
assertEquals(
someText,
LocalSomeTextComposition.current
)
assertEquals(0, LocalSomeIntComposition.current)
CompositionLocalProvider(
LocalSomeIntComposition provides 42
) {
assertEquals(
someText,
LocalSomeTextComposition.current
)
assertEquals(
42,
LocalSomeIntComposition.current
)
}
}
}
}
fun validate() {
validate {
Text(someText)
}
}
validate()
doSubCompose()
someText = "Modified"
doInvalidate()
expectChanges()
validate()
doSubCompose()
}
@Test
fun deferredSubCompose_Static() = compositionTest {
val invalidates = mutableListOf<RecomposeScope>()
fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
var someText = "Unmodified"
var doSubCompose: () -> Unit = { error("Sub-compose callback not set") }
val staticSomeTextCompositionLocal = staticCompositionLocalOf { "Default" }
val staticSomeIntCompositionLocal = staticCompositionLocalOf { -1 }
compose {
invalidates.add(currentRecomposeScope)
CompositionLocalProvider(
staticSomeTextCompositionLocal provides someText,
staticSomeIntCompositionLocal provides 0
) {
assertEquals(someText, staticSomeTextCompositionLocal.current)
assertEquals(0, staticSomeIntCompositionLocal.current)
ReadStringCompositionLocal(
compositionLocal = staticSomeTextCompositionLocal
)
doSubCompose = testDeferredSubcomposition {
assertEquals(someText, staticSomeTextCompositionLocal.current)
assertEquals(0, staticSomeIntCompositionLocal.current)
CompositionLocalProvider(
staticSomeIntCompositionLocal provides 42
) {
assertEquals(someText, staticSomeTextCompositionLocal.current)
assertEquals(42, staticSomeIntCompositionLocal.current)
}
}
}
}
fun validate() {
validate {
Text(someText)
}
}
validate()
doSubCompose()
someText = "Modified"
doInvalidate()
expectChanges()
validate()
doSubCompose()
}
@Test
fun deferredSubCompose_Nested_Static() = compositionTest {
val invalidates = mutableListOf<RecomposeScope>()
fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
var someText = "Unmodified"
var doSubCompose1: () -> Unit = { error("Sub-compose-1 callback not set") }
var doSubCompose2: () -> Unit = { error("Sub-compose-2 callback not set") }
val staticSomeTextCompositionLocal = staticCompositionLocalOf { "Default" }
val staticSomeIntCompositionLocal = staticCompositionLocalOf { -1 }
compose {
invalidates.add(currentRecomposeScope)
CompositionLocalProvider(
staticSomeTextCompositionLocal provides someText,
staticSomeIntCompositionLocal provides 0
) {
assertEquals(someText, staticSomeTextCompositionLocal.current)
assertEquals(0, staticSomeIntCompositionLocal.current)
ReadStringCompositionLocal(
compositionLocal = staticSomeTextCompositionLocal
)
doSubCompose1 = testDeferredSubcomposition {
assertEquals(someText, staticSomeTextCompositionLocal.current)
assertEquals(0, staticSomeIntCompositionLocal.current)
doSubCompose2 = testDeferredSubcomposition {
assertEquals(someText, staticSomeTextCompositionLocal.current)
assertEquals(0, staticSomeIntCompositionLocal.current)
}
}
}
}
fun validate() {
validate {
Text(someText)
}
}
validate()
doSubCompose1()
doSubCompose2()
someText = "Modified"
doInvalidate()
expectChanges()
validate()
doSubCompose1()
doSubCompose2()
}
@Test
fun insertShouldSeePreviouslyProvidedValues() = compositionTest {
val invalidates = mutableListOf<RecomposeScope>()
fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
val someStaticString = staticCompositionLocalOf { "Default" }
var shouldRead = false
compose {
CompositionLocalProvider(
someStaticString provides "Provided A"
) {
invalidates.add(currentRecomposeScope)
if (shouldRead)
ReadStringCompositionLocal(someStaticString)
}
}
fun validate() {
validate {
if (shouldRead)
Text("Provided A")
}
}
validate()
shouldRead = true
doInvalidate()
expectChanges()
validate()
}
@Composable
fun ReadSomeDataCompositionLocal(
compositionLocal: CompositionLocal<SomeData>,
composed: StableRef<Boolean>,
) {
composed.value = true
Text(value = compositionLocal.current.value)
}
@Test
fun providingANewDataClassValueShouldNotRecompose() = compositionTest {
val invalidates = mutableListOf<RecomposeScope>()
fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
val someDataCompositionLocal = compositionLocalOf(structuralEqualityPolicy()) { SomeData() }
val composed = StableRef(false)
compose {
invalidates.add(currentRecomposeScope)
CompositionLocalProvider(
someDataCompositionLocal provides SomeData("provided")
) {
ReadSomeDataCompositionLocal(someDataCompositionLocal, composed)
}
}
assertTrue(composed.value)
composed.value = false
doInvalidate()
expectNoChanges()
assertFalse(composed.value)
}
@Test // Regression test for b/186094122
fun currentRetrievesCorrectValues() = compositionTest {
val state = mutableStateOf(0)
val providedValue = mutableStateOf(100)
val local = compositionLocalOf { 0 }
val static = staticCompositionLocalOf { -1 }
val providedStatic = mutableStateOf(2000)
var includeProviders by mutableStateOf(true)
@Composable
fun Validate(providedValue: Int) {
val currentValue = local.current
assertEquals(providedValue, currentValue)
state.value // Observe state for invalidates.
}
compose {
if (includeProviders) {
CompositionLocalProvider(
local provides providedValue.value,
static provides providedStatic.value
) {
Validate(providedValue.value)
}
}
}
// Force an providerUpdates entry to be created.
providedStatic.value++
advance()
// Force a new provider scope to be created
includeProviders = false
advance()
includeProviders = true
advance()
// Change the provided value
providedValue.value++
advance()
// Ensure the old providerUpdates is not longer contains old values.
state.value++
advance()
}
@Test // Regression test for b/193433239
fun canProvideManyProvidersSimultaneously() {
val locals = (1..1000).map { compositionLocalOf { 2 } }
val LocalTest = compositionLocalOf<Int> { error("") }
@Composable
fun Test() {
CompositionLocalProvider(
LocalTest provides 1,
) {
CompositionLocalProvider(
*locals.map { it provides 3 }.toTypedArray(),
LocalTest provides 2,
) {
assertEquals(2, LocalTest.current)
}
}
}
}
@Test // Regression test for b/193433239
fun testTheUnderlyingPropertiesOfPersistentHashMap() {
val p = persistentHashMapOf<Int, Int>(99 to 1)
val e = Array(101) { it }.map { it to it }
val c = persistentHashMapOf(*e.toTypedArray())
val n = p.builder().apply { putAll(c) }.build()
repeat(101) {
assertEquals(it, n[it])
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun testProvideAllLocals() = compositionTest {
val local1 = compositionLocalOf { 0 }
val local2 = compositionLocalOf { 0 }
val staticLocal = staticCompositionLocalOf { 0 }
var provided: Array<ProvidedValue<Int>> by mutableStateOf(emptyArray())
var actualValues = emptySet<Any>()
@Composable
fun LocalsConsumer() {
val locals by rememberUpdatedState(currentCompositionLocalContext)
DisposableEffect(Unit) {
val context = coroutineContext + TestMonotonicFrameClock(this@compositionTest)
val recomposer = Recomposer(context)
launch(context) {
recomposer.runRecomposeAndApplyChanges()
}
val composition2 = Composition(EmptyApplier(), recomposer)
composition2.setContent {
CompositionLocalProvider(locals) {
actualValues = setOf(
local1.current,
local2.current,
staticLocal.current,
)
}
}
onDispose {
composition2.dispose()
recomposer.cancel()
}
}
}
compose {
CompositionLocalProvider(*provided) {
LocalsConsumer()
}
}
advance()
assertEquals(setOf(0, 0, 0), actualValues)
provided = arrayOf(local1 provides 1)
advance()
assertEquals(setOf(1, 0, 0), actualValues)
provided = arrayOf(local1 provides 2)
advance()
assertEquals(setOf(2, 0, 0), actualValues)
provided = arrayOf(local1 provides 2, staticLocal provides 1)
advance()
assertEquals(setOf(2, 0, 1), actualValues)
provided = arrayOf(local1 provides 2, staticLocal provides 2)
advance()
assertEquals(setOf(2, 0, 2), actualValues)
provided = arrayOf(local1 provides 1, staticLocal provides 1)
advance()
assertEquals(setOf(1, 0, 1), actualValues)
provided = arrayOf(local1 provides 1, local2 provides 1, staticLocal provides 1)
advance()
assertEquals(setOf(1, 1, 1), actualValues)
provided = arrayOf(local1 provides 1, staticLocal provides 1)
advance()
assertEquals(setOf(1, 0, 1), actualValues)
provided = emptyArray()
advance()
assertEquals(setOf(0, 0, 0), actualValues)
}
}
data class SomeData(val value: String = "default")
@Stable class StableRef<T>(var value: T)