Skip to content

Commit

Permalink
Merge pull request #130 from arkivanov/merge-master-to-compose-darwin
Browse files Browse the repository at this point in the history
Merge master into compose-darwin
  • Loading branch information
arkivanov authored Jun 26, 2022
2 parents 1ad8eb8 + e6464a6 commit d501340
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 93 deletions.
12 changes: 9 additions & 3 deletions decompose/api/android/decompose.api
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,21 @@ public abstract interface class com/arkivanov/decompose/router/Router {
}

public final class com/arkivanov/decompose/router/RouterExtKt {
public static final fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final synthetic fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
public static synthetic fun bringToFront$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
public static final fun getActiveChild (Lcom/arkivanov/decompose/router/Router;)Lcom/arkivanov/decompose/Child$Created;
public static final fun navigate (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static final fun pop (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun pop$default (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun popWhile (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static final fun popWhile (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public static final fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final synthetic fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
public static synthetic fun push$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
public static final synthetic fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
public static synthetic fun replaceCurrent$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
}

public final class com/arkivanov/decompose/router/RouterFactoryExtKt {
Expand Down
12 changes: 9 additions & 3 deletions decompose/api/jvm/decompose.api
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,21 @@ public abstract interface class com/arkivanov/decompose/router/Router {
}

public final class com/arkivanov/decompose/router/RouterExtKt {
public static final fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final synthetic fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
public static synthetic fun bringToFront$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
public static final fun getActiveChild (Lcom/arkivanov/decompose/router/Router;)Lcom/arkivanov/decompose/Child$Created;
public static final fun navigate (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static final fun pop (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun pop$default (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun popWhile (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static final fun popWhile (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public static final fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final synthetic fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
public static synthetic fun push$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
public static final synthetic fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
public static synthetic fun replaceCurrent$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
}

public final class com/arkivanov/decompose/router/RouterFactoryExtKt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ fun <C : Any> Router<C, *>.navigate(transformer: (stack: List<C>) -> List<C>) {
navigate(transformer = transformer, onComplete = { _, _ -> })
}

@Deprecated(message = "For binary compatibility", level = DeprecationLevel.HIDDEN)
fun <C : Any> Router<C, *>.push(configuration: C) {
push(configuration = configuration, onComplete = {})
}

/**
* Pushes the provided [configuration] at the top of the stack..
* Pushes the provided [configuration] at the top of the stack.
*
* @param onComplete called when the navigation is finished (either synchronously or asynchronously).
*/
fun <C : Any> Router<C, *>.push(configuration: C) {
navigate { it + configuration }
fun <C : Any> Router<C, *>.push(configuration: C, onComplete: () -> Unit = {}) {
navigate(transformer = { it + configuration }, onComplete = { _, _ -> onComplete() })
}

/**
Expand All @@ -26,20 +33,15 @@ fun <C : Any> Router<C, *>.push(configuration: C) {
fun <C : Any> Router<C, *>.pop(onComplete: (isSuccess: Boolean) -> Unit = {}) {
navigate(
transformer = { stack -> stack.takeIf { it.size > 1 }?.dropLast(1) ?: stack },
onComplete = { newStack, oldStack -> onComplete(newStack.size < oldStack.size) }
onComplete = { newStack, oldStack -> onComplete(newStack.size < oldStack.size) },
)
}

/**
* Drops the configurations at the top of the stack while the [predicate] returns true
* Drops the configurations at the top of the stack while the [predicate] returns `true`.
*/
inline fun <C : Any> Router<C, *>.popWhile(
crossinline predicate: (C) -> Boolean
) {
popWhile(
predicate = predicate,
onComplete = {}
)
inline fun <C : Any> Router<C, *>.popWhile(crossinline predicate: (C) -> Boolean) {
popWhile(predicate = predicate, onComplete = {})
}

/**
Expand All @@ -50,29 +52,45 @@ inline fun <C : Any> Router<C, *>.popWhile(
*/
inline fun <C : Any> Router<C, *>.popWhile(
crossinline predicate: (C) -> Boolean,
crossinline onComplete: (isSuccess: Boolean) -> Unit
crossinline onComplete: (isSuccess: Boolean) -> Unit,
) {
navigate(
transformer = { stack -> stack.dropLastWhile(predicate) },
onComplete = { newStack, oldStack -> onComplete(newStack.size < oldStack.size) }
onComplete = { newStack, oldStack -> onComplete(newStack.size < oldStack.size) },
)
}

@Deprecated(message = "For binary compatibility", level = DeprecationLevel.HIDDEN)
fun <C : Any> Router<C, *>.replaceCurrent(configuration: C) {
replaceCurrent(configuration = configuration, onComplete = {})
}

/**
* Replaces the current configuration at the top of the stack with the provided [configuration].
*
* @param onComplete called when the navigation is finished (either synchronously or asynchronously).
*/
fun <C : Any> Router<C, *>.replaceCurrent(configuration: C) {
navigate { it.dropLast(1) + configuration }
fun <C : Any> Router<C, *>.replaceCurrent(configuration: C, onComplete: () -> Unit = {}) {
navigate(
transformer = { it.dropLast(1) + configuration },
onComplete = { _, _ -> onComplete() },
)
}

@Deprecated(message = "For binary compatibility", level = DeprecationLevel.HIDDEN)
fun <C : Any> Router<C, *>.bringToFront(configuration: C) {
bringToFront(configuration = configuration, onComplete = {})
}

/**
* Removes all components with configurations of [configuration]'s class, and adds the provided [configuration] to the top of the stack.
* The operation is performed as one transaction. If there is already a component with the same configuration, it will not be recreated.
*/
fun <C : Any> Router<C, *>.bringToFront(configuration: C) {
navigate { stack ->
stack.filterNot { it::class == configuration::class } + configuration
}
fun <C : Any> Router<C, *>.bringToFront(configuration: C, onComplete: () -> Unit = {}) {
navigate(
transformer = { stack -> stack.filterNot { it::class == configuration::class } + configuration },
onComplete = { _, _ -> onComplete() },
)
}

val <C : Any, T : Any> Router<C, T>.activeChild: Child.Created<C, T> get() = state.value.activeChild
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.arkivanov.decompose.router

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

@Suppress("TestFunctionName")
class RouterPopWhileTest {

@Test
fun WHEN_popWhile_THEN_popped() {
val router = TestRouter(listOf(1, 2, 3, 4))

router.popWhile { it != 2 }

assertEquals(listOf(1, 2), router.stack)
}

@Test
fun WHEN_popWhile_THEN_onComplete_called() {
val router = TestRouter(listOf(1, 2, 3, 4))
var isCalled = false

router.popWhile(predicate = { it != 2 }, onComplete = { isCalled = true })

assertTrue(isCalled)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.arkivanov.decompose.router

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

@Suppress("TestFunctionName")
class RouterPushTest {

@Test
fun WHEN_push_THEN_pushed() {
val router = TestRouter(listOf(1, 2))

router.push(3)

assertEquals(listOf(1, 2, 3), router.stack)
}

@Test
fun WHEN_push_THEN_onComplete_called() {
val router = TestRouter(listOf(1, 2))
var isCalled = false

router.push(3) { isCalled = true }

assertTrue(isCalled)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.arkivanov.decompose.router

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

@Suppress("TestFunctionName")
class RouterReplaceCurrentTest {

@Test
fun WHEN_replaceCurrent_THEN_last_configuration_replaced() {
val router = TestRouter(listOf(1, 2, 3))

router.replaceCurrent(4)

assertEquals(listOf(1, 2, 4), router.stack)
}

@Test
fun WHEN_replaceCurrent_THEN_onComplete_called() {
val router = TestRouter(listOf(1, 2, 3))
var isCalled = false

router.replaceCurrent(4) { isCalled = true }

assertTrue(isCalled)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ import com.arkivanov.decompose.router.subscribe
import org.w3c.dom.PopStateEvent

@ExperimentalDecomposeApi
class DefaultWebHistoryController(
class DefaultWebHistoryController internal constructor(
private val window: Window,
) : WebHistoryController {

@Suppress("unused") // Public API
constructor() : this(WindowImpl())

override fun <C : Any> attach(
router: Router<C, *>,
getPath: (configuration: C) -> String,
getConfiguration: (path: String) -> C
getConfiguration: (path: String) -> C,
) {
val impl = Impl(router, window, getPath, getConfiguration)
router.state.subscribe(impl::onStateChanged)
Expand Down Expand Up @@ -118,90 +119,52 @@ class DefaultWebHistoryController(

fun onPopState(event: PopStateEvent) {
val newData = event.getData() ?: return
val stack = router.state.value.configurations()
val newConfigurationKey = newData.configurationKey

val indexInStack = stack.indexOfLast { it.hashCode() == newConfigurationKey }
if (indexInStack >= 0) {
if (indexInStack < stack.lastIndex) { // History popped, pop from the Router
isStateObserverEnabled = false
router.navigate { stack.take(indexInStack + 1) }
isStateObserverEnabled = true
val newConfigurationKey = newData.last().configurationKey

isStateObserverEnabled = false

router.navigate { stack ->
val indexInStack = stack.indexOfLast { it.hashCode() == newConfigurationKey }
if (indexInStack >= 0) {
// History popped, pop from the Router
stack.take(indexInStack + 1)
} else {
// History pushed, push to the Router
stack + newData.drop(stack.size).map { getConfiguration(it.path) }
}
} else { // History pushed, push to the Router
val nextPaths = getNextPaths(currentConfiguration = stack.last(), nextData = newData)
val nextConfigurations = nextPaths.map(getConfiguration)
isStateObserverEnabled = false
router.navigate { stack + nextConfigurations }
isStateObserverEnabled = true
}
}

private fun getNextPaths(currentConfiguration: C, nextData: PageData): List<String> {
val paths = ArrayList<String>()
val currentConfigurationKey = currentConfiguration.hashCode()
var data: PageData? = nextData

while ((data != null) && (data.configurationKey != currentConfigurationKey)) {
paths += data.path
data = data.prev
}

return paths.asReversed()
isStateObserverEnabled = true
}

private fun History.pushState(configuration: C) {
val currentData: PageData? = window.history.getData()

val nextData =
PageData(
configurationKey = configuration.hashCode(),
path = getPath(configuration),
prev = currentData,
)

currentData?.next = nextData

pushState(data = nextData, url = nextData.path)
val nextItem = PageItem(configurationKey = configuration.hashCode(), path = getPath(configuration))
val newData = getCurrentData() + nextItem
pushState(data = newData, url = nextItem.path)
}

private fun History.replaceState(configuration: C) {
val currentData: PageData? = window.history.getData()
val prevData: PageData? = currentData?.prev
val nextData: PageData? = currentData?.next

val newData =
PageData(
configurationKey = configuration.hashCode(),
path = getPath(configuration),
prev = prevData,
next = nextData,
)

prevData?.next = nextData
nextData?.prev = newData

replaceState(data = newData, url = newData.path)
val newItem = PageItem(configurationKey = configuration.hashCode(), path = getPath(configuration))
val newData = getCurrentData().dropLast(1).toTypedArray() + newItem
replaceState(data = newData, url = newItem.path)
}

private fun History.getData(): PageData? = state?.unsafeCast<PageData>()
private fun getCurrentData(): Array<PageItem> = window.history.state?.unsafeCast<Array<PageItem>>() ?: emptyArray()

private fun PopStateEvent.getData(): PageData? = state?.unsafeCast<PageData>()
private fun PopStateEvent.getData(): Array<PageItem>? = state?.unsafeCast<Array<PageItem>>()
}

private data class PageData(
private data class PageItem(
val configurationKey: Int,
val path: String,
var prev: PageData? = null,
var next: PageData? = null,
)

interface Window {
internal interface Window {
val history: History
var onPopState: ((PopStateEvent) -> Unit)?
}

interface History {
internal interface History {
val state: Any?

fun go(delta: Int)
Expand Down
Loading

0 comments on commit d501340

Please sign in to comment.