Skip to content

Commit

Permalink
delaying stack restoration to after application is ready
Browse files Browse the repository at this point in the history
  • Loading branch information
programadorthi committed Dec 18, 2023
1 parent fc6dfe1 commit 8977c5b
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal class StackInitializer :
//region StackManagerNotifier
override fun onRegistered(providerId: String, stackManager: StackManager) = synchronized(lock) {
val registry = currentActivity.get()?.savedStateRegistry ?: return
processRegistration(
processRegistry(
registry = registry,
providerId = providerId,
stackManager = stackManager,
Expand All @@ -73,38 +73,28 @@ internal class StackInitializer :
}
currentActivity = WeakReference(activity)
val registry = activity.savedStateRegistry
if (registry.isRestored) {
processRestoration(registry)
} else {
StackManager.subscriptions().forEach { (providerId, stackManager) ->
processRegistration(
registry = registry,
providerId = providerId,
stackManager = stackManager,
)
}
StackManager.subscriptions().forEach { (providerId, stackManager) ->
processRegistry(
registry = registry,
providerId = providerId,
stackManager = stackManager,
)
}
}

private fun processRegistration(
private fun processRegistry(
registry: SavedStateRegistry,
providerId: String,
stackManager: StackManager
stackManager: StackManager,
) {
registry.unregisterSavedStateProvider(providerId)
registry.registerSavedStateProvider(
key = providerId,
provider = StackSavedStateProvider(providerId, stackManager),
)
}

private fun processRestoration(registry: SavedStateRegistry) {
StackManager.subscriptions().forEach { (providerId, stackManager) ->
val provider = StackSavedStateProvider(providerId, stackManager)
if (registry.isRestored) {
val previousState = registry.consumeRestoredStateForKey(providerId)
if (previousState?.isEmpty == false) {
val saver = StackSavedStateProvider(providerId, stackManager)
saver.restoreState(previousState)
provider.restoreState(previousState)
}
}
registry.unregisterSavedStateProvider(providerId)
registry.registerSavedStateProvider(key = providerId, provider = provider)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package dev.programadorthi.routing.core
import dev.programadorthi.routing.core.application.Application
import dev.programadorthi.routing.core.application.ApplicationCall
import dev.programadorthi.routing.core.application.ApplicationPlugin
import dev.programadorthi.routing.core.application.ApplicationStarted
import dev.programadorthi.routing.core.application.ApplicationStopped
import dev.programadorthi.routing.core.application.createApplicationPlugin
import dev.programadorthi.routing.core.application.pluginOrNull
import io.ktor.util.AttributeKey
import io.ktor.util.KtorDsl

private val StackManagerAttributeKey: AttributeKey<StackManager> =
AttributeKey("StackManagerAttributeKey")
Expand All @@ -28,23 +31,43 @@ public val ApplicationCall.previous: StackApplicationCall?
/**
* A plugin that provides a stack manager on each call
*/
public val StackRouting: ApplicationPlugin<Unit> = createApplicationPlugin("StackRouting") {
val stackManager = StackManager(application)
public val StackRouting: ApplicationPlugin<StackRoutingConfig> = createApplicationPlugin(
name = "StackRouting",
createConfiguration = ::StackRoutingConfig,
) {
val stackManager = StackManager(application, pluginConfig)

onCall { call ->
call.stackManager = stackManager
}
}

internal class StackManager(val application: Application) {
@KtorDsl
public class StackRoutingConfig {
/**
* Used for Android or other that have process death restoration
*/
public var emitAfterRestoration: Boolean = true
}

internal class StackManager(
val application: Application,
private val config: StackRoutingConfig,
) {
private val stack = mutableListOf<StackApplicationCall>()

init {
val environment = application.environment
val providerId = "${environment.parentRouting}#${environment.rootPath}"
register(providerId = providerId, stackManager = this)
application.environment.monitor.subscribe(ApplicationStopped) {
unregister(providerId)
environment.monitor.apply {

subscribe(ApplicationStarted) {
register(providerId = providerId, stackManager = this@StackManager)
}

subscribe(ApplicationStopped) {
unregister(providerId)
}
}
}

Expand Down Expand Up @@ -101,6 +124,7 @@ internal class StackManager(val application: Application) {
fun toRestore(previous: List<StackApplicationCall>) {
stack.clear()
stack.addAll(previous)
tryEmitLastItem()
}

private fun ApplicationCall.toReplace(): StackApplicationCall = when {
Expand All @@ -119,6 +143,16 @@ internal class StackManager(val application: Application) {
)
}

// On Android after restoration we need to emit again the last item to notify
// We need neglect to avoid put again on the stack
private fun tryEmitLastItem() {
val item = stack.removeLastOrNull()

if (!config.emitAfterRestoration || item == null) return

application.pluginOrNull(Routing)?.execute(item)
}

internal companion object {
private val subscriptions = mutableMapOf<String, StackManager>()
internal var stackManagerNotifier: StackManagerNotifier? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
package dev.programadorthi.routing.core

import io.ktor.http.Parameters
import kotlinx.coroutines.launch

public fun Routing.pop(
parameters: Parameters = Parameters.Empty,
neglect: Boolean = false,
) {
val lastCall = application.stackManager.lastOrNull() ?: return
application.launch {
execute(
StackApplicationCall.Pop(
application = application,
name = lastCall.name,
uri = lastCall.uri,
parameters = parameters,
).tryNeglect(neglect)
)
}
execute(
StackApplicationCall.Pop(
application = application,
name = lastCall.name,
uri = lastCall.uri,
parameters = parameters,
).tryNeglect(neglect)
)
}

public fun Routing.push(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package dev.programadorthi.routing.core

internal class FakeStackManagerNotifier : StackManagerNotifier {
val callsToRestore = mutableListOf<StackApplicationCall>()
val subscriptions = mutableMapOf<String, StackManager>()

override fun onRegistered(providerId: String, stackManager: StackManager) {
subscriptions += providerId to stackManager
// Simulating Android restoration after register
stackManager.toRestore(callsToRestore)
}

override fun onUnRegistered(providerId: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
Expand All @@ -20,6 +21,11 @@ import kotlin.test.assertNull
@OptIn(ExperimentalCoroutinesApi::class)
class StackRoutingTest {

@BeforeTest
fun beforeEachTest() {
StackManager.stackManagerNotifier = null
}

@Test
fun shouldFailWhenMissingStackPlugin() = runTest {
// WHEN
Expand Down Expand Up @@ -780,4 +786,47 @@ class StackRoutingTest {
assertEquals("path", result[6].name)
assertEquals(parametersOf("key", "value"), result[6].parameters)
}

@Test
fun shouldEmitLastCallAfterRestoration() = runTest {
// GIVEN
val job = Job()
val stackManagerNotifier = FakeStackManagerNotifier()
var result: ApplicationCall? = null

StackManager.stackManagerNotifier = stackManagerNotifier

routing(parentCoroutineContext = coroutineContext + job) {
install(StackRouting)

push(path = "/path01", name = "path01") {
result = call
}

push(path = "/path02", name = "path02") {
result = call
job.complete()
}

// WHEN (Android restored calls)
stackManagerNotifier.callsToRestore += StackApplicationCall.Push(
application = application,
uri = "/path01",
)

stackManagerNotifier.callsToRestore += StackApplicationCall.Push(
application = application,
uri = "/path02",
parameters = parametersOf("key" to listOf("value")),
)
}
advanceTimeBy(99)

// THEN
assertNotNull(result)
assertEquals("/path02", "${result?.uri}")
assertEquals("", "${result?.name}")
assertEquals(StackRouteMethod.Push, result?.routeMethod)
assertEquals(parametersOf("key" to listOf("value")), result?.parameters)
}
}

0 comments on commit 8977c5b

Please sign in to comment.