Skip to content

Commit

Permalink
Optimization of Recomposition
Browse files Browse the repository at this point in the history
Within the Composable functions provided by the Query library, recomposition occurs due to changes in the internal state
related to data loading. To reduce unnecessary state updates in the UI, we have implemented a RecompositionOptimizer
that omits certain state changes based on conditions.

This RecompositionOptimizer can be configured on a per-Composable function basis, and the optimization is enabled by
default. If you need to reference the omitted state items in the UI, you can disable the optimization through the
configuration settings.
  • Loading branch information
ogaclejapan committed Sep 21, 2024
1 parent a372f6e commit 7b07346
Show file tree
Hide file tree
Showing 33 changed files with 2,153 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import soil.query.InfiniteQueryKey
import soil.query.QueryChunks
import soil.query.QueryClient
import soil.query.compose.internal.newInfiniteQuery

/**
* Remember a [InfiniteQueryObject] and subscribes to the query state of [key].
Expand All @@ -27,7 +28,7 @@ fun <T, S> rememberInfiniteQuery(
client: QueryClient = LocalQueryClient.current
): InfiniteQueryObject<QueryChunks<T, S>, S> {
val scope = rememberCoroutineScope()
val query = remember(key.id) { client.getInfiniteQuery(key, config.marker).also { it.launchIn(scope) } }
val query = remember(key.id) { newInfiniteQuery(key, config, client, scope) }
return with(config.mapper) {
config.strategy.collectAsState(query).toObject(query = query, select = { it })
}
Expand All @@ -52,7 +53,7 @@ fun <T, S, U> rememberInfiniteQuery(
client: QueryClient = LocalQueryClient.current
): InfiniteQueryObject<U, S> {
val scope = rememberCoroutineScope()
val query = remember(key.id) { client.getInfiniteQuery(key, config.marker).also { it.launchIn(scope) } }
val query = remember(key.id) { newInfiniteQuery(key, config, client, scope) }
return with(config.mapper) {
config.strategy.collectAsState(query).toObject(query = query, select = select)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,43 @@ import soil.query.core.Marker
/**
* Configuration for the infinite query.
*
* @property mapper The mapper for converting query data.
* @property optimizer The optimizer for recomposing the query data.
* @property strategy The strategy for caching query data.
* @property marker The marker with additional information based on the caller of a query.
*/
@Immutable
data class InfiniteQueryConfig internal constructor(
val strategy: InfiniteQueryStrategy,
val mapper: InfiniteQueryObjectMapper,
val optimizer: InfiniteQueryRecompositionOptimizer,
val strategy: InfiniteQueryStrategy,
val marker: Marker
) {

class Builder {
var strategy: InfiniteQueryStrategy = Default.strategy
var mapper: InfiniteQueryObjectMapper = Default.mapper
var marker: Marker = Default.marker
/**
* Creates a new [InfiniteQueryConfig] with the provided [block].
*/
fun builder(block: Builder.() -> Unit) = Builder(this).apply(block).build()

class Builder(config: InfiniteQueryConfig = Default) {
var mapper: InfiniteQueryObjectMapper = config.mapper
var optimizer: InfiniteQueryRecompositionOptimizer = config.optimizer
var strategy: InfiniteQueryStrategy = config.strategy
var marker: Marker = config.marker

fun build() = InfiniteQueryConfig(
strategy = strategy,
optimizer = optimizer,
mapper = mapper,
marker = marker
)
}

companion object {
val Default = InfiniteQueryConfig(
strategy = InfiniteQueryStrategy.Default,
mapper = InfiniteQueryObjectMapper.Default,
optimizer = InfiniteQueryRecompositionOptimizer.Default,
strategy = InfiniteQueryStrategy.Default,
marker = Marker.None
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2024 Soil Contributors
// SPDX-License-Identifier: Apache-2.0

package soil.query.compose

import soil.query.QueryChunks
import soil.query.QueryState
import soil.query.QueryStatus

/**
* A recomposition optimizer for [QueryState] with [QueryChunks].
*/
interface InfiniteQueryRecompositionOptimizer {

/**
* Omit the specified keys from the [QueryState] with [QueryChunks].
*
* @param state The infinite query state.
* @return The optimized infinite query state.
*/
fun <T, S> omit(state: QueryState<QueryChunks<T, S>>): QueryState<QueryChunks<T, S>>

companion object
}

/**
* Optimizer implementation for [InfiniteQueryStrategy.Companion.Default].
*/
val InfiniteQueryRecompositionOptimizer.Companion.Default: InfiniteQueryRecompositionOptimizer
get() = DefaultInfiniteQueryRecompositionOptimizer

private object DefaultInfiniteQueryRecompositionOptimizer : InfiniteQueryRecompositionOptimizer {
override fun <T, S> omit(state: QueryState<QueryChunks<T, S>>): QueryState<QueryChunks<T, S>> {
val keys = buildSet {
add(QueryState.OmitKey.replyUpdatedAt)
add(QueryState.OmitKey.staleAt)
when (state.status) {
QueryStatus.Pending -> {
add(QueryState.OmitKey.errorUpdatedAt)
add(QueryState.OmitKey.fetchStatus)
}

QueryStatus.Success -> {
add(QueryState.OmitKey.errorUpdatedAt)
if (!state.isInvalidated) {
add(QueryState.OmitKey.fetchStatus)
}
}

QueryStatus.Failure -> {
if (!state.isInvalidated) {
add(QueryState.OmitKey.fetchStatus)
}
}
}
}
return state.omit(keys)
}
}

/**
* Option that performs no optimization.
*/
val InfiniteQueryRecompositionOptimizer.Companion.Disabled: InfiniteQueryRecompositionOptimizer
get() = DisabledInfiniteQueryRecompositionOptimizer

private object DisabledInfiniteQueryRecompositionOptimizer : InfiniteQueryRecompositionOptimizer {
override fun <T, S> omit(state: QueryState<QueryChunks<T, S>>): QueryState<QueryChunks<T, S>> {
return state
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import soil.query.MutationClient
import soil.query.MutationKey
import soil.query.compose.internal.newMutation

/**
* Remember a [MutationObject] and subscribes to the mutation state of [key].
Expand All @@ -26,7 +27,7 @@ fun <T, S> rememberMutation(
client: MutationClient = LocalMutationClient.current
): MutationObject<T, S> {
val scope = rememberCoroutineScope()
val mutation = remember(key.id) { client.getMutation(key, config.marker).also { it.launchIn(scope) } }
val mutation = remember(key.id) { newMutation(key, config, client, scope) }
return with(config.mapper) {
config.strategy.collectAsState(mutation).toObject(mutation = mutation)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,43 @@ import soil.query.core.Marker
/**
* Configuration for the mutation.
*
* @property mapper The mapper for converting mutation data.
* @property optimizer The optimizer for recomposing the mutation data.
* @property strategy The strategy for caching mutation data.
* @property marker The marker with additional information based on the caller of a mutation.
*/
@Immutable
data class MutationConfig internal constructor(
val strategy: MutationStrategy,
val mapper: MutationObjectMapper,
val optimizer: MutationRecompositionOptimizer,
val strategy: MutationStrategy,
val marker: Marker
) {

class Builder {
var strategy: MutationStrategy = Default.strategy
var mapper: MutationObjectMapper = Default.mapper
var marker: Marker = Default.marker
/**
* Creates a new [MutationConfig] with the provided [block].
*/
fun builder(block: Builder.() -> Unit) = Builder(this).apply(block).build()

class Builder(config: MutationConfig = Default) {
var mapper: MutationObjectMapper = config.mapper
var optimizer: MutationRecompositionOptimizer = config.optimizer
var strategy: MutationStrategy = config.strategy
var marker: Marker = config.marker

fun build() = MutationConfig(
strategy = strategy,
mapper = mapper,
optimizer = optimizer,
strategy = strategy,
marker = marker
)
}

companion object {
val Default = MutationConfig(
strategy = MutationStrategy.Default,
mapper = MutationObjectMapper.Default,
optimizer = MutationRecompositionOptimizer.Default,
strategy = MutationStrategy.Default,
marker = Marker.None
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2024 Soil Contributors
// SPDX-License-Identifier: Apache-2.0

package soil.query.compose

import soil.query.MutationState
import soil.query.MutationStatus

/**
* A recomposition optimizer for [MutationState].
*/
interface MutationRecompositionOptimizer {

/**
* Omit the specified keys from the [MutationState].
*
* @param state The mutation state.
* @return The optimized mutation state.
*/
fun <T> omit(state: MutationState<T>): MutationState<T>

companion object
}

/**
* Optimizer implementation for [MutationStrategy.Companion.Default].
*/
val MutationRecompositionOptimizer.Companion.Default: MutationRecompositionOptimizer
get() = DefaultMutationRecompositionOptimizer

private object DefaultMutationRecompositionOptimizer : MutationRecompositionOptimizer {
override fun <T> omit(state: MutationState<T>): MutationState<T> {
val keys = buildSet {
add(MutationState.OmitKey.replyUpdatedAt)
add(MutationState.OmitKey.mutatedCount)
when (state.status) {
MutationStatus.Idle -> {
add(MutationState.OmitKey.errorUpdatedAt)
}

MutationStatus.Pending -> {
if (state.error == null) {
add(MutationState.OmitKey.errorUpdatedAt)
}
}

MutationStatus.Success -> {
add(MutationState.OmitKey.errorUpdatedAt)
}

MutationStatus.Failure -> Unit
}
}
return state.omit(keys)
}
}

/**
* Option that performs no optimization.
*/
val MutationRecompositionOptimizer.Companion.Disabled: MutationRecompositionOptimizer
get() = DisabledMutationRecompositionOptimizer

private object DisabledMutationRecompositionOptimizer : MutationRecompositionOptimizer {
override fun <T> omit(state: MutationState<T>): MutationState<T> {
return state
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
package soil.query.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import soil.query.QueryClient
import soil.query.QueryKey
import soil.query.compose.internal.combineQuery
import soil.query.compose.internal.newCombinedQuery
import soil.query.compose.internal.newQuery

/**
* Remember a [QueryObject] and subscribes to the query state of [key].
Expand All @@ -27,7 +27,7 @@ fun <T> rememberQuery(
client: QueryClient = LocalQueryClient.current
): QueryObject<T> {
val scope = rememberCoroutineScope()
val query = remember(key) { client.getQuery(key, config.marker).also { it.launchIn(scope) } }
val query = remember(key.id) { newQuery(key, config, client, scope) }
return with(config.mapper) {
config.strategy.collectAsState(query).toObject(query = query, select = { it })
}
Expand All @@ -52,7 +52,7 @@ fun <T, U> rememberQuery(
client: QueryClient = LocalQueryClient.current
): QueryObject<U> {
val scope = rememberCoroutineScope()
val query = remember(key) { client.getQuery(key, config.marker).also { it.launchIn(scope) } }
val query = remember(key.id) { newQuery(key, config, client, scope) }
return with(config.mapper) {
config.strategy.collectAsState(query).toObject(query = query, select = select)
}
Expand Down Expand Up @@ -80,17 +80,11 @@ fun <T1, T2, R> rememberQuery(
client: QueryClient = LocalQueryClient.current,
): QueryObject<R> {
val scope = rememberCoroutineScope()
val query1 = remember(key1.id) { client.getQuery(key1, config.marker).also { it.launchIn(scope) } }
val query2 = remember(key2.id) { client.getQuery(key2, config.marker).also { it.launchIn(scope) } }
val combinedQuery = remember(query1, query2) {
combineQuery(query1, query2, transform)
}
DisposableEffect(combinedQuery.id) {
val job = combinedQuery.launchIn(scope)
onDispose { job.cancel() }
val query = remember(key1.id, key2.id) {
newCombinedQuery(key1, key2, transform, config, client, scope)
}
return with(config.mapper) {
config.strategy.collectAsState(combinedQuery).toObject(query = combinedQuery, select = { it })
config.strategy.collectAsState(query).toObject(query = query, select = { it })
}
}

Expand Down Expand Up @@ -119,18 +113,11 @@ fun <T1, T2, T3, R> rememberQuery(
client: QueryClient = LocalQueryClient.current,
): QueryObject<R> {
val scope = rememberCoroutineScope()
val query1 = remember(key1.id) { client.getQuery(key1, config.marker).also { it.launchIn(scope) } }
val query2 = remember(key2.id) { client.getQuery(key2, config.marker).also { it.launchIn(scope) } }
val query3 = remember(key3.id) { client.getQuery(key3, config.marker).also { it.launchIn(scope) } }
val combinedQuery = remember(query1, query2, query3) {
combineQuery(query1, query2, query3, transform)
}
DisposableEffect(combinedQuery.id) {
val job = combinedQuery.launchIn(scope)
onDispose { job.cancel() }
val query = remember(key1.id, key2.id, key3.id) {
newCombinedQuery(key1, key2, key3, transform, config, client, scope)
}
return with(config.mapper) {
config.strategy.collectAsState(combinedQuery).toObject(query = combinedQuery, select = { it })
config.strategy.collectAsState(query).toObject(query = query, select = { it })
}
}

Expand All @@ -153,17 +140,10 @@ fun <T, R> rememberQuery(
client: QueryClient = LocalQueryClient.current
): QueryObject<R> {
val scope = rememberCoroutineScope()
val queries = remember(keys) {
keys.map { key -> client.getQuery(key, config.marker).also { it.launchIn(scope) } }
}
val combinedQuery = remember(queries) {
combineQuery(queries.toTypedArray(), transform)
}
DisposableEffect(combinedQuery.id) {
val job = combinedQuery.launchIn(scope)
onDispose { job.cancel() }
val query = remember(*keys.map { it.id }.toTypedArray()) {
newCombinedQuery(keys, transform, config, client, scope)
}
return with(config.mapper) {
config.strategy.collectAsState(combinedQuery).toObject(query = combinedQuery, select = { it })
config.strategy.collectAsState(query).toObject(query = query, select = { it })
}
}
Loading

0 comments on commit 7b07346

Please sign in to comment.