From f6dbe432e3493e5ff952e6bd90b1539d7552818e Mon Sep 17 00:00:00 2001 From: ogaclejapan Date: Sat, 31 Aug 2024 18:34:12 +0900 Subject: [PATCH] Enabling behavior customization through the strategy layer. We've adjusted the strategy API implemented in #69 so that it can be used for both queries and mutations as needed. refs: #69 --- .../soil/query/compose/CachingStrategy.kt | 108 +++++++++++++ .../soil/query/compose/InfiniteQueryConfig.kt | 7 +- .../query/compose/InfiniteQueryStrategy.kt | 40 +++++ .../soil/query/compose/MutationComposable.kt | 5 +- .../soil/query/compose/MutationConfig.kt | 5 +- .../soil/query/compose/MutationStrategy.kt | 30 ++++ .../query/compose/QueryCachingStrategy.kt | 153 ------------------ .../kotlin/soil/query/compose/QueryConfig.kt | 7 +- .../soil/query/compose/QueryStrategy.kt | 38 +++++ .../compose/InfiniteQueryComposableTest.kt | 2 +- .../query/compose/MutationComposableTest.kt | 1 + .../soil/query/compose/QueryComposableTest.kt | 2 +- 12 files changed, 230 insertions(+), 168 deletions(-) create mode 100644 soil-query-compose/src/commonMain/kotlin/soil/query/compose/CachingStrategy.kt create mode 100644 soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryStrategy.kt create mode 100644 soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationStrategy.kt delete mode 100644 soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryCachingStrategy.kt create mode 100644 soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryStrategy.kt diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/CachingStrategy.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/CachingStrategy.kt new file mode 100644 index 0000000..bb61533 --- /dev/null +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/CachingStrategy.kt @@ -0,0 +1,108 @@ +// Copyright 2024 Soil Contributors +// SPDX-License-Identifier: Apache-2.0 + +package soil.query.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import kotlinx.coroutines.flow.StateFlow +import soil.query.InfiniteQueryRef +import soil.query.QueryChunks +import soil.query.QueryRef +import soil.query.QueryState +import soil.query.annotation.ExperimentalSoilQueryApi +import soil.query.core.UniqueId +import soil.query.core.isNone + +/** + * A mechanism to finely adjust the behavior of the query results on a component basis in Composable functions. + * + * In addition to the default behavior provided by Stale-While-Revalidate, two experimental strategies are now available: + * + * 1. Cache-First: + * This strategy avoids requesting data re-fetch as long as valid cached data is available. + * It prioritizes using the cached data over network requests. + * + * 2. Network-First: + * This strategy maintains the initial loading state until data is re-fetched, regardless of the presence of valid cached data. + * This ensures that the most up-to-date data is always displayed. + * + * + * Background: + * During in-app development, there are scenarios where returning cached data first can lead to issues. + * For example, if the externally updated data state is not accurately reflected on the screen, inconsistencies can occur. + * This is particularly problematic in processes that automatically redirect to other screens based on the data state. + * + * On the other hand, there are situations where data re-fetching should be suppressed to minimize data traffic. + * In such cases, setting a long staleTime in QueryOptions is not sufficient, as specific conditions for reducing data traffic may persist. + */ +@Suppress("unused") +sealed interface CachingStrategy { + + @ExperimentalSoilQueryApi + @Stable + data object CacheFirst : CachingStrategy, QueryStrategy, InfiniteQueryStrategy { + @Composable + override fun collectAsState(query: QueryRef): QueryState { + return collectAsState(query.key.id, query.state, query::resume) + } + + @Composable + override fun collectAsState(query: InfiniteQueryRef): QueryState> { + return collectAsState(query.key.id, query.state, query::resume) + } + + @Composable + private inline fun collectAsState( + key: UniqueId, + flow: StateFlow>, + crossinline resume: suspend () -> Unit + ): QueryState { + val state by flow.collectAsState() + LaunchedEffect(key) { + val currentValue = flow.value + if (currentValue.reply.isNone || currentValue.isInvalidated) { + resume() + } + } + return state + } + } + + @ExperimentalSoilQueryApi + @Stable + data object NetworkFirst : CachingStrategy, QueryStrategy, InfiniteQueryStrategy { + @Composable + override fun collectAsState(query: QueryRef): QueryState { + return collectAsState(query.key.id, query.state, query::resume) + } + + @Composable + override fun collectAsState(query: InfiniteQueryRef): QueryState> { + return collectAsState(query.key.id, query.state, query::resume) + } + + @Composable + private inline fun collectAsState( + key: UniqueId, + flow: StateFlow>, + crossinline resume: suspend () -> Unit + ): QueryState { + var resumed by rememberSaveable(key) { mutableStateOf(false) } + val initialValue = if (resumed) flow.value else QueryState.initial() + val state = produceState(initialValue, key) { + resume() + resumed = true + flow.collect { value = it } + } + return state.value + } + } +} diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryConfig.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryConfig.kt index 2949da9..8cf8ac9 100644 --- a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryConfig.kt +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryConfig.kt @@ -14,13 +14,12 @@ import soil.query.core.Marker */ @Immutable data class InfiniteQueryConfig internal constructor( - val strategy: QueryCachingStrategy, + val strategy: InfiniteQueryStrategy, val marker: Marker ) { - @Suppress("MemberVisibilityCanBePrivate") class Builder { - var strategy: QueryCachingStrategy = Default.strategy + var strategy: InfiniteQueryStrategy = Default.strategy var marker: Marker = Default.marker fun build() = InfiniteQueryConfig( @@ -31,7 +30,7 @@ data class InfiniteQueryConfig internal constructor( companion object { val Default = InfiniteQueryConfig( - strategy = QueryCachingStrategy.Default, + strategy = InfiniteQueryStrategy.Default, marker = Marker.None ) } diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryStrategy.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryStrategy.kt new file mode 100644 index 0000000..143fc59 --- /dev/null +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/InfiniteQueryStrategy.kt @@ -0,0 +1,40 @@ +// Copyright 2024 Soil Contributors +// SPDX-License-Identifier: Apache-2.0 + +package soil.query.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import soil.query.InfiniteQueryRef +import soil.query.QueryChunks +import soil.query.QueryState + +/** + * A mechanism to finely adjust the behavior of the infinite-query on a component basis in Composable functions. + * + * If you want to customize, please create a class implementing [InfiniteQueryStrategy]. + * For example, this is useful when you want to switch your implementation to `collectAsStateWithLifecycle`. + * + * @see CachingStrategy + */ +@Stable +interface InfiniteQueryStrategy { + + @Composable + fun collectAsState(query: InfiniteQueryRef): QueryState> + + companion object Default : InfiniteQueryStrategy { + + @Composable + override fun collectAsState(query: InfiniteQueryRef): QueryState> { + val state by query.state.collectAsState() + LaunchedEffect(query.key.id) { + query.resume() + } + return state + } + } +} diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationComposable.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationComposable.kt index 9350917..3d83d08 100644 --- a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationComposable.kt +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationComposable.kt @@ -4,8 +4,6 @@ package soil.query.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import soil.query.MutationClient @@ -32,8 +30,7 @@ fun rememberMutation( ): MutationObject { val scope = rememberCoroutineScope() val mutation = remember(key) { client.getMutation(key, config.marker).also { it.launchIn(scope) } } - val state by mutation.state.collectAsState() - return state.toObject(mutation = mutation) + return config.strategy.collectAsState(mutation).toObject(mutation = mutation) } private fun MutationState.toObject( diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationConfig.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationConfig.kt index 2263198..3ce11d5 100644 --- a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationConfig.kt +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationConfig.kt @@ -13,20 +13,23 @@ import soil.query.core.Marker */ @Immutable data class MutationConfig internal constructor( + val strategy: MutationStrategy, val marker: Marker ) { - @Suppress("MemberVisibilityCanBePrivate") class Builder { + var strategy: MutationStrategy = Default.strategy var marker: Marker = Default.marker fun build() = MutationConfig( + strategy = strategy, marker = marker ) } companion object { val Default = MutationConfig( + strategy = MutationStrategy.Default, marker = Marker.None ) } diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationStrategy.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationStrategy.kt new file mode 100644 index 0000000..f5ed874 --- /dev/null +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/MutationStrategy.kt @@ -0,0 +1,30 @@ +// Copyright 2024 Soil Contributors +// SPDX-License-Identifier: Apache-2.0 + +package soil.query.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.collectAsState +import soil.query.MutationRef +import soil.query.MutationState + +/** + * A mechanism to finely adjust the behavior of the mutation on a component basis in Composable functions. + * + * If you want to customize, please create a class implementing [MutationStrategy]. + * For example, this is useful when you want to switch your implementation to `collectAsStateWithLifecycle`. + */ +@Stable +interface MutationStrategy { + + @Composable + fun collectAsState(mutation: MutationRef): MutationState + + companion object Default : MutationStrategy { + @Composable + override fun collectAsState(mutation: MutationRef): MutationState { + return mutation.state.collectAsState().value + } + } +} diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryCachingStrategy.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryCachingStrategy.kt deleted file mode 100644 index 51bef6e..0000000 --- a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryCachingStrategy.kt +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2024 Soil Contributors -// SPDX-License-Identifier: Apache-2.0 - -package soil.query.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import kotlinx.coroutines.flow.StateFlow -import soil.query.InfiniteQueryRef -import soil.query.QueryChunks -import soil.query.QueryRef -import soil.query.QueryState -import soil.query.annotation.ExperimentalSoilQueryApi -import soil.query.core.UniqueId -import soil.query.core.isNone - -/** - * A mechanism to finely adjust the behavior of the query results on a component basis in Composable functions. - * - * In addition to the default behavior provided by Stale-While-Revalidate, two experimental strategies are now available: - * - * 1. Cache-First: - * This strategy avoids requesting data re-fetch as long as valid cached data is available. - * It prioritizes using the cached data over network requests. - * - * 2. Network-First: - * This strategy maintains the initial loading state until data is re-fetched, regardless of the presence of valid cached data. - * This ensures that the most up-to-date data is always displayed. - * - * If you want to customize further, please create a class implementing [QueryCachingStrategy]. - * However, as this is an experimental API, the interface may change significantly in future versions. - * - * In future updates, we plan to provide additional options for more granular control over the behavior at the component level. - * - * Background: - * During in-app development, there are scenarios where returning cached data first can lead to issues. - * For example, if the externally updated data state is not accurately reflected on the screen, inconsistencies can occur. - * This is particularly problematic in processes that automatically redirect to other screens based on the data state. - * - * On the other hand, there are situations where data re-fetching should be suppressed to minimize data traffic. - * In such cases, setting a long staleTime in QueryOptions is not sufficient, as specific conditions for reducing data traffic may persist. - */ -@Stable -interface QueryCachingStrategy { - @Composable - fun collectAsState(query: QueryRef): QueryState - - @Composable - fun collectAsState(query: InfiniteQueryRef): QueryState> - - companion object Default : QueryCachingStrategy by StaleWhileRevalidate { - - @Suppress("FunctionName") - @ExperimentalSoilQueryApi - fun CacheFirst(): QueryCachingStrategy = CacheFirst - - @Suppress("FunctionName") - @ExperimentalSoilQueryApi - fun NetworkFirst(): QueryCachingStrategy = NetworkFirst - } -} - -@Stable -private object StaleWhileRevalidate : QueryCachingStrategy { - @Composable - override fun collectAsState(query: QueryRef): QueryState { - return collectAsState(key = query.key.id, flow = query.state, resume = query::resume) - } - - @Composable - override fun collectAsState(query: InfiniteQueryRef): QueryState> { - return collectAsState(key = query.key.id, flow = query.state, resume = query::resume) - } - - @Composable - private inline fun collectAsState( - key: UniqueId, - flow: StateFlow>, - crossinline resume: suspend () -> Unit - ): QueryState { - val state by flow.collectAsState() - LaunchedEffect(key) { - resume() - } - return state - } -} - - -@Stable -private object CacheFirst : QueryCachingStrategy { - @Composable - override fun collectAsState(query: QueryRef): QueryState { - return collectAsState(query.key.id, query.state, query::resume) - } - - @Composable - override fun collectAsState(query: InfiniteQueryRef): QueryState> { - return collectAsState(query.key.id, query.state, query::resume) - } - - @Composable - private inline fun collectAsState( - key: UniqueId, - flow: StateFlow>, - crossinline resume: suspend () -> Unit - ): QueryState { - val state by flow.collectAsState() - LaunchedEffect(key) { - val currentValue = flow.value - if (currentValue.reply.isNone || currentValue.isInvalidated) { - resume() - } - } - return state - } -} - -@Stable -private object NetworkFirst : QueryCachingStrategy { - @Composable - override fun collectAsState(query: QueryRef): QueryState { - return collectAsState(query.key.id, query.state, query::resume) - } - - @Composable - override fun collectAsState(query: InfiniteQueryRef): QueryState> { - return collectAsState(query.key.id, query.state, query::resume) - } - - @Composable - private inline fun collectAsState( - key: UniqueId, - flow: StateFlow>, - crossinline resume: suspend () -> Unit - ): QueryState { - var resumed by rememberSaveable(key) { mutableStateOf(false) } - val initialValue = if (resumed) flow.value else QueryState.initial() - val state = produceState(initialValue, key) { - resume() - resumed = true - flow.collect { value = it } - } - return state.value - } -} diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryConfig.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryConfig.kt index 4d3ea39..0693688 100644 --- a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryConfig.kt +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryConfig.kt @@ -14,13 +14,12 @@ import soil.query.core.Marker */ @Immutable data class QueryConfig internal constructor( - val strategy: QueryCachingStrategy, + val strategy: QueryStrategy, val marker: Marker ) { - @Suppress("MemberVisibilityCanBePrivate") class Builder { - var strategy: QueryCachingStrategy = Default.strategy + var strategy: QueryStrategy = Default.strategy var marker: Marker = Default.marker fun build() = QueryConfig( @@ -31,7 +30,7 @@ data class QueryConfig internal constructor( companion object { val Default = QueryConfig( - strategy = QueryCachingStrategy.Default, + strategy = QueryStrategy.Default, marker = Marker.None ) } diff --git a/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryStrategy.kt b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryStrategy.kt new file mode 100644 index 0000000..8ee80cd --- /dev/null +++ b/soil-query-compose/src/commonMain/kotlin/soil/query/compose/QueryStrategy.kt @@ -0,0 +1,38 @@ +// Copyright 2024 Soil Contributors +// SPDX-License-Identifier: Apache-2.0 + +package soil.query.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import soil.query.QueryRef +import soil.query.QueryState + +/** + * A mechanism to finely adjust the behavior of the query on a component basis in Composable functions. + * + * If you want to customize, please create a class implementing [QueryStrategy]. + * For example, this is useful when you want to switch your implementation to `collectAsStateWithLifecycle`. + * + * @see CachingStrategy + */ +@Stable +interface QueryStrategy { + + @Composable + fun collectAsState(query: QueryRef): QueryState + + companion object Default : QueryStrategy { + @Composable + override fun collectAsState(query: QueryRef): QueryState { + val state by query.state.collectAsState() + LaunchedEffect(query.key.id) { + query.resume() + } + return state + } + } +} 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 bdd7088..dd3dd8d 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 @@ -44,7 +44,7 @@ class InfiniteQueryComposableTest : UnitTest() { setContent { SwrClientProvider(client) { val query = rememberInfiniteQuery(key, config = InfiniteQueryConfig { - strategy = QueryCachingStrategy + strategy = InfiniteQueryStrategy.Default marker = Marker.None }) when (val reply = query.reply) { 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 957cc7c..a6b5422 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 @@ -39,6 +39,7 @@ class MutationComposableTest : UnitTest() { setContent { SwrClientProvider(client) { val mutation = rememberMutation(key, config = MutationConfig { + strategy = MutationStrategy.Default marker = Marker.None }) when (val reply = mutation.reply) { 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 6ec57f8..1698f37 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 @@ -36,7 +36,7 @@ class QueryComposableTest : UnitTest() { setContent { SwrClientProvider(client) { val query = rememberQuery(key, config = QueryConfig { - strategy = QueryCachingStrategy.Default + strategy = QueryStrategy.Default marker = Marker.None }) when (val reply = query.reply) {