Skip to content

Commit

Permalink
feat: compose state restoration and web history mode
Browse files Browse the repository at this point in the history
programadorthi committed Jun 3, 2024
1 parent 3adac4a commit af8c513
Showing 22 changed files with 899 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -7,12 +7,8 @@ import io.ktor.util.AttributeKey
private val ComposeRoutingAnimationScopeAttributeKey: AttributeKey<AnimatedVisibilityScope> =
AttributeKey("ComposeRoutingAnimationScopeAttributeKey")

public var ApplicationCall.animatedVisibilityScope: AnimatedVisibilityScope?
get() = attributes.getOrNull(ComposeRoutingAnimationScopeAttributeKey)
internal var ApplicationCall.animatedVisibilityScope: AnimatedVisibilityScope
get() = attributes[ComposeRoutingAnimationScopeAttributeKey]
internal set(value) {
if (value != null) {
attributes.put(ComposeRoutingAnimationScopeAttributeKey, value)
} else {
attributes.remove(ComposeRoutingAnimationScopeAttributeKey)
}
attributes.put(ComposeRoutingAnimationScopeAttributeKey, value)
}
Original file line number Diff line number Diff line change
@@ -12,8 +12,8 @@ import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import dev.programadorthi.routing.compose.CurrentContent
import dev.programadorthi.routing.compose.Routing
import dev.programadorthi.routing.compose.content
import dev.programadorthi.routing.compose.popped
import dev.programadorthi.routing.core.Route
import dev.programadorthi.routing.core.Routing
@@ -39,14 +39,12 @@ public fun Routing(
popEnterTransition: Animation<EnterTransition> = enterTransition,
popExitTransition: Animation<ExitTransition> = exitTransition,
initial: ComposeAnimatedContent,
content: ComposeAnimatedContent = { call ->
call.content(call)
},
content: ComposeAnimatedContent = { CurrentContent() },
) {
Routing(
routing = routing,
initial = { call ->
call.animatedVisibilityScope?.initial(call)
call.animatedVisibilityScope.initial(call)
},
) { call ->
AnimatedContent(
@@ -94,7 +92,7 @@ public fun Routing(
popExitTransition: Animation<ExitTransition> = exitTransition,
configuration: Route.() -> Unit,
initial: ComposeAnimatedContent,
content: ComposeAnimatedContent = { call -> call.content(call) },
content: ComposeAnimatedContent = { CurrentContent() },
) {
val routing =
remember {
Original file line number Diff line number Diff line change
@@ -123,7 +123,7 @@ public inline fun <reified T : Any> Route.composable(
}
}

public fun <T> PipelineContext<Unit, ApplicationCall>.composable(
public suspend fun <T> PipelineContext<Unit, ApplicationCall>.composable(
routing: Routing,
resource: T? = null,
enterTransition: Animation<EnterTransition>? = null,
7 changes: 7 additions & 0 deletions integration/compose/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -15,6 +15,13 @@ kotlin {
dependencies {
api(projects.resources)
implementation(compose.runtime)
implementation(libs.serialization.json)
}
}

jvmMain {
dependencies {
implementation(compose.runtimeSaveable)
}
}
}
Original file line number Diff line number Diff line change
@@ -9,8 +9,12 @@ public typealias ComposeContent = @Composable (ApplicationCall) -> Unit
private val ComposeRoutingContentAttributeKey: AttributeKey<ComposeContent> =
AttributeKey("ComposeRoutingContentAttributeKey")

public var ApplicationCall.content: ComposeContent
get() = attributes[ComposeRoutingContentAttributeKey]
public var ApplicationCall.content: ComposeContent?
get() = attributes.getOrNull(ComposeRoutingContentAttributeKey)
internal set(value) {
attributes.put(ComposeRoutingContentAttributeKey, value)
if (value != null) {
attributes.put(ComposeRoutingContentAttributeKey, value)
} else {
attributes.remove(ComposeRoutingContentAttributeKey)
}
}
Original file line number Diff line number Diff line change
@@ -35,6 +35,16 @@ internal var Routing.poppedCall: ApplicationCall?
}
}

internal var Routing.popResult: Any?
get() = attributes.getOrNull(ComposeRoutingPopResultAttributeKey)
set(value) {
if (value != null) {
attributes.put(ComposeRoutingPopResultAttributeKey, value)
} else {
attributes.remove(ComposeRoutingPopResultAttributeKey)
}
}

public var ApplicationCall.popped: Boolean
get() = attributes.getOrNull(ComposeRoutingPoppedFlagCallAttributeKey) ?: false
internal set(value) {
@@ -52,3 +62,9 @@ public inline fun <reified T> ApplicationCall.popResult(default: T): T = popResu
public inline fun <reified T> ApplicationCall.popResult(default: () -> T): T = popResult() ?: default()

public fun Routing.poppedCall(): ApplicationCall? = poppedCall

public inline fun <reified T> Routing.popResult(): T? = poppedCall()?.popResult()

public inline fun <reified T> Routing.popResult(default: T): T = popResult() ?: default

public inline fun <reified T> Routing.popResult(default: () -> T): T = popResult() ?: default()
Original file line number Diff line number Diff line change
@@ -7,6 +7,9 @@ import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import dev.programadorthi.routing.compose.history.ComposeHistoryMode
import dev.programadorthi.routing.compose.history.historyMode
import dev.programadorthi.routing.compose.history.restoreState
import dev.programadorthi.routing.core.Route
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application
@@ -24,11 +27,12 @@ public val LocalRouting: ProvidableCompositionLocal<Routing> =
public fun CurrentContent() {
val routing = LocalRouting.current
val lastCall = routing.callStack.last()
lastCall.content(lastCall)
lastCall.content?.invoke(lastCall)
}

@Composable
public fun Routing(
historyMode: ComposeHistoryMode = ComposeHistoryMode.Memory,
routing: Routing,
initial: ComposeContent,
content: ComposeContent = { CurrentContent() },
@@ -45,14 +49,17 @@ public fun Routing(
call.content = initial
stack += call
routing.callStack = stack
routing.historyMode = historyMode
routing
}
router.restoreState()
content(router.callStack.last())
}
}

@Composable
public fun Routing(
historyMode: ComposeHistoryMode = ComposeHistoryMode.Memory,
rootPath: String = "/",
parent: Routing? = null,
parentCoroutineContext: CoroutineContext? = null,
@@ -81,6 +88,7 @@ public fun Routing(
}

Routing(
historyMode = historyMode,
routing = routing,
initial = initial,
content = content,
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package dev.programadorthi.routing.compose

import androidx.compose.runtime.Composable
import dev.programadorthi.routing.compose.history.platformPush
import dev.programadorthi.routing.compose.history.platformReplace
import dev.programadorthi.routing.compose.history.platformReplaceAll
import dev.programadorthi.routing.compose.history.shouldNeglect
import dev.programadorthi.routing.core.Route
import dev.programadorthi.routing.core.RouteMethod
import dev.programadorthi.routing.core.Routing
@@ -59,30 +63,27 @@ public inline fun <reified T : Any> Route.composable(
}
}

public fun <T> PipelineContext<Unit, ApplicationCall>.composable(
public suspend fun <T> PipelineContext<Unit, ApplicationCall>.composable(
routing: Routing,
resource: T?,
body: ComposeContent,
) {
routing.poppedCall = null // Clear pop call after each new routing call

call.popped = false
call.resource = resource
call.content = body

val callStack = routing.callStack
when (call.routeMethod) {
RouteMethod.Push -> callStack.add(call)
RouteMethod.Replace -> {
callStack.removeLastOrNull()
callStack.add(call)
}
if (call.shouldNeglect()) {
return
}

RouteMethod.ReplaceAll -> {
callStack.clear()
callStack.add(call)
}
// Clear pop call after each new routing call
routing.poppedCall = null
routing.popResult = null

when (call.routeMethod) {
RouteMethod.Push -> call.platformPush(routing)
RouteMethod.Replace -> call.platformReplace(routing)
RouteMethod.ReplaceAll -> call.platformReplaceAll(routing)
else ->
error(
"Compose needs a stack route method to work. You called a composable ${call.uri} " +
Original file line number Diff line number Diff line change
@@ -1,13 +1,112 @@
package dev.programadorthi.routing.compose

import dev.programadorthi.routing.compose.history.ComposeHistoryNeglectAttributeKey
import dev.programadorthi.routing.core.RouteMethod
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.call
import io.ktor.http.Parameters
import io.ktor.util.Attributes

public fun Routing.canPop(): Boolean = callStack.size > 1
internal expect fun Routing.popOnPlatform(
result: Any? = null,
fallback: () -> Unit,
)

public fun Routing.pop(result: Any? = null) {
if (!canPop()) return
popOnPlatform(result = result) {
if (callStack.size < 2) return@popOnPlatform

poppedCall = callStack.removeLastOrNull()
poppedCall?.popped = true
poppedCall?.popResult = result
poppedCall = callStack.removeLastOrNull()
poppedCall?.popped = true
poppedCall?.popResult = result

val last = callStack.lastOrNull() ?: return@popOnPlatform
if (last.content == null) {
execute(last)
}
}
}

public fun Routing.push(
path: String,
parameters: Parameters = Parameters.Empty,
neglect: Boolean = false,
) {
call(
uri = path,
parameters = parameters,
routeMethod = RouteMethod.Push,
attributes = neglect.toAttributes(),
)
}

public fun Routing.pushNamed(
name: String,
parameters: Parameters = Parameters.Empty,
neglect: Boolean = false,
) {
call(
name = name,
parameters = parameters,
routeMethod = RouteMethod.Push,
attributes = neglect.toAttributes(),
)
}

public fun Routing.replace(
path: String,
parameters: Parameters = Parameters.Empty,
neglect: Boolean = false,
) {
call(
uri = path,
parameters = parameters,
routeMethod = RouteMethod.Replace,
attributes = neglect.toAttributes(),
)
}

public fun Routing.replaceNamed(
name: String,
parameters: Parameters = Parameters.Empty,
neglect: Boolean = false,
) {
call(
name = name,
parameters = parameters,
routeMethod = RouteMethod.Replace,
attributes = neglect.toAttributes(),
)
}

public fun Routing.replaceAll(
path: String,
parameters: Parameters = Parameters.Empty,
neglect: Boolean = false,
) {
call(
uri = path,
parameters = parameters,
routeMethod = RouteMethod.ReplaceAll,
attributes = neglect.toAttributes(),
)
}

public fun Routing.replaceAllNamed(
name: String,
parameters: Parameters = Parameters.Empty,
neglect: Boolean = false,
) {
call(
name = name,
parameters = parameters,
routeMethod = RouteMethod.ReplaceAll,
attributes = neglect.toAttributes(),
)
}

private fun Boolean.toAttributes(): Attributes {
val attributes = Attributes()
attributes.put(ComposeHistoryNeglectAttributeKey, this)
return attributes
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package dev.programadorthi.routing.compose.history

import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application
import dev.programadorthi.routing.core.application.Application
import dev.programadorthi.routing.core.application.ApplicationCall
import io.ktor.util.AttributeKey

internal val ComposeHistoryModeAttributeKey: AttributeKey<ComposeHistoryMode> =
AttributeKey("ComposeHistoryModeAttributeKey")

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

internal val ComposeHistoryRestoredCallAttributeKey: AttributeKey<Boolean> =
AttributeKey("ComposeHistoryRestoredCallAttributeKey")

internal var Application.historyMode: ComposeHistoryMode
get() = attributes[ComposeHistoryModeAttributeKey]
set(value) {
attributes.put(ComposeHistoryModeAttributeKey, value)
}

internal var Routing.historyMode: ComposeHistoryMode
get() = application.historyMode
set(value) {
application.historyMode = value
}

internal var ApplicationCall.neglect: Boolean
get() = attributes.getOrNull(ComposeHistoryNeglectAttributeKey) ?: false
set(value) {
attributes.put(ComposeHistoryNeglectAttributeKey, value)
}

internal var ApplicationCall.restored: Boolean
get() = attributes.getOrNull(ComposeHistoryRestoredCallAttributeKey) ?: false
set(value) {
attributes.put(ComposeHistoryRestoredCallAttributeKey, value)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.programadorthi.routing.compose.history

import androidx.compose.runtime.Composable
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application.ApplicationCall

internal expect suspend fun ApplicationCall.platformPush(routing: Routing)

internal expect suspend fun ApplicationCall.platformReplace(routing: Routing)

internal expect suspend fun ApplicationCall.platformReplaceAll(routing: Routing)

internal expect fun ApplicationCall.shouldNeglect(): Boolean

@Composable
internal expect fun Routing.restoreState()
Loading

0 comments on commit af8c513

Please sign in to comment.