From adbd2fb51a2bd13e9c2c7e1d0960b5ca1ee3f124 Mon Sep 17 00:00:00 2001 From: ogaclejapan Date: Sat, 7 Sep 2024 19:26:13 +0900 Subject: [PATCH] Introduce Optional Query The popular TanStack Query library in the React ecosystem has a feature known as [Dependent Query](https://tanstack.com/query/v5/docs/framework/react/guides/dependent-queries). Currently, Soil Query does not have an API for conditionally executing queries. Therefore, we have implemented the Optional Query feature to enable queries based on certain conditions, such as the results of other queries. Previously, developers could achieve similar functionality by manually implementing conditional branching, but this new feature can replace that approach. --- .../query/compose/InfiniteQueryComposables.kt | 47 +++++ .../soil/query/compose/MutationComposables.kt | 27 +++ .../soil/query/compose/QueryComposables.kt | 107 +++++++++++ .../query/compose/SubscriptionComposables.kt | 49 +++++ .../compose/InfiniteQueryComposableTest.kt | 78 ++++++++ .../query/compose/MutationComposableTest.kt | 57 +++++- .../soil/query/compose/QueryComposableTest.kt | 180 +++++++++++++++++- .../compose/SubscriptionComposableTest.kt | 67 +++++++ .../kotlin/soil/query/core/Reply.kt | 5 + 9 files changed, 613 insertions(+), 4 deletions(-) create mode 100644 soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryComposables.kt create mode 100644 soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationComposables.kt create mode 100644 soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryComposables.kt create mode 100644 soil-query-compose/src/commonMain/kotlin/soil/query/compose/SubscriptionComposables.kt diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryComposables.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryComposables.kt new file mode 100644 index 0000000..5850e5d --- /dev/null +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryComposables.kt @@ -0,0 +1,47 @@ +// Copyright 2024 Soil Contributors +// SPDX-License-Identifier: Apache-2.0 + +package soil.query.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import soil.query.InfiniteQueryKey +import soil.query.QueryChunks +import soil.query.QueryClient + +/** + * Provides a conditional [rememberInfiniteQuery]. + * + * Calls [rememberInfiniteQuery] only if [keyFactory] returns a [InfiniteQueryKey] from [value]. + * + * @see rememberInfiniteQuery + */ +@Composable +fun rememberInfiniteQueryIf( + value: V, + keyFactory: (value: V) -> InfiniteQueryKey?, + config: InfiniteQueryConfig = InfiniteQueryConfig.Default, + client: QueryClient = LocalQueryClient.current +): InfiniteQueryObject, S>? { + val key = remember(value) { keyFactory(value) } ?: return null + return rememberInfiniteQuery(key, config, client) +} + +/** + * Provides a conditional [rememberInfiniteQuery]. + * + * Calls [rememberInfiniteQuery] only if [keyFactory] returns a [InfiniteQueryKey] from [value]. + * + * @see rememberInfiniteQuery + */ +@Composable +fun rememberInfiniteQueryIf( + value: V, + keyFactory: (value: V) -> InfiniteQueryKey?, + select: (chunks: QueryChunks) -> U, + config: InfiniteQueryConfig = InfiniteQueryConfig.Default, + client: QueryClient = LocalQueryClient.current +): InfiniteQueryObject? { + val key = remember(value) { keyFactory(value) } ?: return null + return rememberInfiniteQuery(key, select, config, client) +} diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationComposables.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationComposables.kt new file mode 100644 index 0000000..3cc4c3a --- /dev/null +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationComposables.kt @@ -0,0 +1,27 @@ +// Copyright 2024 Soil Contributors +// SPDX-License-Identifier: Apache-2.0 + +package soil.query.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import soil.query.MutationClient +import soil.query.MutationKey + +/** + * Provides a conditional [rememberMutation]. + * + * Calls [rememberMutation] only if [keyFactory] returns a [MutationKey] from [value]. + * + * @see rememberMutation + */ +@Composable +fun rememberMutationIf( + value: V, + keyFactory: (value: V) -> MutationKey?, + config: MutationConfig = MutationConfig.Default, + client: MutationClient = LocalMutationClient.current +): MutationObject? { + val key = remember(value) { keyFactory(value) } ?: return null + return rememberMutation(key, config, client) +} diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryComposables.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryComposables.kt new file mode 100644 index 0000000..ce5300e --- /dev/null +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryComposables.kt @@ -0,0 +1,107 @@ +// Copyright 2024 Soil Contributors +// SPDX-License-Identifier: Apache-2.0 + +package soil.query.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import soil.query.QueryClient +import soil.query.QueryKey +import kotlin.jvm.JvmName + +/** + * Provides a conditional [rememberQuery]. + * + * Calls [rememberQuery] only if [keyFactory] returns a [QueryKey] from [value]. + * + * @see rememberQuery + */ +@Composable +fun rememberQueryIf( + value: V, + keyFactory: (value: V) -> QueryKey?, + config: QueryConfig = QueryConfig.Default, + client: QueryClient = LocalQueryClient.current +): QueryObject? { + val key = remember(value) { keyFactory(value) } ?: return null + return rememberQuery(key, config, client) +} + +/** + * Provides a conditional [rememberQuery]. + * + * Calls [rememberQuery] only if [keyFactory] returns a [QueryKey] from [value]. + * + * @see rememberQuery + */ +@Composable +fun rememberQueryIf( + value: V, + keyFactory: (value: V) -> QueryKey?, + select: (T) -> U, + config: QueryConfig = QueryConfig.Default, + client: QueryClient = LocalQueryClient.current +): QueryObject? { + val key = remember(value) { keyFactory(value) } ?: return null + return rememberQuery(key, select, config, client) +} + +/** + * Provides a conditional [rememberQuery]. + * + * Calls [rememberQuery] only if [keyPairFactory] returns a [Pair] of [QueryKey]s from [value]. + * + * @see rememberQuery + */ +@JvmName("rememberQueryIfWithPair") +@Composable +fun rememberQueryIf( + value: V, + keyPairFactory: (value: V) -> Pair, QueryKey>?, + transform: (T1, T2) -> R, + config: QueryConfig = QueryConfig.Default, + client: QueryClient = LocalQueryClient.current +): QueryObject? { + val keyPair = remember(value) { keyPairFactory(value) } ?: return null + return rememberQuery(keyPair.first, keyPair.second, transform, config, client) +} + +/** + * Provides a conditional [rememberQuery]. + * + * Calls [rememberQuery] only if [keyTripleFactory] returns a [Triple] of [QueryKey]s from [value]. + * + * @see rememberQuery + */ +@JvmName("rememberQueryIfWithTriple") +@Composable +fun rememberQueryIf( + value: V, + keyTripleFactory: (value: V) -> Triple, QueryKey, QueryKey>?, + transform: (T1, T2, T3) -> R, + config: QueryConfig = QueryConfig.Default, + client: QueryClient = LocalQueryClient.current +): QueryObject? { + val keyTriple = remember(value) { keyTripleFactory(value) } ?: return null + return rememberQuery(keyTriple.first, keyTriple.second, keyTriple.third, transform, config, client) +} + +/** + * Provides a conditional [rememberQuery]. + * + * Calls [rememberQuery] only if [keyListFactory] returns a [List] of [QueryKey]s from [value]. + * + * @see rememberQuery + */ +@JvmName("rememberQueryIfWithList") +@Composable +fun rememberQueryIf( + value: V, + keyListFactory: (value: V) -> List>?, + transform: (List) -> R, + config: QueryConfig = QueryConfig.Default, + client: QueryClient = LocalQueryClient.current +): QueryObject? { + val keys = remember(value) { keyListFactory(value) } ?: return null + return rememberQuery(keys, transform, config, client) +} diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/SubscriptionComposables.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/SubscriptionComposables.kt new file mode 100644 index 0000000..6080daa --- /dev/null +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/SubscriptionComposables.kt @@ -0,0 +1,49 @@ +// Copyright 2024 Soil Contributors +// SPDX-License-Identifier: Apache-2.0 + +package soil.query.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import soil.query.SubscriptionClient +import soil.query.SubscriptionKey +import soil.query.annotation.ExperimentalSoilQueryApi + +/** + * Provides a conditional [rememberSubscription]. + * + * Calls [rememberSubscription] only if [keyFactory] returns a [SubscriptionKey] from [value]. + * + * @see rememberSubscription + */ +@ExperimentalSoilQueryApi +@Composable +fun rememberSubscriptionIf( + value: V, + keyFactory: (value: V) -> SubscriptionKey?, + config: SubscriptionConfig = SubscriptionConfig.Default, + client: SubscriptionClient = LocalSubscriptionClient.current +): SubscriptionObject? { + val key = remember(value) { keyFactory(value) } ?: return null + return rememberSubscription(key, config, client) +} + +/** + * Provides a conditional [rememberSubscription]. + * + * Calls [rememberSubscription] only if [keyFactory] returns a [SubscriptionKey] from [value]. + * + * @see rememberSubscription + */ +@ExperimentalSoilQueryApi +@Composable +fun rememberSubscriptionIf( + value: V, + keyFactory: (value: V) -> SubscriptionKey?, + select: (T) -> U, + config: SubscriptionConfig = SubscriptionConfig.Default, + client: SubscriptionClient = LocalSubscriptionClient.current +): SubscriptionObject? { + val key = remember(value) { keyFactory(value) } ?: return null + return rememberSubscription(key, select, config, client) +} diff --git a/soil-query-compose/src/commonTest/kotlin/soil/query/compose/InfiniteQueryComposableTest.kt b/soil-query-compose/src/commonTest/kotlin/soil/query/compose/InfiniteQueryComposableTest.kt index 384a8cd..fce05ba 100644 --- a/soil-query-compose/src/commonTest/kotlin/soil/query/compose/InfiniteQueryComposableTest.kt +++ b/soil-query-compose/src/commonTest/kotlin/soil/query/compose/InfiniteQueryComposableTest.kt @@ -6,7 +6,11 @@ package soil.query.compose import androidx.compose.foundation.layout.Column import androidx.compose.material.Button import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi @@ -32,6 +36,7 @@ import soil.query.compose.tooling.QueryPreviewClient import soil.query.compose.tooling.SwrPreviewClient import soil.query.core.Marker import soil.query.core.Reply +import soil.query.core.orNone import soil.testing.UnitTest import kotlin.test.Test @@ -249,6 +254,79 @@ class InfiniteQueryComposableTest : UnitTest() { onNodeWithTag("query").assertTextEquals("ChunkSize: 1") } + @Test + fun testRememberInfiniteQueryIf() = runComposeUiTest { + val key = TestInfiniteQueryKey() + val client = SwrCache(coroutineScope = SwrCacheScope()) + setContent { + SwrClientProvider(client) { + var enabled by remember { mutableStateOf(false) } + val query = rememberInfiniteQueryIf(enabled, keyFactory = { if (it) key else null }) + Column { + Button(onClick = { enabled = !enabled }, modifier = Modifier.testTag("toggle")) { + Text("Toggle") + } + when (val reply = query?.reply.orNone()) { + is Reply.Some -> { + reply.value.forEach { chunk -> + Text( + "Size: ${chunk.data.size} - Page: ${chunk.param.page}", + modifier = Modifier.testTag("query") + ) + } + } + + is Reply.None -> Unit + } + } + } + } + + waitForIdle() + onNodeWithTag("query").assertDoesNotExist() + onNodeWithTag("toggle").performClick() + + waitUntilAtLeastOneExists(hasTestTag("query")) + onNodeWithTag("query").assertTextEquals("Size: 10 - Page: 0") + } + + @Test + fun testRememberInfiniteQueryIf_select() = runComposeUiTest { + val key = TestInfiniteQueryKey() + val client = SwrCache(coroutineScope = SwrCacheScope()) + setContent { + SwrClientProvider(client) { + var enabled by remember { mutableStateOf(false) } + val query = rememberInfiniteQueryIf( + value = enabled, + keyFactory = { if (it) key else null }, + select = { it.chunkedData }) + Column { + Button(onClick = { enabled = !enabled }, modifier = Modifier.testTag("toggle")) { + Text("Toggle") + } + when (val reply = query?.reply.orNone()) { + is Reply.Some -> { + reply.value.forEach { data -> + Text(data, modifier = Modifier.testTag("query")) + } + } + + is Reply.None -> Unit + } + } + } + } + + waitForIdle() + onNodeWithTag("query").assertDoesNotExist() + onNodeWithTag("toggle").performClick() + + waitUntilAtLeastOneExists(hasTestTag("query")) + onAllNodes(hasTestTag("query")).assertCountEquals(10) + } + + private class TestInfiniteQueryKey : InfiniteQueryKey, PageParam> by buildInfiniteQueryKey( id = Id, fetch = { param -> diff --git a/soil-query-compose/src/commonTest/kotlin/soil/query/compose/MutationComposableTest.kt b/soil-query-compose/src/commonTest/kotlin/soil/query/compose/MutationComposableTest.kt index 497ca82..e6da13c 100644 --- a/soil-query-compose/src/commonTest/kotlin/soil/query/compose/MutationComposableTest.kt +++ b/soil-query-compose/src/commonTest/kotlin/soil/query/compose/MutationComposableTest.kt @@ -3,9 +3,14 @@ package soil.query.compose +import androidx.compose.foundation.layout.Column import androidx.compose.material.Button import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi @@ -25,6 +30,7 @@ import soil.query.compose.tooling.MutationPreviewClient import soil.query.compose.tooling.SwrPreviewClient import soil.query.core.Marker import soil.query.core.Reply +import soil.query.core.orNone import soil.query.test.test import soil.testing.UnitTest import kotlin.test.Test @@ -61,10 +67,10 @@ class MutationComposableTest : UnitTest() { } } - // TODO: I don't know why but it's broken. - // Related issue: https://github.com/JetBrains/compose-multiplatform-core/blob/46232e6533a71625f7599c206594fca5e2e28e09/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/Assertions.skikoMain.kt#L26 - // onNodeWithTag("result").assertIsNotDisplayed() + waitForIdle() + onNodeWithTag("result").assertDoesNotExist() onNodeWithTag("mutation").performClick() + waitUntilExactlyOneExists(hasTestTag("result")) onNodeWithTag("result").assertTextEquals("Soil - 1") } @@ -227,6 +233,51 @@ class MutationComposableTest : UnitTest() { onNodeWithTag("mutation").assertTextEquals("Error") } + @Test + fun testRememberMutationIf() = runComposeUiTest { + val key = TestMutationKey() + val client = SwrCache(coroutineScope = SwrCacheScope()) + setContent { + SwrClientProvider(client) { + var enabled by remember { mutableStateOf(false) } + val mutation = rememberMutationIf(enabled, { if (it) key else null }) + Column { + Button(onClick = { enabled = !enabled }, modifier = Modifier.testTag("toggle")) { + Text("Toggle") + } + when (val reply = mutation?.reply.orNone()) { + is Reply.Some -> Text(reply.value, modifier = Modifier.testTag("result")) + is Reply.None -> Unit + } + val scope = rememberCoroutineScope() + if (mutation != null) { + Button( + onClick = { + scope.launch { + mutation.mutate(TestForm("Soil", 1)) + } + }, + modifier = Modifier.testTag("mutation") + ) { + Text("Mutate") + } + } + } + } + } + + waitForIdle() + onNodeWithTag("mutation").assertDoesNotExist() + onNodeWithTag("toggle").performClick() + + waitForIdle() + onNodeWithTag("result").assertDoesNotExist() + onNodeWithTag("mutation").performClick() + + waitUntilExactlyOneExists(hasTestTag("result")) + onNodeWithTag("result").assertTextEquals("Soil - 1") + } + private class TestMutationKey : MutationKey by buildMutationKey( mutate = { form -> "${form.name} - ${form.age}" diff --git a/soil-query-compose/src/commonTest/kotlin/soil/query/compose/QueryComposableTest.kt b/soil-query-compose/src/commonTest/kotlin/soil/query/compose/QueryComposableTest.kt index 784587b..9121155 100644 --- a/soil-query-compose/src/commonTest/kotlin/soil/query/compose/QueryComposableTest.kt +++ b/soil-query-compose/src/commonTest/kotlin/soil/query/compose/QueryComposableTest.kt @@ -3,13 +3,20 @@ package soil.query.compose +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Button import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.test.waitUntilExactlyOneExists import soil.query.QueryId @@ -22,6 +29,7 @@ import soil.query.compose.tooling.QueryPreviewClient import soil.query.compose.tooling.SwrPreviewClient import soil.query.core.Marker import soil.query.core.Reply +import soil.query.core.orNone import soil.query.test.test import soil.testing.UnitTest import kotlin.test.Test @@ -164,7 +172,6 @@ class QueryComposableTest : UnitTest() { onNodeWithTag("query").assertTextEquals("Hello, Compose!|Hello, Soil!|Hello, Kotlin!") } - @Test fun testRememberQuery_loadingPreview() = runComposeUiTest { val key = TestQueryKey() @@ -249,6 +256,177 @@ class QueryComposableTest : UnitTest() { onNodeWithTag("query").assertTextEquals("Hello, Query!") } + @Test + fun testRememberQueryIf() = runComposeUiTest { + val key = TestQueryKey() + val client = SwrCache(coroutineScope = SwrCacheScope()) + setContent { + SwrClientProvider(client) { + var enabled by remember { mutableStateOf(false) } + val query = rememberQueryIf(enabled, keyFactory = { if (it) key else null }) + Column { + Button(onClick = { enabled = !enabled }, modifier = Modifier.testTag("toggle")) { + Text("Toggle") + } + when (val reply = query?.reply.orNone()) { + is Reply.Some -> Text(reply.value, modifier = Modifier.testTag("query")) + is Reply.None -> Unit + } + } + } + } + + waitForIdle() + onNodeWithTag("query").assertDoesNotExist() + onNodeWithTag("toggle").performClick() + + waitUntilExactlyOneExists(hasTestTag("query")) + onNodeWithTag("query").assertTextEquals("Hello, Soil!") + } + + @Test + fun testRememberQueryIf_select() = runComposeUiTest { + val key = TestQueryKey() + val client = SwrCache(coroutineScope = SwrCacheScope()).test { + on(key.id) { "Hello, Compose!" } + } + setContent { + SwrClientProvider(client) { + var enabled by remember { mutableStateOf(false) } + val query = rememberQueryIf( + value = enabled, + keyFactory = { if (it) key else null }, + select = { it.uppercase() } + ) + Column { + Button(onClick = { enabled = !enabled }, modifier = Modifier.testTag("toggle")) { + Text("Toggle") + } + when (val reply = query?.reply.orNone()) { + is Reply.Some -> Text(reply.value, modifier = Modifier.testTag("query")) + is Reply.None -> Unit + } + } + } + } + + waitForIdle() + onNodeWithTag("query").assertDoesNotExist() + onNodeWithTag("toggle").performClick() + + waitUntilExactlyOneExists(hasTestTag("query")) + onNodeWithTag("query").assertTextEquals("HELLO, COMPOSE!") + } + + @Test + fun testRememberQueryIf_combineTwo() = runComposeUiTest { + val key1 = TestQueryKey(number = 1) + val key2 = TestQueryKey(number = 2) + val client = SwrCache(coroutineScope = SwrCacheScope()).test { + on(key1.id) { "Hello, Compose!" } + on(key2.id) { "Hello, Soil!" } + } + setContent { + SwrClientProvider(client) { + var enabled by remember { mutableStateOf(false) } + val query = rememberQueryIf( + value = enabled, + keyPairFactory = { if (it) key1 to key2 else null }, + transform = { a, b -> a + b }) + Column { + Button(onClick = { enabled = !enabled }, modifier = Modifier.testTag("toggle")) { + Text("Toggle") + } + when (val reply = query?.reply.orNone()) { + is Reply.Some -> Text(reply.value, modifier = Modifier.testTag("query")) + is Reply.None -> Unit + } + } + } + } + + waitForIdle() + onNodeWithTag("query").assertDoesNotExist() + onNodeWithTag("toggle").performClick() + + waitUntilExactlyOneExists(hasTestTag("query")) + onNodeWithTag("query").assertTextEquals("Hello, Compose!Hello, Soil!") + } + + @Test + fun testRememberQueryIf_combineThree() = runComposeUiTest { + val key1 = TestQueryKey(number = 1) + val key2 = TestQueryKey(number = 2) + val key3 = TestQueryKey(number = 3) + val client = SwrCache(coroutineScope = SwrCacheScope()).test { + on(key1.id) { "Hello, Compose!" } + on(key2.id) { "Hello, Soil!" } + on(key3.id) { "Hello, Kotlin!" } + } + setContent { + SwrClientProvider(client) { + var enabled by remember { mutableStateOf(false) } + val query = rememberQueryIf( + value = enabled, + keyTripleFactory = { if (it) Triple(key1, key2, key3) else null }, + transform = { a, b, c -> a + b + c }) + Column { + Button(onClick = { enabled = !enabled }, modifier = Modifier.testTag("toggle")) { + Text("Toggle") + } + when (val reply = query?.reply.orNone()) { + is Reply.Some -> Text(reply.value, modifier = Modifier.testTag("query")) + is Reply.None -> Unit + } + } + } + } + + waitForIdle() + onNodeWithTag("query").assertDoesNotExist() + onNodeWithTag("toggle").performClick() + + waitUntilExactlyOneExists(hasTestTag("query")) + onNodeWithTag("query").assertTextEquals("Hello, Compose!Hello, Soil!Hello, Kotlin!") + } + + @Test + fun testRememberQueryIf_combineN() = runComposeUiTest { + val key1 = TestQueryKey(number = 1) + val key2 = TestQueryKey(number = 2) + val key3 = TestQueryKey(number = 3) + val client = SwrCache(coroutineScope = SwrCacheScope()).test { + on(key1.id) { "Hello, Compose!" } + on(key2.id) { "Hello, Soil!" } + on(key3.id) { "Hello, Kotlin!" } + } + setContent { + SwrClientProvider(client) { + var enabled by remember { mutableStateOf(false) } + val query = rememberQueryIf( + value = enabled, + keyListFactory = { if (it) listOf(key1, key2, key3) else null }, + transform = { it.joinToString("|") }) + Column { + Button(onClick = { enabled = !enabled }, modifier = Modifier.testTag("toggle")) { + Text("Toggle") + } + when (val reply = query?.reply.orNone()) { + is Reply.Some -> Text(reply.value, modifier = Modifier.testTag("query")) + is Reply.None -> Unit + } + } + } + } + + waitForIdle() + onNodeWithTag("query").assertDoesNotExist() + onNodeWithTag("toggle").performClick() + + waitUntilExactlyOneExists(hasTestTag("query")) + onNodeWithTag("query").assertTextEquals("Hello, Compose!|Hello, Soil!|Hello, Kotlin!") + } + private class TestQueryKey( number: Int = 1 ) : QueryKey by buildQueryKey( diff --git a/soil-query-compose/src/commonTest/kotlin/soil/query/compose/SubscriptionComposableTest.kt b/soil-query-compose/src/commonTest/kotlin/soil/query/compose/SubscriptionComposableTest.kt index 2e02f95..492b75c 100644 --- a/soil-query-compose/src/commonTest/kotlin/soil/query/compose/SubscriptionComposableTest.kt +++ b/soil-query-compose/src/commonTest/kotlin/soil/query/compose/SubscriptionComposableTest.kt @@ -3,13 +3,20 @@ package soil.query.compose +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Button import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.test.waitUntilExactlyOneExists import kotlinx.coroutines.flow.MutableStateFlow @@ -26,6 +33,7 @@ import soil.query.compose.tooling.SubscriptionPreviewClient import soil.query.compose.tooling.SwrPreviewClient import soil.query.core.Marker import soil.query.core.Reply +import soil.query.core.orNone import soil.query.test.testPlus import soil.testing.UnitTest import kotlin.test.Test @@ -196,6 +204,65 @@ class SubscriptionComposableTest : UnitTest() { onNodeWithTag("subscription").assertTextEquals("Error") } + @Test + fun testRememberSubscriptionIf() = runComposeUiTest { + val key = TestSubscriptionKey() + val client = SwrCachePlus(coroutineScope = SwrCacheScope()) + setContent { + SwrClientProvider(client) { + var enabled by remember { mutableStateOf(false) } + val subscription = rememberSubscriptionIf(enabled, keyFactory = { if (it) key else null }) + Column { + Button(onClick = { enabled = !enabled }, modifier = Modifier.testTag("toggle")) { + Text("Toggle") + } + when (val reply = subscription?.reply.orNone()) { + is Reply.Some -> Text(reply.value, modifier = Modifier.testTag("subscription")) + is Reply.None -> Unit + } + } + } + } + + waitForIdle() + onNodeWithTag("subscription").assertDoesNotExist() + onNodeWithTag("toggle").performClick() + + waitUntilExactlyOneExists(hasTestTag("subscription")) + onNodeWithTag("subscription").assertTextEquals("Hello, Soil!") + } + + @Test + fun testRememberSubscriptionIf_select() = runComposeUiTest { + val key = TestSubscriptionKey() + val client = SwrCachePlus(coroutineScope = SwrCacheScope()).testPlus { + on(key.id) { MutableStateFlow("Hello, Compose!") } + } + setContent { + SwrClientProvider(client) { + var enabled by remember { mutableStateOf(false) } + val subscription = + rememberSubscriptionIf(enabled, keyFactory = { if (it) key else null }, select = { it.uppercase() }) + Column { + Button(onClick = { enabled = !enabled }, modifier = Modifier.testTag("toggle")) { + Text("Toggle") + } + when (val reply = subscription?.reply.orNone()) { + is Reply.Some -> Text(reply.value, modifier = Modifier.testTag("subscription")) + is Reply.None -> Unit + } + } + } + } + + waitForIdle() + onNodeWithTag("subscription").assertDoesNotExist() + onNodeWithTag("toggle").performClick() + + waitUntilExactlyOneExists(hasTestTag("subscription")) + onNodeWithTag("subscription").assertTextEquals("HELLO, COMPOSE!") + } + private class TestSubscriptionKey : SubscriptionKey by buildSubscriptionKey( id = Id, subscribe = { MutableStateFlow("Hello, Soil!") } diff --git a/soil-query-core/src/commonMain/kotlin/soil/query/core/Reply.kt b/soil-query-core/src/commonMain/kotlin/soil/query/core/Reply.kt index f018b86..a017abb 100644 --- a/soil-query-core/src/commonMain/kotlin/soil/query/core/Reply.kt +++ b/soil-query-core/src/commonMain/kotlin/soil/query/core/Reply.kt @@ -50,6 +50,11 @@ fun Reply.getOrElse(default: () -> T): T = when (this) { is Reply.Some -> value } +/** + * Returns the value of the [Reply.Some] instance, or the [default] value if there is no reply yet ([Reply.None]). + */ +inline fun Reply?.orNone(): Reply = this ?: Reply.none() + /** * Transforms the value of the [Reply.Some] instance using the provided [transform] function, * or returns [Reply.None] if there is no reply yet ([Reply.None]).