Skip to content

Commit dff20d4

Browse files
committed
Bump landscapist and improve the palette tracking logic
1 parent 516c51e commit dff20d4

File tree

3 files changed

+171
-13
lines changed

3 files changed

+171
-13
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Designed and developed by 2024 skydoves (Jaewoong Eum)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
@file:OptIn(ExperimentalCoroutinesApi::class)
17+
18+
package com.skydoves.pokedex.compose.feature.details
19+
20+
import android.util.Log
21+
import kotlinx.coroutines.CoroutineScope
22+
import kotlinx.coroutines.ExperimentalCoroutinesApi
23+
import kotlinx.coroutines.delay
24+
import kotlinx.coroutines.flow.Flow
25+
import kotlinx.coroutines.flow.MutableStateFlow
26+
import kotlinx.coroutines.flow.SharingCommand
27+
import kotlinx.coroutines.flow.SharingStarted
28+
import kotlinx.coroutines.flow.StateFlow
29+
import kotlinx.coroutines.flow.combine
30+
import kotlinx.coroutines.flow.distinctUntilChanged
31+
import kotlinx.coroutines.flow.dropWhile
32+
import kotlinx.coroutines.flow.stateIn
33+
import kotlinx.coroutines.flow.transformLatest
34+
35+
/**
36+
* Create an upstream cold flow as a StateFlow that triggers the upstream operation only once,
37+
* preventing re-execution no matter how many times it's subscribed.
38+
* After the initial emission, it will simply replay the latest cached value.
39+
*
40+
* @param scope the coroutine scope in which sharing is started.
41+
* @param stopTimeout configures a delay (in milliseconds) between the disappearance of the last
42+
* subscriber and the stopping of the sharing coroutine. It defaults to zero (stop immediately).
43+
* @param replayExpiration configures a delay (in milliseconds) between the stopping of
44+
* the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the
45+
* [shareIn] operator and resets the cached value to the original `initialValue`
46+
* for the [stateIn] operator). It defaults to `Long.MAX_VALUE` (keep replay cache forever,
47+
* never reset buffer). Use zero value to expire the cache immediately.
48+
* @param initialValue the initial value of the state flow. This value is also used when the
49+
* state flow is reset using the [SharingStarted]. [WhileSubscribed] strategy with the
50+
* [replayExpiration] parameter.
51+
*/
52+
public fun <T> Flow<T>.onetimeStateIn(
53+
scope: CoroutineScope,
54+
stopTimeout: Long = 0,
55+
replayExpiration: Long = Long.MAX_VALUE,
56+
initialValue: T,
57+
): StateFlow<T> {
58+
return stateIn(
59+
scope = scope,
60+
started = OnetimeWhileSubscribed(
61+
stopTimeout = stopTimeout,
62+
replayExpiration = replayExpiration,
63+
),
64+
initialValue,
65+
)
66+
}
67+
68+
/**
69+
* This is a companion extension of [SharingStarted], which is a [SharingStarted] strategy
70+
* designed to limit upstream emissions to only once. After the initial emission,
71+
* it remains inactive until an active subscriber reappears.
72+
*
73+
* @param stopTimeout configures a delay (in milliseconds) between the disappearance of the last
74+
* subscriber and the stopping of the sharing coroutine. It defaults to zero (stop immediately).
75+
* @param replayExpiration configures a delay (in milliseconds) between the stopping of
76+
* the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the
77+
* [shareIn] operator and resets the cached value to the original `initialValue`
78+
* for the [stateIn] operator). It defaults to `Long.MAX_VALUE` (keep replay cache forever,
79+
* never reset buffer). Use zero value to expire the cache immediately.
80+
*/
81+
public fun SharingStarted.Companion.OnetimeWhileSubscribed(
82+
stopTimeout: Long,
83+
replayExpiration: Long = Long.MAX_VALUE,
84+
): OnetimeWhileSubscribed {
85+
return OnetimeWhileSubscribed(
86+
stopTimeout = stopTimeout,
87+
replayExpiration = replayExpiration,
88+
)
89+
}
90+
91+
/**
92+
* `OnetimeWhileSubscribed` is a [SharingStarted] strategy designed to limit upstream emissions to
93+
* only once. After the initial emission, it remains inactive until an active subscriber reappears.
94+
*
95+
* @param stopTimeout configures a delay (in milliseconds) between the disappearance of the last
96+
* subscriber and the stopping of the sharing coroutine. It defaults to zero (stop immediately).
97+
* @param replayExpiration configures a delay (in milliseconds) between the stopping of
98+
* the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the
99+
* [shareIn] operator and resets the cached value to the original `initialValue`
100+
* for the [stateIn] operator). It defaults to `Long.MAX_VALUE` (keep replay cache forever,
101+
* never reset buffer). Use zero value to expire the cache immediately.
102+
*/
103+
public class OnetimeWhileSubscribed(
104+
private val stopTimeout: Long,
105+
private val replayExpiration: Long,
106+
) : SharingStarted {
107+
108+
private val hasCollected: MutableStateFlow<Boolean> = MutableStateFlow(false)
109+
110+
init {
111+
require(stopTimeout >= 0) { "stopTimeout($stopTimeout ms) cannot be negative" }
112+
require(replayExpiration >= 0) { "replayExpiration($replayExpiration ms) cannot be negative" }
113+
}
114+
115+
override fun command(subscriptionCount: StateFlow<Int>): Flow<SharingCommand> =
116+
combine(hasCollected, subscriptionCount) { collected, counts ->
117+
collected to counts
118+
}
119+
.transformLatest { pair ->
120+
val (collected, count) = pair
121+
if (count > 0 && !collected) {
122+
emit(SharingCommand.START)
123+
hasCollected.value = true
124+
} else {
125+
delay(stopTimeout)
126+
if (replayExpiration > 0) {
127+
emit(SharingCommand.STOP)
128+
delay(replayExpiration)
129+
}
130+
}
131+
}
132+
.dropWhile {
133+
it != SharingCommand.START
134+
} // don't emit any STOP/RESET_BUFFER to start with, only START
135+
.distinctUntilChanged() // just in case somebody forgets it, don't leak our multiple sending of START
136+
137+
override fun toString(): String {
138+
val params = buildList(2) {
139+
if (stopTimeout > 0) add("stopTimeout=${stopTimeout}ms")
140+
if (replayExpiration < Long.MAX_VALUE) add("replayExpiration=${replayExpiration}ms")
141+
}
142+
return "SharingStarted.WhileSubscribed(${params.joinToString()})"
143+
}
144+
145+
// equals & hashcode to facilitate testing, not documented in public contract
146+
override fun equals(other: Any?): Boolean =
147+
other is OnetimeWhileSubscribed &&
148+
stopTimeout == other.stopTimeout &&
149+
replayExpiration == other.replayExpiration
150+
151+
override fun hashCode(): Int = stopTimeout.hashCode() * 31 + replayExpiration.hashCode()
152+
}

