Skip to content

Commit

Permalink
Introduce Optional Query
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ogaclejapan committed Sep 16, 2024
1 parent d027a36 commit adbd2fb
Show file tree
Hide file tree
Showing 9 changed files with 613 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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 <T, S, V> rememberInfiniteQueryIf(
value: V,
keyFactory: (value: V) -> InfiniteQueryKey<T, S>?,
config: InfiniteQueryConfig = InfiniteQueryConfig.Default,
client: QueryClient = LocalQueryClient.current
): InfiniteQueryObject<QueryChunks<T, S>, 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 <T, S, U, V> rememberInfiniteQueryIf(
value: V,
keyFactory: (value: V) -> InfiniteQueryKey<T, S>?,
select: (chunks: QueryChunks<T, S>) -> U,
config: InfiniteQueryConfig = InfiniteQueryConfig.Default,
client: QueryClient = LocalQueryClient.current
): InfiniteQueryObject<U, S>? {
val key = remember(value) { keyFactory(value) } ?: return null
return rememberInfiniteQuery(key, select, config, client)
}
Original file line number Diff line number Diff line change
@@ -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 <T, S, V> rememberMutationIf(
value: V,
keyFactory: (value: V) -> MutationKey<T, S>?,
config: MutationConfig = MutationConfig.Default,
client: MutationClient = LocalMutationClient.current
): MutationObject<T, S>? {
val key = remember(value) { keyFactory(value) } ?: return null
return rememberMutation(key, config, client)
}
Original file line number Diff line number Diff line change
@@ -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 <T, V> rememberQueryIf(
value: V,
keyFactory: (value: V) -> QueryKey<T>?,
config: QueryConfig = QueryConfig.Default,
client: QueryClient = LocalQueryClient.current
): QueryObject<T>? {
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 <T, U, V> rememberQueryIf(
value: V,
keyFactory: (value: V) -> QueryKey<T>?,
select: (T) -> U,
config: QueryConfig = QueryConfig.Default,
client: QueryClient = LocalQueryClient.current
): QueryObject<U>? {
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 <T1, T2, R, V> rememberQueryIf(
value: V,
keyPairFactory: (value: V) -> Pair<QueryKey<T1>, QueryKey<T2>>?,
transform: (T1, T2) -> R,
config: QueryConfig = QueryConfig.Default,
client: QueryClient = LocalQueryClient.current
): QueryObject<R>? {
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 <T1, T2, T3, R, V> rememberQueryIf(
value: V,
keyTripleFactory: (value: V) -> Triple<QueryKey<T1>, QueryKey<T2>, QueryKey<T3>>?,
transform: (T1, T2, T3) -> R,
config: QueryConfig = QueryConfig.Default,
client: QueryClient = LocalQueryClient.current
): QueryObject<R>? {
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 <T, R, V> rememberQueryIf(
value: V,
keyListFactory: (value: V) -> List<QueryKey<T>>?,
transform: (List<T>) -> R,
config: QueryConfig = QueryConfig.Default,
client: QueryClient = LocalQueryClient.current
): QueryObject<R>? {
val keys = remember(value) { keyListFactory(value) } ?: return null
return rememberQuery(keys, transform, config, client)
}
Original file line number Diff line number Diff line change
@@ -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 <T, V> rememberSubscriptionIf(
value: V,
keyFactory: (value: V) -> SubscriptionKey<T>?,
config: SubscriptionConfig = SubscriptionConfig.Default,
client: SubscriptionClient = LocalSubscriptionClient.current
): SubscriptionObject<T>? {
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 <T, U, V> rememberSubscriptionIf(
value: V,
keyFactory: (value: V) -> SubscriptionKey<T>?,
select: (T) -> U,
config: SubscriptionConfig = SubscriptionConfig.Default,
client: SubscriptionClient = LocalSubscriptionClient.current
): SubscriptionObject<U>? {
val key = remember(value) { keyFactory(value) } ?: return null
return rememberSubscription(key, select, config, client)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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<List<String>, PageParam> by buildInfiniteQueryKey(
id = Id,
fetch = { param ->
Expand Down
Loading

0 comments on commit adbd2fb

Please sign in to comment.