Skip to content

Commit

Permalink
feat: compose and stack behaviors integration
Browse files Browse the repository at this point in the history
  • Loading branch information
programadorthi committed Dec 21, 2023
1 parent 2c97aa7 commit 0fe75d7
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 16 deletions.
2 changes: 1 addition & 1 deletion compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api(projects.core)
api(projects.coreStack)
implementation(libs.compose.runtime)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ package dev.programadorthi.routing.compose
import androidx.compose.runtime.Composable
import dev.programadorthi.routing.core.Route
import dev.programadorthi.routing.core.RouteMethod
import dev.programadorthi.routing.core.StackRouteMethod
import dev.programadorthi.routing.core.application.ApplicationCall
import dev.programadorthi.routing.core.application.application
import dev.programadorthi.routing.core.application.call
import dev.programadorthi.routing.core.previousCall
import dev.programadorthi.routing.core.route
import io.ktor.util.KtorDsl
import io.ktor.util.pipeline.PipelineContext
import io.ktor.util.pipeline.execute

@KtorDsl
public fun Route.composable(
Expand All @@ -29,6 +33,15 @@ public fun Route.composable(
body: @Composable PipelineContext<Unit, ApplicationCall>.() -> Unit,
) {
handle {
call.content = { body(this) }
// Avoiding recompose same content on a popped call
if (call.routeMethod != StackRouteMethod.Pop) {
call.content = { body(this) }
} else {
// Checking for previous ApplicationCall and redirecting to it
val previous = previousCall()
if (previous != null) {
application.execute(previous)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
package dev.programadorthi.routing.compose

import dev.programadorthi.routing.core.RouteMethod
import dev.programadorthi.routing.core.StackRouteMethod
import dev.programadorthi.routing.core.StackRouting
import dev.programadorthi.routing.core.application
import dev.programadorthi.routing.core.application.ApplicationCall
import dev.programadorthi.routing.core.application.call
import dev.programadorthi.routing.core.handle
import dev.programadorthi.routing.core.install
import dev.programadorthi.routing.core.pop
import dev.programadorthi.routing.core.push
import dev.programadorthi.routing.core.replace
import dev.programadorthi.routing.core.replaceAll
import dev.programadorthi.routing.core.route
import dev.programadorthi.routing.core.routing
import io.ktor.http.Parameters
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceTimeBy
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

@OptIn(ExperimentalCoroutinesApi::class)
internal class ComposeRoutingTest {
Expand Down Expand Up @@ -186,4 +197,205 @@ internal class ComposeRoutingTest {
// THEN
assertEquals("I'm the generic based content", fakeContent.result)
}

@Test
fun shouldPushAComposable() =
runComposeTest { coroutineContext, composition, clock ->
// GIVEN
val fakeContent = FakeContent()
var result: ApplicationCall? = null

val routing = routing(parentCoroutineContext = coroutineContext) {
install(StackRouting)

composable(path = "/path") {
result = call
fakeContent.content = "I'm the push based content"
fakeContent.Composable()
}
}

composition.setContent {
Routing(
routing = routing,
initial = {
fakeContent.content = "I'm the initial content"
fakeContent.Composable()
},
)
}

// WHEN
routing.push(path = "/path")
advanceTimeBy(99) // Ask for routing
clock.sendFrame(0L) // Ask for recomposition

// THEN
assertNotNull(result)
assertEquals("I'm the push based content", fakeContent.result)
assertEquals("/path", "${result?.uri}")
assertEquals("", "${result?.name}")
assertEquals(StackRouteMethod.Push, result?.routeMethod)
assertEquals(Parameters.Empty, result?.parameters)
}

@Test
fun shouldReplaceAComposable() =
runComposeTest { coroutineContext, composition, clock ->
// GIVEN
val pushContent = FakeContent()
val replaceContent = FakeContent()
var result: ApplicationCall? = null

val routing = routing(parentCoroutineContext = coroutineContext) {
install(StackRouting)

composable(path = "/push") {
pushContent.content = "I'm the push based content"
pushContent.Composable()
}
composable(path = "/replace") {
result = call
replaceContent.content = "I'm the replace based content"
replaceContent.Composable()
}
}

composition.setContent {
Routing(
routing = routing,
initial = {
replaceContent.content = "I'm the initial content"
replaceContent.Composable()
},
)
}

// WHEN
routing.push(path = "/push")
advanceTimeBy(99) // Ask for routing
clock.sendFrame(0L) // Ask for recomposition

routing.replace(path = "/replace")
advanceTimeBy(99) // Ask for routing
clock.sendFrame(0L) // Ask for recomposition

// THEN
assertNotNull(result)
assertEquals("I'm the push based content", pushContent.result)
assertEquals("I'm the replace based content", replaceContent.result)
assertEquals("/replace", "${result?.uri}")
assertEquals("", "${result?.name}")
assertEquals(StackRouteMethod.Replace, result?.routeMethod)
assertEquals(Parameters.Empty, result?.parameters)
}

@Test
fun shouldReplaceAllComposable() =
runComposeTest { coroutineContext, composition, clock ->
// GIVEN
val pushContent = FakeContent()
val replaceContent = FakeContent()
var result: ApplicationCall? = null

val routing = routing(parentCoroutineContext = coroutineContext) {
install(StackRouting)

composable(path = "/push") {
pushContent.content = "I'm the push based content"
pushContent.Composable()
}
composable(path = "/replace") {
result = call
replaceContent.content = "I'm the replace all based content"
replaceContent.Composable()
}
}

composition.setContent {
Routing(
routing = routing,
initial = {
replaceContent.content = "I'm the initial content"
replaceContent.Composable()
},
)
}

// WHEN
routing.push(path = "/push")
advanceTimeBy(99) // Ask for routing
clock.sendFrame(0L) // Ask for recomposition

routing.replaceAll(path = "/replace")
advanceTimeBy(99) // Ask for routing
clock.sendFrame(0L) // Ask for recomposition

// THEN
assertNotNull(result)
assertEquals("I'm the push based content", pushContent.result)
assertEquals("I'm the replace all based content", replaceContent.result)
assertEquals("/replace", "${result?.uri}")
assertEquals("", "${result?.name}")
assertEquals(StackRouteMethod.ReplaceAll, result?.routeMethod)
assertEquals(Parameters.Empty, result?.parameters)
}

@Test
fun shouldPopAComposable() =
runComposeTest { coroutineContext, composition, clock ->
// GIVEN
var poppedCall: ApplicationCall? = null
var result: ApplicationCall? = null
var pushedCounter = 0

val routing = routing(parentCoroutineContext = coroutineContext) {
install(StackRouting)

composable(path = "/push") {
pushedCounter += 1
}
composable(path = "/pop") {
result = call
if (call.routeMethod == StackRouteMethod.Pop) {
error("I will never be called in a composable with a pop call")
}
}

handle(path = "/pop") {
poppedCall = call
}
}

composition.setContent {
Routing(
routing = routing,
initial = {},
)
}

// WHEN
routing.push(path = "/push")
advanceTimeBy(99) // Ask for routing
clock.sendFrame(0L) // Ask for recomposition

routing.push(path = "/pop")
advanceTimeBy(99) // Ask for routing
clock.sendFrame(0L) // Ask for recomposition

routing.pop()
advanceTimeBy(99) // Ask for routing
clock.sendFrame(0L) // Ask for recomposition

// THEN
assertEquals(2, pushedCounter)
assertEquals("/pop", "${result?.uri}")
assertEquals("", "${result?.name}")
assertEquals(StackRouteMethod.Push, result?.routeMethod)
assertEquals(Parameters.Empty, result?.parameters)
assertEquals("/pop", "${poppedCall?.uri}")
assertEquals("", "${poppedCall?.name}")
assertEquals(StackRouteMethod.Pop, poppedCall?.routeMethod)
assertEquals(Parameters.Empty, poppedCall?.parameters)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ internal class StackManager(

fun lastOrNull(): ApplicationCall? = stack.lastOrNull()

fun toPop(): ApplicationCall? = stack.removeLastOrNull()

fun update(call: ApplicationCall) {
// Check if route should be out of the stack
if (call.stackNeglect) {
if (call.stackNeglect || call.routeMethod == StackRouteMethod.Pop) {
return
}

Expand All @@ -113,13 +115,6 @@ internal class StackManager(
stack += call
}

StackRouteMethod.Pop -> {
// Pop in a valid state only
if (call.uri == stack.lastOrNull()?.uri) {
stack.removeLastOrNull()
}
}

else -> Unit
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ import dev.programadorthi.routing.core.application.ApplicationCall
import io.ktor.http.Parameters

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

Expand Down

0 comments on commit 0fe75d7

Please sign in to comment.