feature/home/src/main/kotlin/com/skydoves/pokedex/compose/feature/home/PokedexHome.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import com.skydoves.landscapist.animation.crossfade.CrossfadePlugin
6161
import com.skydoves.landscapist.components.rememberImageComponent
6262
import com.skydoves.landscapist.glide.GlideImage
6363
import com.skydoves.landscapist.palette.PalettePlugin
64+
import com.skydoves.landscapist.palette.rememberPaletteState
6465
import com.skydoves.landscapist.placeholder.shimmer.Shimmer
6566
import com.skydoves.landscapist.placeholder.shimmer.ShimmerPlugin
6667
import com.skydoves.pokedex.compose.core.data.repository.home.FakeHomeRepository
@@ -117,9 +118,14 @@ private fun SharedTransitionScope.HomeContent(
117118
fetchNextPokemonList()
118119
}
119120

121+
var palette by rememberPaletteState()
122+
val backgroundColor by palette.paletteBackgroundColor()
123+
120124
PokemonCard(
121125
animatedVisibilityScope = animatedVisibilityScope,
122126
pokemon = pokemon,
127+
onPaletteLoaded = { palette = it },
128+
backgroundColor = backgroundColor,
123129
)
124130
}
125131
}
@@ -133,11 +139,11 @@ private fun SharedTransitionScope.HomeContent(
133139
@Composable
134140
private fun SharedTransitionScope.PokemonCard(
135141
animatedVisibilityScope: AnimatedVisibilityScope,
142+
onPaletteLoaded: (Palette) -> Unit,
143+
backgroundColor: Color,
136144
pokemon: Pokemon,
137145
) {
138146
val composeNavigator = currentComposeNavigator
139-
var palette by remember { mutableStateOf<Palette?>(null) }
140-
val backgroundColor by palette.paletteBackgroundColor()
141147

142148
Card(
143149
modifier = Modifier
@@ -182,7 +188,7 @@ private fun SharedTransitionScope.PokemonCard(
182188
+PalettePlugin(
183189
imageModel = pokemon.imageUrl,
184190
useCache = true,
185-
paletteLoadedListener = { palette = it },
191+
paletteLoadedListener = { onPaletteLoaded.invoke(it) },
186192
)
187193
}
188194
},

gradle/libs.versions.toml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
[versions]
2-
agp = "8.7.1"
3-
kotlin = "2.0.21"
4-
ksp = "2.0.21-1.0.25"
2+
agp = "8.7.3"
3+
kotlin = "2.1.0"
4+
ksp = "2.0.21-1.0.28"
55
kotlinxImmutable = "0.3.7"
66
androidxActivity = "1.9.3"
7-
androidxCore = "1.13.1"
8-
androidxLifecycle = "2.8.6"
7+
androidxCore = "1.15.0"
8+
androidxLifecycle = "2.8.7"
99
androidxRoom = "2.6.1"
1010
androidxArchCore = "2.2.0"
1111
androidXStartup = "1.2.0"
12-
androidxCompose = "1.7.4"
13-
androidxComposeMaterial3 = "1.3.0"
14-
androidxNavigation = "2.8.3"
12+
androidxCompose = "1.7.6"
13+
androidxComposeMaterial3 = "1.3.1"
14+
androidxNavigation = "2.8.5"
1515
androidxHiltNavigationCompose = "1.2.0"
1616
composeStableMarker = "1.0.5"
1717
kotlinxSerializationJson = "1.7.3"
18-
hilt = "2.52"
18+
hilt = "2.54"
1919
retrofit = "2.11.0"
2020
okHttp = "4.12.0"
2121
sandwich = "2.0.10"
22-
landscapist = "2.4.1"
22+
landscapist = "2.4.6"
2323
coroutines = "1.9.0"
2424
profileInstaller = "1.4.1"
2525
macroBenchmark = "1.3.3"

0 commit comments

Comments
 (0)