Skip to content

Commit 72cbefe

Browse files
fix: animated routing duplicate content recomposition after state restoration
1 parent 4c6d101 commit 72cbefe

File tree

10 files changed

+91
-84
lines changed

10 files changed

+91
-84
lines changed

integration/compose-animation/common/src/dev/programadorthi/routing/compose/animation/ComposeAnimations.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,9 @@ internal var ApplicationCall.popExitTransition: Animation<ExitTransition>?
6161
attributes.remove(ComposeRoutingPopExitTransitionAttributeKey)
6262
}
6363
}
64+
65+
public val AnimatedContentTransitionScope<ApplicationCall>.nextCall: ApplicationCall
66+
get() = targetState
67+
68+
public val AnimatedContentTransitionScope<ApplicationCall>.previousCall: ApplicationCall
69+
get() = initialState

integration/compose-animation/common/src/dev/programadorthi/routing/compose/animation/ComposeRouting.kt

Lines changed: 21 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,16 @@ package dev.programadorthi.routing.compose.animation
33
import androidx.compose.animation.AnimatedContent
44
import androidx.compose.animation.EnterTransition
55
import androidx.compose.animation.ExitTransition
6+
import androidx.compose.animation.fadeIn
7+
import androidx.compose.animation.fadeOut
68
import androidx.compose.animation.togetherWith
79
import androidx.compose.runtime.Composable
810
import androidx.compose.runtime.DisposableEffect
9-
import androidx.compose.runtime.SideEffect
10-
import androidx.compose.runtime.getValue
11-
import androidx.compose.runtime.mutableStateOf
1211
import androidx.compose.runtime.remember
13-
import androidx.compose.runtime.saveable.Saver
14-
import androidx.compose.runtime.saveable.rememberSaveable
15-
import androidx.compose.runtime.setValue
1612
import dev.programadorthi.routing.compose.CallContent
1713
import dev.programadorthi.routing.compose.Routing
1814
import dev.programadorthi.routing.compose.history.ComposeHistoryMode
15+
import dev.programadorthi.routing.compose.history.restored
1916
import dev.programadorthi.routing.compose.popped
2017
import dev.programadorthi.routing.core.Route
2118
import dev.programadorthi.routing.core.Routing
@@ -35,54 +32,30 @@ public fun Routing(
3532
popEnterTransition: Animation<EnterTransition> = enterTransition,
3633
popExitTransition: Animation<ExitTransition> = exitTransition,
3734
) {
38-
var restored by rememberSaveable(
39-
key = "state:restored:$routing",
40-
saver =
41-
Saver(
42-
restore = { mutableStateOf(true) },
43-
save = { true },
44-
),
45-
) {
46-
mutableStateOf(false)
47-
}
4835
Routing(
4936
historyMode = historyMode,
5037
routing = routing,
5138
startUri = startUri,
5239
) { call ->
53-
if (restored) {
54-
CallContent(call)
55-
} else {
56-
AnimatedContent(
57-
targetState = call,
58-
transitionSpec = {
59-
val previousCall = initialState
60-
val nextCall = targetState
61-
val enter =
62-
when {
63-
previousCall.popped -> nextCall.popEnterTransition ?: popEnterTransition
64-
else -> nextCall.enterTransition ?: enterTransition
65-
}
66-
val exit =
67-
when {
68-
previousCall.popped ->
69-
previousCall.popExitTransition
70-
?: popExitTransition
71-
72-
else -> previousCall.exitTransition ?: exitTransition
73-
}
74-
40+
AnimatedContent(
41+
targetState = call,
42+
transitionSpec = {
43+
if (nextCall.restored) {
44+
fadeIn() togetherWith fadeOut()
45+
} else if (previousCall.popped) {
46+
val popEnter = nextCall.popEnterTransition ?: popEnterTransition
47+
val popExit = previousCall.popExitTransition ?: popExitTransition
48+
popEnter(this) togetherWith popExit(this)
49+
} else {
50+
val enter = nextCall.enterTransition ?: enterTransition
51+
val exit = previousCall.exitTransition ?: exitTransition
7552
enter(this) togetherWith exit(this)
76-
},
77-
content = { animatedCall ->
78-
CallContent(animatedCall)
79-
},
80-
)
81-
}
82-
83-
SideEffect {
84-
restored = false
85-
}
53+
}
54+
},
55+
content = { animatedCall ->
56+
CallContent(animatedCall)
57+
},
58+
)
8659
}
8760
}
8861

