Skip to content

Commit

Permalink
Merge pull request #84 from soil-kt/strategy-layer
Browse files Browse the repository at this point in the history
Enabling behavior customization through the strategy layer.
  • Loading branch information
ogaclejapan authored Aug 31, 2024
2 parents bae226c + f6dbe43 commit f0cd671
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 168 deletions.
Original file line number Diff line number Diff line change
@@ -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 <T> collectAsState(query: QueryRef<T>): QueryState<T> {
return collectAsState(query.key.id, query.state, query::resume)
}

@Composable
override fun <T, S> collectAsState(query: InfiniteQueryRef<T, S>): QueryState<QueryChunks<T, S>> {
return collectAsState(query.key.id, query.state, query::resume)
}

@Composable
private inline fun <T> collectAsState(
key: UniqueId,
flow: StateFlow<QueryState<T>>,
crossinline resume: suspend () -> Unit
): QueryState<T> {
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 <T> collectAsState(query: QueryRef<T>): QueryState<T> {
return collectAsState(query.key.id, query.state, query::resume)
}

@Composable
override fun <T, S> collectAsState(query: InfiniteQueryRef<T, S>): QueryState<QueryChunks<T, S>> {
return collectAsState(query.key.id, query.state, query::resume)
}

@Composable
private inline fun <T> collectAsState(
key: UniqueId,
flow: StateFlow<QueryState<T>>,
crossinline resume: suspend () -> Unit
): QueryState<T> {
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -31,7 +30,7 @@ data class InfiniteQueryConfig internal constructor(

companion object {
val Default = InfiniteQueryConfig(
strategy = QueryCachingStrategy.Default,
strategy = InfiniteQueryStrategy.Default,
marker = Marker.None
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T, S> collectAsState(query: InfiniteQueryRef<T, S>): QueryState<QueryChunks<T, S>>

companion object Default : InfiniteQueryStrategy {

@Composable
override fun <T, S> collectAsState(query: InfiniteQueryRef<T, S>): QueryState<QueryChunks<T, S>> {
val state by query.state.collectAsState()
LaunchedEffect(query.key.id) {
query.resume()
}
return state
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,8 +30,7 @@ fun <T, S> rememberMutation(
): MutationObject<T, S> {
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 <T, S> MutationState<T>.toObject(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T, S> collectAsState(mutation: MutationRef<T, S>): MutationState<T>

companion object Default : MutationStrategy {
@Composable
override fun <T, S> collectAsState(mutation: MutationRef<T, S>): MutationState<T> {
return mutation.state.collectAsState().value
}
}
}
Loading

0 comments on commit f0cd671

Please sign in to comment.