integration/compose/build.gradle.kts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,8 @@ kotlin {
1515
dependencies {
1616
api(projects.resources)
1717
implementation(compose.runtime)
18-
implementation(libs.serialization.json)
19-
}
20-
}
21-
22-
jvmMain {
23-
dependencies {
2418
implementation(compose.runtimeSaveable)
19+
implementation(libs.serialization.json)
2520
}
2621
}
2722
}

integration/compose/common/src/dev/programadorthi/routing/compose/ComposeRoutingBuilder.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
44
import dev.programadorthi.routing.compose.history.platformPush
55
import dev.programadorthi.routing.compose.history.platformReplace
66
import dev.programadorthi.routing.compose.history.platformReplaceAll
7+
import dev.programadorthi.routing.compose.history.restored
78
import dev.programadorthi.routing.compose.history.shouldNeglect
89
import dev.programadorthi.routing.core.Route
910
import dev.programadorthi.routing.core.RouteMethod
@@ -72,6 +73,12 @@ public suspend fun <T> PipelineContext<Unit, ApplicationCall>.composable(
7273
call.resource = resource
7374
call.content = body
7475

76+
// Try clear restored flag on previous call
77+
val previousCall = routing.callStack.lastOrNull()
78+
if (previousCall?.restored == true) {
79+
previousCall.restored = false
80+
}
81+
7582
if (call.shouldNeglect()) {
7683
return
7784
}

integration/compose/common/src/dev/programadorthi/routing/compose/ComposeRoutingExt.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@ public fun Routing.pop(result: Any? = null) {
2222
poppedCall?.popped = true
2323
poppedCall?.popResult = result
2424

25-
val last = callStack.lastOrNull() ?: return@popOnPlatform
26-
if (last.content == null) {
27-
execute(last)
25+
val lastCall = callStack.lastOrNull() ?: return@popOnPlatform
26+
if (lastCall.content == null) {
27+
call(
28+
name = lastCall.name,
29+
uri = lastCall.uri,
30+
parameters = lastCall.parameters,
31+
routeMethod = RouteMethod.Replace,
32+
)
2833
}
2934
}
3035
}

integration/compose/common/src/dev/programadorthi/routing/compose/history/ComposeHistoryAttribute.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import dev.programadorthi.routing.core.application.Application
66
import dev.programadorthi.routing.core.application.ApplicationCall
77
import io.ktor.util.AttributeKey
88

9-
internal val ComposeHistoryModeAttributeKey: AttributeKey<ComposeHistoryMode> =
9+
private val ComposeHistoryModeAttributeKey: AttributeKey<ComposeHistoryMode> =
1010
AttributeKey("ComposeHistoryModeAttributeKey")
1111

1212
internal val ComposeHistoryNeglectAttributeKey: AttributeKey<Boolean> =
1313
AttributeKey("ComposeHistoryNeglectAttributeKey")
1414

15-
internal val ComposeHistoryRestoredCallAttributeKey: AttributeKey<Boolean> =
15+
private val ComposeHistoryRestoredCallAttributeKey: AttributeKey<Boolean> =
1616
AttributeKey("ComposeHistoryRestoredCallAttributeKey")
1717

1818
internal var Application.historyMode: ComposeHistoryMode
@@ -33,8 +33,8 @@ internal var ApplicationCall.neglect: Boolean
3333
attributes.put(ComposeHistoryNeglectAttributeKey, value)
3434
}
3535

36-
internal var ApplicationCall.restored: Boolean
36+
public var ApplicationCall.restored: Boolean
3737
get() = attributes.getOrNull(ComposeHistoryRestoredCallAttributeKey) ?: false
38-
set(value) {
38+
internal set(value) {
3939
attributes.put(ComposeHistoryRestoredCallAttributeKey, value)
4040
}

integration/compose/common/src/dev/programadorthi/routing/compose/history/ComposeHistoryState.kt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,13 @@ internal data class ComposeHistoryState(
1616
)
1717

1818
internal fun ComposeHistoryState.toCall(application: Application): ApplicationCall {
19-
val call =
20-
ApplicationCall(
21-
application = application,
22-
name = name,
23-
uri = uri,
24-
routeMethod = RouteMethod.parse(routeMethod),
25-
parameters = parametersOf(parameters),
26-
)
27-
call.restored = true
28-
return call
19+
return ApplicationCall(
20+
application = application,
21+
name = name,
22+
uri = uri,
23+
routeMethod = RouteMethod.parse(routeMethod),
24+
parameters = parametersOf(parameters),
25+
)
2926
}
3027

3128
internal fun ApplicationCall.toHistoryState(): ComposeHistoryState {

integration/compose/js/src/dev/programadorthi/routing/compose/history/ComposeHistoryExt.js.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import kotlin.coroutines.suspendCoroutine
1414

1515
private const val HASH_PREFIX = "/#"
1616

17-
internal actual fun ApplicationCall.shouldNeglect(): Boolean = neglect || restored
17+
internal actual fun ApplicationCall.shouldNeglect(): Boolean = neglect
1818

1919
internal actual suspend fun ApplicationCall.platformPush(routing: Routing) {
2020
window.history.pushState(

integration/compose/jvm/src/dev/programadorthi/routing/compose/history/ComposeHistoryExt.jvm.kt

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import androidx.compose.runtime.remember
66
import androidx.compose.runtime.saveable.Saver
77
import androidx.compose.runtime.saveable.rememberSaveable
88
import dev.programadorthi.routing.compose.callStack
9+
import dev.programadorthi.routing.compose.content
910
import dev.programadorthi.routing.compose.push
11+
import dev.programadorthi.routing.core.RouteMethod
1012
import dev.programadorthi.routing.core.Routing
1113
import dev.programadorthi.routing.core.application
1214
import dev.programadorthi.routing.core.application.ApplicationCall
15+
import dev.programadorthi.routing.core.call
1316
import kotlinx.serialization.encodeToString
1417
import kotlinx.serialization.json.Json
1518

16-
internal actual fun ApplicationCall.shouldNeglect(): Boolean = restored
19+
internal actual fun ApplicationCall.shouldNeglect(): Boolean = false
1720

1821
internal actual suspend fun ApplicationCall.platformPush(routing: Routing) {
1922
routing.callStack.add(this)
@@ -52,16 +55,37 @@ internal actual fun Routing.restoreState(startUri: String) {
5255
},
5356
),
5457
)
55-
LaunchedEffect(Unit) {
56-
val lastCall = history.lastOrNull()
57-
when {
58-
lastCall?.restored == true -> {
59-
callStack.clear()
60-
callStack.addAll(history)
61-
execute(lastCall)
62-
}
63-
64-
else -> push(path = startUri)
58+
LaunchedEffect(routingPath) {
59+
when (history.lastOrNull()) {
60+
null -> push(path = startUri)
61+
else -> restoreLastCall(history)
6562
}
6663
}
6764
}
65+
66+
private fun Routing.restoreLastCall(history: List<ApplicationCall>) {
67+
val lastCall = history.last()
68+
lastCall.restored = true
69+
val hasContent = lastCall.content != null
70+
callStack.clear()
71+
callStack.addAll(
72+
history.subList(
73+
fromIndex = 0,
74+
toIndex =
75+
if (hasContent) {
76+
history.size
77+
} else {
78+
history.lastIndex
79+
},
80+
),
81+
)
82+
if (!hasContent) {
83+
call(
84+
name = lastCall.name,
85+
uri = lastCall.uri,
86+
parameters = lastCall.parameters,
87+
attributes = lastCall.attributes,
88+
routeMethod = RouteMethod.Push,
89+
)
90+
}
91+
}

integration/compose/posix/src/dev/programadorthi/routing/compose/history/ComposeHistoryExt.posix.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import dev.programadorthi.routing.compose.push
77
import dev.programadorthi.routing.core.Routing
88
import dev.programadorthi.routing.core.application.ApplicationCall
99

10-
internal actual fun ApplicationCall.shouldNeglect(): Boolean = restored
10+
internal actual fun ApplicationCall.shouldNeglect(): Boolean = false
1111

1212
internal actual suspend fun ApplicationCall.platformPush(routing: Routing) {
1313
routing.callStack.add(this)

0 commit comments

Comments
 (0)