From 0137389fd4da80ee6a8c15f8306121f2e7768413 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sat, 26 Oct 2024 12:41:33 +0100 Subject: [PATCH] Added the new web history API with nested navigation support --- decompose/api/android/decompose.api | 58 ++ decompose/api/decompose.klib.api | 60 ++ decompose/api/jvm/decompose.api | 58 ++ .../arkivanov/decompose/router/pages/Pages.kt | 3 + .../router/pages/PagesWebNavigation.kt | 96 +++ .../router/panels/PanelsWebNavigation.kt | 116 +++ .../router/stack/ChildStackFactory.kt | 2 +- .../router/stack/StackWebNavigation.kt | 109 +++ .../router/webhistory/NoOpWebNavigation.kt | 22 + .../router/webhistory/WebNavigation.kt | 63 ++ .../router/webhistory/WebNavigationOwner.kt | 21 + .../router/webhistory/WebHistoryNavigation.kt | 49 ++ .../router/webhistory/WebHistoryNavigation.kt | 49 ++ .../kotlin/com/arkivanov/decompose/Utils.kt | 17 + .../arkivanov/decompose/router/stack/Utils.kt | 6 +- .../router/webhistory/WebHistoryNavigation.kt | 363 +++++++++ .../com/arkivanov/decompose/UrlEncodeTest.kt | 16 + .../router/webhistory/TestWebNavigation.kt | 89 ++ .../webhistory/WebHistoryNavigationTest.kt | 768 ++++++++++++++++++ .../UserInterfaceState.xcuserstate | Bin 43756 -> 44128 bytes .../app-ios-compose/iOSApp.swift | 3 +- sample/app-ios/app-ios/app_iosApp.swift | 3 +- .../arkivanov/decompose/sample/app/Main.kt | 20 +- .../src/jsMain/resources/index.html | 1 + .../kotlin/com/arkivanov/sample/app/Main.kt | 19 +- sample/app-js/src/main/resources/index.html | 1 + sample/shared/compose/build.gradle.kts | 4 + .../sample/shared/cards/CardsContent.kt | 3 + .../sample/shared/counters/CountersContent.kt | 3 + .../sample/shared/menu/MenuContent.kt | 3 + .../shared/multipane/MultiPaneContent.kt | 3 + .../gallery/GalleryContent.kt | 3 + .../sharedtransitions/photo/PhotoContent.kt | 3 + .../arkivanov/sample/shared/utils/Utils.kt | 3 + .../arkivanov/sample/shared/utils/Utils.js.kt | 13 + .../sample/shared/utils/Utils.nonWeb.kt | 8 + .../kotlin/com/arkivanov/sample/shared/Url.kt | 36 + .../com/arkivanov/sample/shared/Utils.kt | 11 + .../multipane/DefaultMultiPaneComponent.kt | 37 +- .../shared/multipane/MultiPaneComponent.kt | 3 +- .../multipane/PreviewMultiPaneComponent.kt | 6 +- .../shared/pages/DefaultPagesComponent.kt | 30 +- .../sample/shared/pages/PagesComponent.kt | 5 +- .../shared/root/DefaultRootComponent.kt | 99 +-- .../shared/root/PreviewRootComponent.kt | 8 +- .../sample/shared/root/RootComponent.kt | 7 +- .../DefaultSharedTransitionsComponent.kt | 64 +- .../SharedTransitionsComponent.kt | 3 +- .../gallery/DefaultGalleryComponent.kt | 16 +- .../shared/sharedtransitions/photo/Image.kt | 2 - .../shared/tabs/DefaultTabsComponent.kt | 50 +- .../shared/tabs/PreviewTabsComponent.kt | 5 +- .../sample/shared/tabs/TabsComponent.kt | 3 +- .../root/RootComponentIntegrationTest.kt | 28 +- .../tabs/TabsComponentIntegrationTest.kt | 1 + 55 files changed, 2331 insertions(+), 141 deletions(-) create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesWebNavigation.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/PanelsWebNavigation.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackWebNavigation.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/NoOpWebNavigation.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigation.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigationOwner.kt create mode 100644 decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt create mode 100644 decompose/src/wasmJsMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt create mode 100644 decompose/src/webMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt create mode 100644 decompose/src/webTest/kotlin/com/arkivanov/decompose/UrlEncodeTest.kt create mode 100644 decompose/src/webTest/kotlin/com/arkivanov/decompose/router/webhistory/TestWebNavigation.kt create mode 100644 decompose/src/webTest/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigationTest.kt create mode 100644 sample/shared/compose/src/jsMain/kotlin/com/arkivanov/sample/shared/utils/Utils.js.kt create mode 100644 sample/shared/compose/src/nonWebMain/kotlin/com/arkivanov/sample/shared/utils/Utils.nonWeb.kt create mode 100644 sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/Url.kt diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api index 829c49e0a..93b7ddb52 100644 --- a/decompose/api/android/decompose.api +++ b/decompose/api/android/decompose.api @@ -187,6 +187,7 @@ public final class com/arkivanov/decompose/router/pages/ChildPagesFactoryKt { } public final class com/arkivanov/decompose/router/pages/Pages { + public static final field Companion Lcom/arkivanov/decompose/router/pages/Pages$Companion; public fun ()V public fun (Ljava/util/List;I)V public final fun component1 ()Ljava/util/List; @@ -200,6 +201,21 @@ public final class com/arkivanov/decompose/router/pages/Pages { public fun toString ()Ljava/lang/String; } +public synthetic class com/arkivanov/decompose/router/pages/Pages$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public fun (Lkotlinx/serialization/KSerializer;)V + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/arkivanov/decompose/router/pages/Pages; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/arkivanov/decompose/router/pages/Pages;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/arkivanov/decompose/router/pages/Pages$Companion { + public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer; +} + public abstract interface class com/arkivanov/decompose/router/pages/PagesNavigation : com/arkivanov/decompose/router/children/NavigationSource, com/arkivanov/decompose/router/pages/PagesNavigator { } @@ -234,6 +250,11 @@ public final class com/arkivanov/decompose/router/pages/PagesNavigatorExtKt { public static synthetic fun selectPrev$default (Lcom/arkivanov/decompose/router/pages/PagesNavigator;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } +public final class com/arkivanov/decompose/router/pages/PagesWebNavigationKt { + public static final fun childPagesWebNavigation (Lcom/arkivanov/decompose/router/pages/PagesNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; + public static synthetic fun childPagesWebNavigation$default (Lcom/arkivanov/decompose/router/pages/PagesNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + public final class com/arkivanov/decompose/router/panels/ChildPanels { public fun (Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/router/panels/ChildPanelsMode;)V public synthetic fun (Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/router/panels/ChildPanelsMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -354,6 +375,11 @@ public final class com/arkivanov/decompose/router/panels/PanelsNavigatorExtKt { public static synthetic fun setMode$default (Lcom/arkivanov/decompose/router/panels/PanelsNavigator;Lcom/arkivanov/decompose/router/panels/ChildPanelsMode;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } +public final class com/arkivanov/decompose/router/panels/PanelsWebNavigationKt { + public static final fun childPanelsWebNavigation (Lcom/arkivanov/decompose/router/panels/PanelsNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; + public static synthetic fun childPanelsWebNavigation$default (Lcom/arkivanov/decompose/router/panels/PanelsNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + public final class com/arkivanov/decompose/router/slot/ChildSlot { public fun ()V public fun (Lcom/arkivanov/decompose/Child$Created;)V @@ -472,6 +498,11 @@ public final class com/arkivanov/decompose/router/stack/StackNavigatorExtKt { public static synthetic fun replaceCurrent$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V } +public final class com/arkivanov/decompose/router/stack/StackWebNavigationKt { + public static final fun childStackWebNavigation (Lcom/arkivanov/decompose/router/stack/StackNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; + public static synthetic fun childStackWebNavigation$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + public final class com/arkivanov/decompose/router/stack/ValueExtKt { public static final fun getActive (Lcom/arkivanov/decompose/value/Value;)Lcom/arkivanov/decompose/Child$Created; public static final fun getBackStack (Lcom/arkivanov/decompose/value/Value;)Ljava/util/List; @@ -487,6 +518,33 @@ public final class com/arkivanov/decompose/router/stack/webhistory/WebHistoryCon public static synthetic fun attach$default (Lcom/arkivanov/decompose/router/stack/webhistory/WebHistoryController;Lcom/arkivanov/decompose/router/stack/StackNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } +public abstract interface class com/arkivanov/decompose/router/webhistory/WebNavigation { + public abstract fun getHistory ()Lcom/arkivanov/decompose/value/Value; + public abstract fun getSerializer ()Lkotlinx/serialization/KSerializer; + public abstract fun navigate (Ljava/util/List;)V + public abstract fun onBeforeNavigate ()Z +} + +public final class com/arkivanov/decompose/router/webhistory/WebNavigation$HistoryItem { + public fun (Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Lcom/arkivanov/decompose/router/webhistory/WebNavigationOwner;)V + public final fun getChild ()Lcom/arkivanov/decompose/router/webhistory/WebNavigationOwner; + public final fun getKey ()Ljava/lang/Object; + public final fun getParameters ()Ljava/util/Map; + public final fun getPath ()Ljava/lang/String; +} + +public abstract interface class com/arkivanov/decompose/router/webhistory/WebNavigationOwner { + public abstract fun getWebNavigation ()Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + +public abstract interface class com/arkivanov/decompose/router/webhistory/WebNavigationOwner$NoOp : com/arkivanov/decompose/router/webhistory/WebNavigationOwner { + public abstract fun getWebNavigation ()Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + +public final class com/arkivanov/decompose/router/webhistory/WebNavigationOwner$NoOp$DefaultImpls { + public static fun getWebNavigation (Lcom/arkivanov/decompose/router/webhistory/WebNavigationOwner$NoOp;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + public abstract class com/arkivanov/decompose/value/MutableValue : com/arkivanov/decompose/value/Value { public fun ()V public abstract fun compareAndSet (Ljava/lang/Object;Ljava/lang/Object;)Z diff --git a/decompose/api/decompose.klib.api b/decompose/api/decompose.klib.api index 59791561f..4f410b1aa 100644 --- a/decompose/api/decompose.klib.api +++ b/decompose/api/decompose.klib.api @@ -114,6 +114,29 @@ abstract interface <#A: kotlin/Any> com.arkivanov.decompose.router.stack/StackNa abstract fun navigate(kotlin/Function1, kotlin.collections/List<#A>>, kotlin/Function2, kotlin.collections/List<#A>, kotlin/Unit>) // com.arkivanov.decompose.router.stack/StackNavigator.navigate|navigate(kotlin.Function1,kotlin.collections.List<1:0>>;kotlin.Function2,kotlin.collections.List<1:0>,kotlin.Unit>){}[0] } +abstract interface <#A: kotlin/Any> com.arkivanov.decompose.router.webhistory/WebNavigation { // com.arkivanov.decompose.router.webhistory/WebNavigation|null[0] + abstract val history // com.arkivanov.decompose.router.webhistory/WebNavigation.history|{}history[0] + abstract fun (): com.arkivanov.decompose.value/Value>> // com.arkivanov.decompose.router.webhistory/WebNavigation.history.|(){}[0] + abstract val serializer // com.arkivanov.decompose.router.webhistory/WebNavigation.serializer|{}serializer[0] + abstract fun (): kotlinx.serialization/KSerializer<#A> // com.arkivanov.decompose.router.webhistory/WebNavigation.serializer.|(){}[0] + + abstract fun navigate(kotlin.collections/List<#A>) // com.arkivanov.decompose.router.webhistory/WebNavigation.navigate|navigate(kotlin.collections.List<1:0>){}[0] + abstract fun onBeforeNavigate(): kotlin/Boolean // com.arkivanov.decompose.router.webhistory/WebNavigation.onBeforeNavigate|onBeforeNavigate(){}[0] + + final class <#A1: out kotlin/Any?> HistoryItem { // com.arkivanov.decompose.router.webhistory/WebNavigation.HistoryItem|null[0] + constructor (#A1, kotlin/String, kotlin.collections/Map, com.arkivanov.decompose.router.webhistory/WebNavigationOwner?) // com.arkivanov.decompose.router.webhistory/WebNavigation.HistoryItem.|(1:0;kotlin.String;kotlin.collections.Map;com.arkivanov.decompose.router.webhistory.WebNavigationOwner?){}[0] + + final val child // com.arkivanov.decompose.router.webhistory/WebNavigation.HistoryItem.child|{}child[0] + final fun (): com.arkivanov.decompose.router.webhistory/WebNavigationOwner? // com.arkivanov.decompose.router.webhistory/WebNavigation.HistoryItem.child.|(){}[0] + final val key // com.arkivanov.decompose.router.webhistory/WebNavigation.HistoryItem.key|{}key[0] + final fun (): #A1 // com.arkivanov.decompose.router.webhistory/WebNavigation.HistoryItem.key.|(){}[0] + final val parameters // com.arkivanov.decompose.router.webhistory/WebNavigation.HistoryItem.parameters|{}parameters[0] + final fun (): kotlin.collections/Map // com.arkivanov.decompose.router.webhistory/WebNavigation.HistoryItem.parameters.|(){}[0] + final val path // com.arkivanov.decompose.router.webhistory/WebNavigation.HistoryItem.path|{}path[0] + final fun (): kotlin/String // com.arkivanov.decompose.router.webhistory/WebNavigation.HistoryItem.path.|(){}[0] + } +} + abstract interface <#A: out kotlin/Any> com.arkivanov.decompose.router.children/ChildNavState { // com.arkivanov.decompose.router.children/ChildNavState|null[0] abstract val configuration // com.arkivanov.decompose.router.children/ChildNavState.configuration|{}configuration[0] abstract fun (): #A // com.arkivanov.decompose.router.children/ChildNavState.configuration.|(){}[0] @@ -157,6 +180,16 @@ abstract interface com.arkivanov.decompose.router.stack.webhistory/WebHistoryCon abstract fun <#A1: kotlin/Any> attach(com.arkivanov.decompose.router.stack/StackNavigator<#A1>, com.arkivanov.decompose.value/Value>, kotlinx.serialization/KSerializer<#A1>, kotlin/Function1<#A1, kotlin/String>, kotlin/Function1, kotlin/Function2, kotlin.collections/List<#A1>, kotlin/Boolean> = ...) // com.arkivanov.decompose.router.stack.webhistory/WebHistoryController.attach|attach(com.arkivanov.decompose.router.stack.StackNavigator<0:0>;com.arkivanov.decompose.value.Value>;kotlinx.serialization.KSerializer<0:0>;kotlin.Function1<0:0,kotlin.String>;kotlin.Function1;kotlin.Function2,kotlin.collections.List<0:0>,kotlin.Boolean>){0§}[0] } +abstract interface com.arkivanov.decompose.router.webhistory/WebNavigationOwner { // com.arkivanov.decompose.router.webhistory/WebNavigationOwner|null[0] + abstract val webNavigation // com.arkivanov.decompose.router.webhistory/WebNavigationOwner.webNavigation|{}webNavigation[0] + abstract fun (): com.arkivanov.decompose.router.webhistory/WebNavigation<*> // com.arkivanov.decompose.router.webhistory/WebNavigationOwner.webNavigation.|(){}[0] + + abstract interface NoOp : com.arkivanov.decompose.router.webhistory/WebNavigationOwner { // com.arkivanov.decompose.router.webhistory/WebNavigationOwner.NoOp|null[0] + open val webNavigation // com.arkivanov.decompose.router.webhistory/WebNavigationOwner.NoOp.webNavigation|{}webNavigation[0] + open fun (): com.arkivanov.decompose.router.webhistory/WebNavigation<*> // com.arkivanov.decompose.router.webhistory/WebNavigationOwner.NoOp.webNavigation.|(){}[0] + } +} + abstract interface com.arkivanov.decompose/ComponentContext : com.arkivanov.decompose/GenericComponentContext // com.arkivanov.decompose/ComponentContext|null[0] abstract class <#A: kotlin/Any> com.arkivanov.decompose.value/MutableValue : com.arkivanov.decompose.value/Value<#A> { // com.arkivanov.decompose.value/MutableValue|null[0] @@ -334,6 +367,27 @@ final class <#A: out kotlin/Any> com.arkivanov.decompose.router.pages/Pages { // final fun equals(kotlin/Any?): kotlin/Boolean // com.arkivanov.decompose.router.pages/Pages.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // com.arkivanov.decompose.router.pages/Pages.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // com.arkivanov.decompose.router.pages/Pages.toString|toString(){}[0] + + final class <#A1: kotlin/Any?> $serializer : kotlinx.serialization.internal/GeneratedSerializer> { // com.arkivanov.decompose.router.pages/Pages.$serializer|null[0] + constructor (kotlinx.serialization/KSerializer<#A1>) // com.arkivanov.decompose.router.pages/Pages.$serializer.|(kotlinx.serialization.KSerializer<1:0>){}[0] + + final val descriptor // com.arkivanov.decompose.router.pages/Pages.$serializer.descriptor|{}descriptor[0] + final fun (): kotlinx.serialization.descriptors/SerialDescriptor // com.arkivanov.decompose.router.pages/Pages.$serializer.descriptor.|(){}[0] + final val typeSerial0 // com.arkivanov.decompose.router.pages/Pages.$serializer.typeSerial0|{}typeSerial0[0] + + final fun childSerializers(): kotlin/Array> // com.arkivanov.decompose.router.pages/Pages.$serializer.childSerializers|childSerializers(){}[0] + final fun deserialize(kotlinx.serialization.encoding/Decoder): com.arkivanov.decompose.router.pages/Pages<#A1> // com.arkivanov.decompose.router.pages/Pages.$serializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0] + final fun serialize(kotlinx.serialization.encoding/Encoder, com.arkivanov.decompose.router.pages/Pages<#A1>) // com.arkivanov.decompose.router.pages/Pages.$serializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;com.arkivanov.decompose.router.pages.Pages<1:0>){}[0] + final fun typeParametersSerializers(): kotlin/Array> // com.arkivanov.decompose.router.pages/Pages.$serializer.typeParametersSerializers|typeParametersSerializers(){}[0] + } + + final object Companion : kotlinx.serialization.internal/SerializerFactory { // com.arkivanov.decompose.router.pages/Pages.Companion|null[0] + final val $cachedDescriptor // com.arkivanov.decompose.router.pages/Pages.Companion.$cachedDescriptor|{}$cachedDescriptor[0] + final fun (): kotlinx.serialization.descriptors/SerialDescriptor // com.arkivanov.decompose.router.pages/Pages.Companion.$cachedDescriptor.|(){}[0] + + final fun <#A2: kotlin/Any?> serializer(kotlinx.serialization/KSerializer<#A2>): kotlinx.serialization/KSerializer> // com.arkivanov.decompose.router.pages/Pages.Companion.serializer|serializer(kotlinx.serialization.KSerializer<0:0>){0§}[0] + final fun serializer(kotlin/Array>...): kotlinx.serialization/KSerializer<*> // com.arkivanov.decompose.router.pages/Pages.Companion.serializer|serializer(kotlin.Array>...){}[0] + } } final class com.arkivanov.decompose/DefaultComponentContext : com.arkivanov.decompose/ComponentContext { // com.arkivanov.decompose/DefaultComponentContext|null[0] @@ -441,6 +495,7 @@ final fun <#A: com.arkivanov.decompose/GenericComponentContext<#A>, #B: kotlin/A final fun <#A: com.arkivanov.decompose/GenericComponentContext<#A>, #B: kotlin/Any, #C: kotlin/Any> (#A).com.arkivanov.decompose.router.stack/childStack(com.arkivanov.decompose.router.children/NavigationSource>, kotlinx.serialization/KSerializer<#B>?, #B, kotlin/String = ..., kotlin/Boolean = ..., kotlin/Function2<#B, #A, #C>): com.arkivanov.decompose.value/Value> // com.arkivanov.decompose.router.stack/childStack|childStack@0:0(com.arkivanov.decompose.router.children.NavigationSource>;kotlinx.serialization.KSerializer<0:1>?;0:1;kotlin.String;kotlin.Boolean;kotlin.Function2<0:1,0:0,0:2>){0§>;1§;2§}[0] final fun <#A: com.arkivanov.decompose/GenericComponentContext<#A>, #B: kotlin/Any, #C: kotlin/Any> (#A).com.arkivanov.decompose.router.stack/childStack(com.arkivanov.decompose.router.children/NavigationSource>, kotlinx.serialization/KSerializer<#B>?, kotlin/Function0>, kotlin/String = ..., kotlin/Boolean = ..., kotlin/Function2<#B, #A, #C>): com.arkivanov.decompose.value/Value> // com.arkivanov.decompose.router.stack/childStack|childStack@0:0(com.arkivanov.decompose.router.children.NavigationSource>;kotlinx.serialization.KSerializer<0:1>?;kotlin.Function0>;kotlin.String;kotlin.Boolean;kotlin.Function2<0:1,0:0,0:2>){0§>;1§;2§}[0] final fun <#A: com.arkivanov.decompose/GenericComponentContext<#A>> (#A).com.arkivanov.decompose/childContext(kotlin/String, com.arkivanov.essenty.lifecycle/Lifecycle? = ...): #A // com.arkivanov.decompose/childContext|childContext@0:0(kotlin.String;com.arkivanov.essenty.lifecycle.Lifecycle?){0§>}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any, #C: kotlin/Any, #D: kotlin/Any, #E: kotlin/Any, #F: kotlin/Any> com.arkivanov.decompose.router.panels/childPanelsWebNavigation(com.arkivanov.decompose.router.panels/PanelsNavigator<#A, #C, #E>, com.arkivanov.decompose.value/Value>, kotlinx.serialization/KSerializer<#A>, kotlinx.serialization/KSerializer<#C>, kotlinx.serialization/KSerializer<#E>, kotlin/Function1, kotlin/String?> = ..., kotlin/Function1, kotlin.collections/Map?> = ..., kotlin/Function0 = ..., kotlin/Function1, com.arkivanov.decompose.router.webhistory/WebNavigationOwner?> = ...): com.arkivanov.decompose.router.webhistory/WebNavigation<*> // com.arkivanov.decompose.router.panels/childPanelsWebNavigation|childPanelsWebNavigation(com.arkivanov.decompose.router.panels.PanelsNavigator<0:0,0:2,0:4>;com.arkivanov.decompose.value.Value>;kotlinx.serialization.KSerializer<0:0>;kotlinx.serialization.KSerializer<0:2>;kotlinx.serialization.KSerializer<0:4>;kotlin.Function1,kotlin.String?>;kotlin.Function1,kotlin.collections.Map?>;kotlin.Function0;kotlin.Function1,com.arkivanov.decompose.router.webhistory.WebNavigationOwner?>){0§;1§;2§;3§;4§;5§}[0] final fun <#A: kotlin/Any, #B: kotlin/Any, #C: kotlin/Any> (com.arkivanov.decompose.router.panels/PanelsNavigator<#A, #B, #C>).com.arkivanov.decompose.router.panels/activateDetails(#B, kotlin/Function2, com.arkivanov.decompose.router.panels/Panels<#A, #B, #C>, kotlin/Unit> = ...) // com.arkivanov.decompose.router.panels/activateDetails|activateDetails@com.arkivanov.decompose.router.panels.PanelsNavigator<0:0,0:1,0:2>(0:1;kotlin.Function2,com.arkivanov.decompose.router.panels.Panels<0:0,0:1,0:2>,kotlin.Unit>){0§;1§;2§}[0] final fun <#A: kotlin/Any, #B: kotlin/Any, #C: kotlin/Any> (com.arkivanov.decompose.router.panels/PanelsNavigator<#A, #B, #C>).com.arkivanov.decompose.router.panels/activateExtra(#C, kotlin/Function2, com.arkivanov.decompose.router.panels/Panels<#A, #B, #C>, kotlin/Unit> = ...) // com.arkivanov.decompose.router.panels/activateExtra|activateExtra@com.arkivanov.decompose.router.panels.PanelsNavigator<0:0,0:1,0:2>(0:2;kotlin.Function2,com.arkivanov.decompose.router.panels.Panels<0:0,0:1,0:2>,kotlin.Unit>){0§;1§;2§}[0] final fun <#A: kotlin/Any, #B: kotlin/Any, #C: kotlin/Any> (com.arkivanov.decompose.router.panels/PanelsNavigator<#A, #B, #C>).com.arkivanov.decompose.router.panels/activateMain(#A, kotlin/Function2, com.arkivanov.decompose.router.panels/Panels<#A, #B, #C>, kotlin/Unit> = ...) // com.arkivanov.decompose.router.panels/activateMain|activateMain@com.arkivanov.decompose.router.panels.PanelsNavigator<0:0,0:1,0:2>(0:0;kotlin.Function2,com.arkivanov.decompose.router.panels.Panels<0:0,0:1,0:2>,kotlin.Unit>){0§;1§;2§}[0] @@ -454,6 +509,8 @@ final fun <#A: kotlin/Any, #B: kotlin/Any, #C: kotlin/Any> (com.arkivanov.decomp final fun <#A: kotlin/Any, #B: kotlin/Any, #C: kotlin/Any> (com.arkivanov.decompose.router.panels/PanelsNavigator<#A, #B, #C>).com.arkivanov.decompose.router.panels/setMode(com.arkivanov.decompose.router.panels/ChildPanelsMode, kotlin/Function2, com.arkivanov.decompose.router.panels/Panels<#A, #B, #C>, kotlin/Unit> = ...) // com.arkivanov.decompose.router.panels/setMode|setMode@com.arkivanov.decompose.router.panels.PanelsNavigator<0:0,0:1,0:2>(com.arkivanov.decompose.router.panels.ChildPanelsMode;kotlin.Function2,com.arkivanov.decompose.router.panels.Panels<0:0,0:1,0:2>,kotlin.Unit>){0§;1§;2§}[0] final fun <#A: kotlin/Any, #B: kotlin/Any, #C: kotlin/Any> com.arkivanov.decompose.router.panels/PanelsNavigation(): com.arkivanov.decompose.router.panels/PanelsNavigation<#A, #B, #C> // com.arkivanov.decompose.router.panels/PanelsNavigation|PanelsNavigation(){0§;1§;2§}[0] final fun <#A: kotlin/Any, #B: kotlin/Any> (com.arkivanov.decompose.value/Value<#A>).com.arkivanov.decompose.value.operator/map(kotlin/Function1<#A, #B>): com.arkivanov.decompose.value/Value<#B> // com.arkivanov.decompose.value.operator/map|map@com.arkivanov.decompose.value.Value<0:0>(kotlin.Function1<0:0,0:1>){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> com.arkivanov.decompose.router.pages/childPagesWebNavigation(com.arkivanov.decompose.router.pages/PagesNavigator<#A>, com.arkivanov.decompose.value/Value>, kotlinx.serialization/KSerializer<#A>, kotlin/Function1, kotlin/String?> = ..., kotlin/Function1, kotlin.collections/Map?> = ..., kotlin/Function0 = ..., kotlin/Function1, com.arkivanov.decompose.router.webhistory/WebNavigationOwner?> = ...): com.arkivanov.decompose.router.webhistory/WebNavigation<*> // com.arkivanov.decompose.router.pages/childPagesWebNavigation|childPagesWebNavigation(com.arkivanov.decompose.router.pages.PagesNavigator<0:0>;com.arkivanov.decompose.value.Value>;kotlinx.serialization.KSerializer<0:0>;kotlin.Function1,kotlin.String?>;kotlin.Function1,kotlin.collections.Map?>;kotlin.Function0;kotlin.Function1,com.arkivanov.decompose.router.webhistory.WebNavigationOwner?>){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> com.arkivanov.decompose.router.stack/childStackWebNavigation(com.arkivanov.decompose.router.stack/StackNavigator<#A>, com.arkivanov.decompose.value/Value>, kotlinx.serialization/KSerializer<#A>, kotlin/Boolean = ..., kotlin/Function1, kotlin/String?> = ..., kotlin/Function1, kotlin.collections/Map?> = ..., kotlin/Function0 = ..., kotlin/Function1, com.arkivanov.decompose.router.webhistory/WebNavigationOwner?> = ...): com.arkivanov.decompose.router.webhistory/WebNavigation<*> // com.arkivanov.decompose.router.stack/childStackWebNavigation|childStackWebNavigation(com.arkivanov.decompose.router.stack.StackNavigator<0:0>;com.arkivanov.decompose.value.Value>;kotlinx.serialization.KSerializer<0:0>;kotlin.Boolean;kotlin.Function1,kotlin.String?>;kotlin.Function1,kotlin.collections.Map?>;kotlin.Function0;kotlin.Function1,com.arkivanov.decompose.router.webhistory.WebNavigationOwner?>){0§;1§}[0] final fun <#A: kotlin/Any> (com.arkivanov.decompose.router.pages/PagesNavigator<#A>).com.arkivanov.decompose.router.pages/clear(kotlin/Function2, com.arkivanov.decompose.router.pages/Pages<#A>, kotlin/Unit> = ...) // com.arkivanov.decompose.router.pages/clear|clear@com.arkivanov.decompose.router.pages.PagesNavigator<0:0>(kotlin.Function2,com.arkivanov.decompose.router.pages.Pages<0:0>,kotlin.Unit>){0§}[0] final fun <#A: kotlin/Any> (com.arkivanov.decompose.router.pages/PagesNavigator<#A>).com.arkivanov.decompose.router.pages/navigate(kotlin/Function1, com.arkivanov.decompose.router.pages/Pages<#A>>) // com.arkivanov.decompose.router.pages/navigate|navigate@com.arkivanov.decompose.router.pages.PagesNavigator<0:0>(kotlin.Function1,com.arkivanov.decompose.router.pages.Pages<0:0>>){0§}[0] final fun <#A: kotlin/Any> (com.arkivanov.decompose.router.pages/PagesNavigator<#A>).com.arkivanov.decompose.router.pages/select(kotlin/Int, kotlin/Function2, com.arkivanov.decompose.router.pages/Pages<#A>, kotlin/Unit> = ...) // com.arkivanov.decompose.router.pages/select|select@com.arkivanov.decompose.router.pages.PagesNavigator<0:0>(kotlin.Int;kotlin.Function2,com.arkivanov.decompose.router.pages.Pages<0:0>,kotlin.Unit>){0§}[0] @@ -496,3 +553,6 @@ final class com.arkivanov.decompose.router.stack.webhistory/DefaultWebHistoryCon final fun <#A1: kotlin/Any> attach(com.arkivanov.decompose.router.stack/StackNavigator<#A1>, com.arkivanov.decompose.value/Value>, kotlinx.serialization/KSerializer<#A1>, kotlin/Function1<#A1, kotlin/String>, kotlin/Function1, kotlin/Function2, kotlin.collections/List<#A1>, kotlin/Boolean>) // com.arkivanov.decompose.router.stack.webhistory/DefaultWebHistoryController.attach|attach(com.arkivanov.decompose.router.stack.StackNavigator<0:0>;com.arkivanov.decompose.value.Value>;kotlinx.serialization.KSerializer<0:0>;kotlin.Function1<0:0,kotlin.String>;kotlin.Function1;kotlin.Function2,kotlin.collections.List<0:0>,kotlin.Boolean>){0§}[0] } + +// Targets: [js, wasmJs] +final fun <#A: com.arkivanov.decompose.router.webhistory/WebNavigationOwner> com.arkivanov.decompose.router.webhistory/withWebHistory(kotlin/Function2): #A // com.arkivanov.decompose.router.webhistory/withWebHistory|withWebHistory(kotlin.Function2){0§}[0] diff --git a/decompose/api/jvm/decompose.api b/decompose/api/jvm/decompose.api index 81673ba37..33d26549c 100644 --- a/decompose/api/jvm/decompose.api +++ b/decompose/api/jvm/decompose.api @@ -167,6 +167,7 @@ public final class com/arkivanov/decompose/router/pages/ChildPagesFactoryKt { } public final class com/arkivanov/decompose/router/pages/Pages { + public static final field Companion Lcom/arkivanov/decompose/router/pages/Pages$Companion; public fun ()V public fun (Ljava/util/List;I)V public final fun component1 ()Ljava/util/List; @@ -180,6 +181,21 @@ public final class com/arkivanov/decompose/router/pages/Pages { public fun toString ()Ljava/lang/String; } +public synthetic class com/arkivanov/decompose/router/pages/Pages$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public fun (Lkotlinx/serialization/KSerializer;)V + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/arkivanov/decompose/router/pages/Pages; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/arkivanov/decompose/router/pages/Pages;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/arkivanov/decompose/router/pages/Pages$Companion { + public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer; +} + public abstract interface class com/arkivanov/decompose/router/pages/PagesNavigation : com/arkivanov/decompose/router/children/NavigationSource, com/arkivanov/decompose/router/pages/PagesNavigator { } @@ -214,6 +230,11 @@ public final class com/arkivanov/decompose/router/pages/PagesNavigatorExtKt { public static synthetic fun selectPrev$default (Lcom/arkivanov/decompose/router/pages/PagesNavigator;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } +public final class com/arkivanov/decompose/router/pages/PagesWebNavigationKt { + public static final fun childPagesWebNavigation (Lcom/arkivanov/decompose/router/pages/PagesNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; + public static synthetic fun childPagesWebNavigation$default (Lcom/arkivanov/decompose/router/pages/PagesNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + public final class com/arkivanov/decompose/router/panels/ChildPanels { public fun (Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/router/panels/ChildPanelsMode;)V public synthetic fun (Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/Child$Created;Lcom/arkivanov/decompose/router/panels/ChildPanelsMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -334,6 +355,11 @@ public final class com/arkivanov/decompose/router/panels/PanelsNavigatorExtKt { public static synthetic fun setMode$default (Lcom/arkivanov/decompose/router/panels/PanelsNavigator;Lcom/arkivanov/decompose/router/panels/ChildPanelsMode;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } +public final class com/arkivanov/decompose/router/panels/PanelsWebNavigationKt { + public static final fun childPanelsWebNavigation (Lcom/arkivanov/decompose/router/panels/PanelsNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; + public static synthetic fun childPanelsWebNavigation$default (Lcom/arkivanov/decompose/router/panels/PanelsNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + public final class com/arkivanov/decompose/router/slot/ChildSlot { public fun ()V public fun (Lcom/arkivanov/decompose/Child$Created;)V @@ -452,6 +478,11 @@ public final class com/arkivanov/decompose/router/stack/StackNavigatorExtKt { public static synthetic fun replaceCurrent$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V } +public final class com/arkivanov/decompose/router/stack/StackWebNavigationKt { + public static final fun childStackWebNavigation (Lcom/arkivanov/decompose/router/stack/StackNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; + public static synthetic fun childStackWebNavigation$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + public final class com/arkivanov/decompose/router/stack/ValueExtKt { public static final fun getActive (Lcom/arkivanov/decompose/value/Value;)Lcom/arkivanov/decompose/Child$Created; public static final fun getBackStack (Lcom/arkivanov/decompose/value/Value;)Ljava/util/List; @@ -467,6 +498,33 @@ public final class com/arkivanov/decompose/router/stack/webhistory/WebHistoryCon public static synthetic fun attach$default (Lcom/arkivanov/decompose/router/stack/webhistory/WebHistoryController;Lcom/arkivanov/decompose/router/stack/StackNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } +public abstract interface class com/arkivanov/decompose/router/webhistory/WebNavigation { + public abstract fun getHistory ()Lcom/arkivanov/decompose/value/Value; + public abstract fun getSerializer ()Lkotlinx/serialization/KSerializer; + public abstract fun navigate (Ljava/util/List;)V + public abstract fun onBeforeNavigate ()Z +} + +public final class com/arkivanov/decompose/router/webhistory/WebNavigation$HistoryItem { + public fun (Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Lcom/arkivanov/decompose/router/webhistory/WebNavigationOwner;)V + public final fun getChild ()Lcom/arkivanov/decompose/router/webhistory/WebNavigationOwner; + public final fun getKey ()Ljava/lang/Object; + public final fun getParameters ()Ljava/util/Map; + public final fun getPath ()Ljava/lang/String; +} + +public abstract interface class com/arkivanov/decompose/router/webhistory/WebNavigationOwner { + public abstract fun getWebNavigation ()Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + +public abstract interface class com/arkivanov/decompose/router/webhistory/WebNavigationOwner$NoOp : com/arkivanov/decompose/router/webhistory/WebNavigationOwner { + public abstract fun getWebNavigation ()Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + +public final class com/arkivanov/decompose/router/webhistory/WebNavigationOwner$NoOp$DefaultImpls { + public static fun getWebNavigation (Lcom/arkivanov/decompose/router/webhistory/WebNavigationOwner$NoOp;)Lcom/arkivanov/decompose/router/webhistory/WebNavigation; +} + public abstract class com/arkivanov/decompose/value/MutableValue : com/arkivanov/decompose/value/Value { public fun ()V public abstract fun compareAndSet (Ljava/lang/Object;Ljava/lang/Object;)Z diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/Pages.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/Pages.kt index b692a6639..6f8954c56 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/Pages.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/Pages.kt @@ -1,5 +1,7 @@ package com.arkivanov.decompose.router.pages +import kotlinx.serialization.Serializable + /** * Represents a state of Child Pages navigation model. * @@ -7,6 +9,7 @@ package com.arkivanov.decompose.router.pages * @param selectedIndex an index of the selected child configuration. * Must be within the range of [items] indices if [items] is not empty, otherwise can be any number. */ +@Serializable data class Pages( val items: List, val selectedIndex: Int, diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesWebNavigation.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesWebNavigation.kt new file mode 100644 index 000000000..76671233a --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesWebNavigation.kt @@ -0,0 +1,96 @@ +package com.arkivanov.decompose.router.pages + +import com.arkivanov.decompose.Child +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.router.webhistory.WebNavigation +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner +import com.arkivanov.decompose.value.Value +import com.arkivanov.decompose.value.operator.map +import kotlinx.serialization.KSerializer + +/** + * Creates and returns an implementation of [WebNavigation] attached + * to the provided [navigator] and [pages]. The navigation observes page + * changes and manipulates the browser history to match the state. It also + * observes the browser history changes and navigates the pages as needed. + * + * The navigation only takes into account the currently selected page, replacing + * the current entry of the browser history when the selected page changes. + * Multiple entries can still be pushed/popped to/from the browser history at the + * same time if required to follow the nested navigation. + * + * @param navigator a [PagesNavigator] that should be used for navigation when + * the user presses the browser back and forward buttons. + * @param pages an observable [ChildPages] that should be bound to the browser + * navigation history. + * @param serializer a [KSerializer] of configurations [C]. + * @param pathMapper an optional function that returns a part of the URL path for + * the provided [ChildPages], or `null` (or empty string) if the path shouldn't be + * affected. The resulting URL path is constructed by concatenating all URL parts from + * every parent and child navigation, top to bottom. Default return value is `null`. + * @param parametersMapper an optional function that returns a [Map] containing all + * required URL parameters for the provided [ChildPages], or `null` (or empty Map) + * if parameters are not required. The resulting URL parameters string is constructed + * by merging all URL parameters from every parent and child navigation. Default + * return value is `null`. + * @param onBeforeNavigate an optional callback that can be used to allow (by + * returning `true`) or deny (by returning `false`) the browser-initiated navigation. + * Can be used e.g. to display an alert dialog before closing a screen. Returns `true` + * by default. + * @param childSelector an optional function that selects and returns a child + * [WebNavigationOwner] for the provided child. + */ +@ExperimentalDecomposeApi +fun childPagesWebNavigation( + navigator: PagesNavigator, + pages: Value>, + serializer: KSerializer, + pathMapper: (ChildPages) -> String? = { null }, + parametersMapper: (ChildPages) -> Map? = { null }, + onBeforeNavigate: () -> Boolean = { true }, + childSelector: (Child.Created) -> WebNavigationOwner? = { null }, +): WebNavigation<*> = + PagesWebNavigation( + navigator = navigator, + pages = pages, + serializer = serializer, + pathMapper = pathMapper, + parametersMapper = parametersMapper, + onBeforeNavigate = onBeforeNavigate, + childSelector = childSelector, + ) + +private class PagesWebNavigation( + private val navigator: PagesNavigator, + pages: Value>, + serializer: KSerializer, + private val pathMapper: (ChildPages) -> String?, + private val parametersMapper: (ChildPages) -> Map?, + private val onBeforeNavigate: () -> Boolean, + private val childSelector: (Child.Created) -> WebNavigationOwner?, +) : WebNavigation> { + + override val serializer: KSerializer> = Pages.serializer(serializer) + + override val history: Value>>> = + pages.map { pages -> + listOf( + WebNavigation.HistoryItem( + path = pathMapper(pages) ?: "", + parameters = parametersMapper(pages) ?: emptyMap(), + key = Pages( + items = pages.items.map { it.configuration }, + selectedIndex = pages.selectedIndex, + ), + child = (pages.items.getOrNull(pages.selectedIndex) as? Child.Created)?.let(childSelector), + ) + ) + } + + override fun onBeforeNavigate(): Boolean = + onBeforeNavigate.invoke() + + override fun navigate(history: List>) { + navigator.navigate { history.first() } + } +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/PanelsWebNavigation.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/PanelsWebNavigation.kt new file mode 100644 index 000000000..0f54630ab --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/PanelsWebNavigation.kt @@ -0,0 +1,116 @@ +package com.arkivanov.decompose.router.panels + +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.router.pages.PagesNavigator +import com.arkivanov.decompose.router.webhistory.WebNavigation +import com.arkivanov.decompose.router.webhistory.WebNavigation.HistoryItem +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner +import com.arkivanov.decompose.value.Value +import com.arkivanov.decompose.value.operator.map +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable + +/** + * Creates and returns an implementation of [WebNavigation] attached + * to the provided [navigator] and [panels]. The navigation observes panel + * changes and manipulates the browser history to match the state. It also + * observes the browser history changes and navigates the panels as needed. + * + * The navigation replaces the current entry of the browser history when the + * [ChildPanels] state changes. Multiple entries can still be pushed/popped + * to/from the browser history at the same time if required to follow the nested + * navigation. + * + * @param navigator a [PagesNavigator] that should be used for navigation when + * the user presses the browser back and forward buttons. + * @param panels an observable [ChildPanels] that should be bound to the browser + * navigation history. + * @param mainSerializer a [KSerializer] of the `main` configuration [MC]. + * @param detailsSerializer a [KSerializer] of the `details` configuration [DC]. + * @param extraSerializer a [KSerializer] of the `extra` configuration [EC]. + * @param pathMapper an optional function that returns a part of the URL path for + * the provided [ChildPanels], or `null` (or empty string) if the path shouldn't be + * affected. The resulting URL path is constructed by concatenating all URL parts + * from every parent and child navigation, top to bottom. Default return value is `null`. + * @param parametersMapper an optional function that returns a [Map] containing all + * required URL parameters for the provided [ChildPanels], or `null` (or empty Map) + * if parameters are not required The resulting URL parameters string is constructed + * by merging all URL parameters from every parent and child navigation. Default + * return value is `null`. + * @param onBeforeNavigate an optional callback that can be used to allow (by + * returning `true`) or deny (by returning `false`) the browser-initiated navigation. + * Can be used e.g. to display an alert dialog before closing a screen. Returns `true` + * by default. + * @param childSelector an optional function that selects and returns a child + * [WebNavigationOwner] for the provided [ChildPanels]. + */ +@ExperimentalDecomposeApi +fun childPanelsWebNavigation( + navigator: PanelsNavigator, + panels: Value>, + mainSerializer: KSerializer, + detailsSerializer: KSerializer, + extraSerializer: KSerializer, + pathMapper: (ChildPanels) -> String? = { null }, + parametersMapper: (ChildPanels) -> Map? = { null }, + onBeforeNavigate: () -> Boolean = { true }, + childSelector: (ChildPanels) -> WebNavigationOwner? = { null }, +): WebNavigation<*> = + PanelsWebNavigation( + navigator = navigator, + panels = panels, + mainSerializer = mainSerializer, + detailsSerializer = detailsSerializer, + extraSerializer = extraSerializer, + pathMapper = pathMapper, + parametersMapper = parametersMapper, + onBeforeNavigate = onBeforeNavigate, + childSelector = childSelector, + ) + +private class PanelsWebNavigation( + private val navigator: PanelsNavigator, + panels: Value>, + mainSerializer: KSerializer, + detailsSerializer: KSerializer, + extraSerializer: KSerializer, + private val pathMapper: (ChildPanels) -> String?, + private val parametersMapper: (ChildPanels) -> Map?, + private val onBeforeNavigate: () -> Boolean, + private val childSelector: (ChildPanels) -> WebNavigationOwner?, +) : WebNavigation> { + + override val serializer: KSerializer> = + HistoryItemKey.serializer(mainSerializer, detailsSerializer, extraSerializer) + + override val history: Value>>> = + panels.map { panels -> + listOf( + HistoryItem( + path = pathMapper(panels) ?: "", + parameters = parametersMapper(panels) ?: emptyMap(), + key = HistoryItemKey( + main = panels.main.configuration, + extra = panels.extra?.configuration, + details = panels.details?.configuration, + ), + child = childSelector(panels), + ), + ) + } + + override fun onBeforeNavigate(): Boolean = + onBeforeNavigate.invoke() + + override fun navigate(history: List>) { + val data = history.single() + navigator.navigate(main = data.main, details = data.details, extra = data.extra) + } + + @Serializable + data class HistoryItemKey( + val main: MC, + val details: DC?, + val extra: EC?, + ) +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt index b965512ab..e334160de 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt @@ -30,7 +30,7 @@ import kotlinx.serialization.builtins.ListSerializer * @param childFactory a factory function that creates new child instances. * @return an observable [Value] of [ChildStack]. */ -fun , C : Any, T : Any> Ctx.childStack( +fun , C : Any, T : Any> Ctx.childStack( source: NavigationSource>, serializer: KSerializer?, initialStack: () -> List, diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackWebNavigation.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackWebNavigation.kt new file mode 100644 index 000000000..2314465a6 --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackWebNavigation.kt @@ -0,0 +1,109 @@ +package com.arkivanov.decompose.router.stack + +import com.arkivanov.decompose.Child +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.router.webhistory.WebNavigation +import com.arkivanov.decompose.router.webhistory.WebNavigation.HistoryItem +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner +import com.arkivanov.decompose.value.Value +import com.arkivanov.decompose.value.operator.map +import kotlinx.serialization.KSerializer + +/** + * Creates and returns an implementation of [WebNavigation] attached + * to the provided [navigator] and [stack]. The navigation observes stack + * changes and manipulates the browser history to match the state. It also + * observes the browser history changes and navigates the stack as needed. + * + * When [enableHistory] parameter is `true`, the browser history follows + * the entire stack, taking the back stack into account. E.g. the navigation + * pushes an entry to the browser history when a child is pushed to the stack, + * pops an entry from the browser history when a child is popped from the stack, + * replaces the current entry of the browser history when the active child of + * the stack is changed. Multiple entries can be pushed/popped to/from the + * browser history at the same time if required to follow the stack. + * + * When [enableHistory] parameter is `false`, the browser history only takes + * into account the currently active child of the stack. Multiple entries can + * still be pushed/popped to/from the browser history at the same time if + * required to follow the nested navigation. This mode can be useful for bottom + * tab navigation. + * + * @param navigator a [StackNavigator] that should be used for navigation when + * the user presses the browser back and forward buttons. + * @param stack an observable [ChildStack] that should be bound to the browser + * navigation history. + * @param serializer a [KSerializer] of configurations [C]. + * @param enableHistory a flag that enabled or disables the back stack history. + * When `true`, the browser navigation history follows the entire stack. + * When `false`, the browser navigation history follows only the currently + * active child (i.e. the back stack will not be taken into account), can be useful + * for bottom tab navigation. Default value is `true`. + * @param pathMapper an optional function that returns a part of the URL path for + * the provided child, or `null` (or empty string) if the path shouldn't be affected. + * The resulting URL path is constructed by concatenating all URL parts from every + * parent and child navigation, top to bottom. Default return value is `null`. + * @param parametersMapper an optional function that returns a [Map] containing all + * required URL parameters for the provided child, or `null` (or empty Map) if + * parameters are not required. The resulting URL parameters string is constructed by + * merging all URL parameters from every parent and child navigation. Default return + * value is `null`. + * @param onBeforeNavigate an optional callback that can be used to allow (by + * returning `true`) or deny (by returning `false`) the browser-initiated navigation. + * Can be used e.g. to display an alert dialog before closing a screen. Returns `true` + * by default. + * @param childSelector an optional function that selects and returns a child + * [WebNavigationOwner] for the provided child. + */ +@ExperimentalDecomposeApi +fun childStackWebNavigation( + navigator: StackNavigator, + stack: Value>, + serializer: KSerializer, + enableHistory: Boolean = true, + pathMapper: (Child.Created) -> String? = { null }, + parametersMapper: (Child.Created) -> Map? = { null }, + onBeforeNavigate: () -> Boolean = { true }, + childSelector: (Child.Created) -> WebNavigationOwner? = { null }, +): WebNavigation<*> = + StackWebNavigation( + navigator = navigator, + stack = stack, + serializer = serializer, + enableHistory = enableHistory, + pathMapper = pathMapper, + parametersMapper = parametersMapper, + onBeforeNavigate = onBeforeNavigate, + childSelector = childSelector, + ) + +private class StackWebNavigation( + private val navigator: StackNavigator, + stack: Value>, + override val serializer: KSerializer, + private val enableHistory: Boolean, + private val pathMapper: (Child.Created) -> String?, + private val parametersMapper: (Child.Created) -> Map?, + private val onBeforeNavigate: () -> Boolean, + private val childSelector: (Child.Created) -> WebNavigationOwner?, +) : WebNavigation { + + override val history: Value>> = stack.map { it.toHistory() } + + private fun ChildStack.toHistory(): List> = + (if (enableHistory) items else items.takeLast(1)).map { child -> + HistoryItem( + path = pathMapper(child) ?: "", + parameters = parametersMapper(child) ?: emptyMap(), + key = child.configuration, + child = childSelector(child), + ) + } + + override fun onBeforeNavigate(): Boolean = + onBeforeNavigate.invoke() + + override fun navigate(history: List) { + navigator.navigate { history } + } +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/NoOpWebNavigation.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/NoOpWebNavigation.kt new file mode 100644 index 000000000..f855e75d8 --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/NoOpWebNavigation.kt @@ -0,0 +1,22 @@ +package com.arkivanov.decompose.router.webhistory + +import com.arkivanov.decompose.router.webhistory.WebNavigation.HistoryItem +import com.arkivanov.decompose.value.MutableValue +import com.arkivanov.decompose.value.Value +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.NothingSerializer + +internal object NoOpWebNavigation : WebNavigation { + + @OptIn(ExperimentalSerializationApi::class) + override val serializer: KSerializer get() = NothingSerializer() + + override val history: Value>> = MutableValue(emptyList()) + + override fun onBeforeNavigate(): Boolean = true + + override fun navigate(history: List) { + // No-op + } +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigation.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigation.kt new file mode 100644 index 000000000..c30b4834b --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigation.kt @@ -0,0 +1,63 @@ +package com.arkivanov.decompose.router.webhistory + +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.value.Value +import kotlinx.serialization.KSerializer + +/** + * A two-way navigation controller for Web browsers that connects a navigation + * model (e.g. `Child Stack`) with the Web browser's navigation history. + */ +@ExperimentalDecomposeApi +interface WebNavigation { + + /** + * A serializer of [HistoryItem.key]. + */ + val serializer: KSerializer + + /** + * An observable list of history items, must not be empty. The last item is + * considered active, i.e. its [HistoryItem.child] navigation controller is + * also observed. The browser history is automatically updated (pushed, + * popped or replaced) when the history list changes. + */ + val history: Value>> + + /** + * A callback that can be used to allow (by returning `true`) or deny (by + * returning `false`) the browser-initiated navigation. Can be used e.g. to + * display an alert dialog before closing a screen. + */ + fun onBeforeNavigate(): Boolean + + /** + * Manipulates the underlying navigation model when the browser navigation history + * is changed, e.g. when the user presses the browser's back or forward button. + * + * @param history a list of history item keys to be applied to the underlying + * navigation model. + */ + fun navigate(history: List) + + /** + * Represents an item of the browser history. + * + * @param key a key of the item that can be serialized using + * [WebNavigation.serializer] and later restored and passed back to + * [WebNavigation.navigate] for navigation. + * @param path a part of the URL path. The resulting URL path is constructed by + * concatenating all URL parts from every parent and child navigation, top to bottom. + * Can be empty. + * @param parameters a [Map] containing all required URL parameters for the provided + * item. The resulting URL parameters string is constructed by merging all URL + * parameters from every parent and child navigation. Can be empty. + * @param child an optional child [WebNavigationOwner]. + */ + class HistoryItem( + val key: T, + val path: String, + val parameters: Map, + val child: WebNavigationOwner?, + ) +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigationOwner.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigationOwner.kt new file mode 100644 index 000000000..86ba3ba77 --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigationOwner.kt @@ -0,0 +1,21 @@ +package com.arkivanov.decompose.router.webhistory + +import com.arkivanov.decompose.ExperimentalDecomposeApi + +/** + * Represents a holder of [WebNavigation], typically implemented by + * a Decompose component. + */ +@ExperimentalDecomposeApi +interface WebNavigationOwner { + + val webNavigation: WebNavigation<*> + + /** + * A no-op interface that extends [WebNavigationOwner]. Can be useful + * for fake or preview component implementations. + */ + interface NoOp : WebNavigationOwner { + override val webNavigation: WebNavigation<*> get() = NoOpWebNavigation + } +} diff --git a/decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt b/decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt new file mode 100644 index 000000000..969e729a4 --- /dev/null +++ b/decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt @@ -0,0 +1,49 @@ +package com.arkivanov.decompose.router.webhistory + +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.JsonString +import com.arkivanov.decompose.decodeContainer +import com.arkivanov.decompose.encodeToJson +import com.arkivanov.essenty.statekeeper.StateKeeper +import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher +import kotlinx.browser.sessionStorage +import kotlinx.browser.window +import org.w3c.dom.get +import org.w3c.dom.set + +/** + * Enables Web browser history navigation for the root [WebNavigationOwner] + * returned by [block] function. + * + * @param block a function that accepts a [StateKeeper] and an optional deep + * link string and creates and returns a root [WebNavigationOwner]. + */ +@ExperimentalDecomposeApi +fun withWebHistory( + block: (StateKeeper, deepLink: String?) -> T, +): T { + val deepLink = window.location.href.takeUnless { it == sessionStorage[KEY_SAVED_URL] } + + val stateKeeper = + StateKeeperDispatcher( + savedState = sessionStorage[KEY_SAVED_STATE] + ?.takeIf { deepLink == null } + ?.let(::JsonString) + ?.decodeContainer(), + ) + + window.onbeforeunload = + { + sessionStorage[KEY_SAVED_STATE] = stateKeeper.save().encodeToJson().value + sessionStorage[KEY_SAVED_URL] = window.location.href + null + } + + val root = block(stateKeeper, deepLink) + enableWebHistory(root.webNavigation, DefaultBrowserHistory) + + return root +} + +private const val KEY_SAVED_STATE = "decompose_saved_state" +private const val KEY_SAVED_URL = "decompose_saved_url" diff --git a/decompose/src/wasmJsMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt b/decompose/src/wasmJsMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt new file mode 100644 index 000000000..969e729a4 --- /dev/null +++ b/decompose/src/wasmJsMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt @@ -0,0 +1,49 @@ +package com.arkivanov.decompose.router.webhistory + +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.JsonString +import com.arkivanov.decompose.decodeContainer +import com.arkivanov.decompose.encodeToJson +import com.arkivanov.essenty.statekeeper.StateKeeper +import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher +import kotlinx.browser.sessionStorage +import kotlinx.browser.window +import org.w3c.dom.get +import org.w3c.dom.set + +/** + * Enables Web browser history navigation for the root [WebNavigationOwner] + * returned by [block] function. + * + * @param block a function that accepts a [StateKeeper] and an optional deep + * link string and creates and returns a root [WebNavigationOwner]. + */ +@ExperimentalDecomposeApi +fun withWebHistory( + block: (StateKeeper, deepLink: String?) -> T, +): T { + val deepLink = window.location.href.takeUnless { it == sessionStorage[KEY_SAVED_URL] } + + val stateKeeper = + StateKeeperDispatcher( + savedState = sessionStorage[KEY_SAVED_STATE] + ?.takeIf { deepLink == null } + ?.let(::JsonString) + ?.decodeContainer(), + ) + + window.onbeforeunload = + { + sessionStorage[KEY_SAVED_STATE] = stateKeeper.save().encodeToJson().value + sessionStorage[KEY_SAVED_URL] = window.location.href + null + } + + val root = block(stateKeeper, deepLink) + enableWebHistory(root.webNavigation, DefaultBrowserHistory) + + return root +} + +private const val KEY_SAVED_STATE = "decompose_saved_state" +private const val KEY_SAVED_URL = "decompose_saved_url" diff --git a/decompose/src/webMain/kotlin/com/arkivanov/decompose/Utils.kt b/decompose/src/webMain/kotlin/com/arkivanov/decompose/Utils.kt index b79d0ccf4..3bbd70bb4 100644 --- a/decompose/src/webMain/kotlin/com/arkivanov/decompose/Utils.kt +++ b/decompose/src/webMain/kotlin/com/arkivanov/decompose/Utils.kt @@ -1,8 +1,25 @@ package com.arkivanov.decompose +import com.arkivanov.essenty.statekeeper.SerializableContainer import kotlinx.serialization.json.Json internal val Json = Json { allowStructuredMapKeys = true } + +internal fun SerializableContainer.encodeToJson(): JsonString = + JsonString(Json.encodeToString(SerializableContainer.serializer(), this)) + +internal fun JsonString.decodeContainer(): SerializableContainer? = + try { + Json.decodeFromString(SerializableContainer.serializer(), value) + } catch (e: Exception) { + null + } + +internal value class JsonString(val value: String) + +internal external fun encodeURIComponent(str: String): String + +internal external fun decodeURIComponent(str: String): String diff --git a/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/stack/Utils.kt b/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/stack/Utils.kt index 758a71b2e..8dfa3cfb0 100644 --- a/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/stack/Utils.kt +++ b/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/stack/Utils.kt @@ -1,5 +1,6 @@ package com.arkivanov.decompose.router.stack +import com.arkivanov.decompose.Cancellation import com.arkivanov.decompose.value.Value import kotlin.math.min @@ -32,9 +33,10 @@ internal fun List.findFirstDifferentIndex(other: List): Int { return i } -internal fun Value.subscribe(observer: (new: T, old: T) -> Unit) { +internal fun Value.subscribe(observer: (new: T, old: T) -> Unit): Cancellation { var old = value - subscribe { new -> + + return subscribe callback@{ new -> val tmp = old old = new observer(new, tmp) diff --git a/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt b/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt new file mode 100644 index 000000000..1521d97bb --- /dev/null +++ b/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigation.kt @@ -0,0 +1,363 @@ +package com.arkivanov.decompose.router.webhistory + +import com.arkivanov.decompose.Cancellation +import com.arkivanov.decompose.Json +import com.arkivanov.decompose.encodeURIComponent +import com.arkivanov.decompose.router.stack.startsWith +import com.arkivanov.decompose.router.stack.subscribe +import com.arkivanov.decompose.router.webhistory.WebNavigation.HistoryItem +import com.arkivanov.essenty.statekeeper.SerializableContainer +import com.arkivanov.essenty.statekeeper.consumeRequired +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer + +internal fun enableWebHistory(navigation: WebNavigation, browserHistory: BrowserHistory) { + if (browserHistory.state == null) { + browserHistory.replaceState(navigation.nodeHistory()) + } + + var isEnabled = true + + fun onPopState(state: String?) { + val deserializedState = state?.deserializeState() ?: return + + if (navigation.onBeforeNavigateRecursive()) { + isEnabled = false + navigation.navigate(deserializedState.nodesContainer.consumeNodes()) + isEnabled = true + } else { + val delta = navigation.history().lastIndex - deserializedState.index + if (delta != 0) { + browserHistory.setOnPopStateListener { browserHistory.setOnPopStateListener(::onPopState) } + browserHistory.go(delta) + } + } + } + + browserHistory.setOnPopStateListener(::onPopState) + + navigation.subscribe( + isEnabled = { isEnabled }, + onPush = { history -> + history.forEach(browserHistory::pushState) + }, + onPop = { count, nodes -> + val popCount = count.coerceAtMost(browserHistory.currentIndex()) + + if (popCount > 0) { + browserHistory.setOnPopStateListener { + browserHistory.setOnPopStateListener(::onPopState) + + if (popCount < count) { + browserHistory.replaceState(nodes) + } + } + + browserHistory.go(-popCount) + } else { + browserHistory.replaceState(nodes) + } + }, + onRewrite = { oldSize, newHistory -> + if (oldSize > 1) { + browserHistory.setOnPopStateListener { + browserHistory.setOnPopStateListener(::onPopState) + browserHistory.replaceState(newHistory.first()) + newHistory.drop(1).forEach(browserHistory::pushState) + } + + browserHistory.go(-oldSize + 1) + } else { + browserHistory.replaceState(newHistory.first()) + newHistory.drop(1).forEach(browserHistory::pushState) + } + }, + onUpdateUrl = browserHistory::replaceState + ) +} + +private fun WebNavigation.onBeforeNavigateRecursive(): Boolean { + val isChildAllowed = history.value.lastOrNull()?.child?.webNavigation?.onBeforeNavigateRecursive() ?: true + + return if (isChildAllowed) onBeforeNavigate() else false +} + +private fun WebNavigation.navigate(nodes: List) { + val items: List = nodes.map { it.data.consumeRequired(serializer) } + navigate(items) + + history.value.forEachIndexed { index, item -> + item.child?.webNavigation?.navigate(nodes[index].children) + } +} + +private fun BrowserHistory.replaceState(nodes: NodeHistory<*>) { + replaceState( + data = serializeState(index = currentIndex(), nodes = nodes), + url = nodes.last().url(), + ) +} + +private fun BrowserHistory.pushState(nodes: NodeHistory<*>) { + pushState( + data = serializeState(index = currentIndex() + 1, nodes = nodes), + url = nodes.last().url() + ) +} + +private fun BrowserHistory.currentIndex(): Int = + state?.deserializeState()?.index ?: 0 + +private fun Node.toSerializableNode(): SerializableNode = + SerializableNode( + data = SerializableContainer(value = key, strategy = serializer), + children = children.map { it.toSerializableNode() }, + ) + +private fun HistoryState.serialize(): String = + Json.encodeToString(serializer = HistoryState.serializer(), value = this) + +private fun String.deserializeState(): HistoryState = + Json.decodeFromString(deserializer = HistoryState.serializer(), string = this) + +private fun SerializableContainer.consumeNodes(): List = + consumeRequired(SerializableNode.listSerializer) + +private fun serializeState( + index: Int, + nodes: NodeHistory<*>, +): String = + HistoryState(index = index, nodes = nodes.map { it.toSerializableNode() }).serialize() + +private fun WebNavigation.subscribe( + isEnabled: () -> Boolean, + onPush: (List>) -> Unit, + onPop: (count: Int, NodeHistory) -> Unit, + onRewrite: (oldSize: Int, newHistory: List>) -> Unit, + onUpdateUrl: (NodeHistory) -> Unit, +): Cancellation { + var activeChildCancellation: Cancellation? = null + var isInitial = true + + return history.subscribe { newHistory, oldHistory -> + activeChildCancellation?.cancel() + + check(newHistory.isNotEmpty()) + + if (!isInitial && isEnabled()) { + onHistoryChanged(newHistory, oldHistory, onPush, onPop, onRewrite, onUpdateUrl) + } + + isInitial = false + + val activeItem = newHistory.last() + val inactiveNodes = newHistory.dropLast(1).map(::nodeTreeOf) + + activeChildCancellation = + activeItem.child?.webNavigation?.subscribe( + isEnabled = isEnabled, + onPush = { childHistory -> + onPush(childHistory.map { childNodes -> inactiveNodes + nodeOf(item = activeItem, children = childNodes) }) + }, + onPop = { count, childNodes -> + onPop(count, inactiveNodes + nodeOf(item = activeItem, children = childNodes)) + }, + onRewrite = { oldSize, childHistory -> + onRewrite( + oldSize, + childHistory.map { childNodes -> inactiveNodes + nodeOf(item = activeItem, children = childNodes) }, + ) + }, + onUpdateUrl = { childNodes -> + onUpdateUrl(inactiveNodes + nodeOf(item = activeItem, children = childNodes)) + }, + ) + } +} + +private fun WebNavigation.onHistoryChanged( + newHistory: List>, + oldHistory: List>, + onPush: (List>) -> Unit, + onPop: (count: Int, NodeHistory) -> Unit, + onRewrite: (oldSize: Int, newHistory: List>) -> Unit, + onUpdateUrl: (NodeHistory) -> Unit, +) { + val newKeys = newHistory.map { it.key } + val oldKeys = oldHistory.map { it.key } + + when { + newKeys == oldKeys -> { // History is not changed, but path or parameters might be + onUpdateUrl(newHistory.map(::nodeTreeOf)) + } + + newKeys.startsWith(oldKeys) -> { // Items pushed + val newItems = newHistory.takeLast(newHistory.size - oldHistory.size) + val historyChange = ArrayList>() + val previousNodes = oldHistory.mapTo(ArrayList(), ::nodeTreeOf) + + newItems.forEach { item -> + val itemHistory = historyOf(item) + itemHistory.forEach { + historyChange += previousNodes + it + } + previousNodes += itemHistory.last() + } + + onPush(historyChange) + } + + oldKeys.startsWith(newKeys) -> { // Items popped + val oldPaths = oldHistory.takeLast(oldHistory.size - newHistory.size).flatMap(::historyOf) + onPop(oldPaths.size, newHistory.map(::nodeTreeOf)) + } + + else -> { // Rewriting the history + val historyChange = ArrayList>() + val previousNodes = ArrayList>() + + newHistory.forEach { item -> + val itemHistory = historyOf(item) + itemHistory.forEach { + historyChange += previousNodes + it + } + previousNodes += itemHistory.last() + } + + val oldPaths = oldHistory.flatMap(::historyOf) + + onRewrite(oldPaths.size, historyChange) + } + } +} + +private fun Node<*>.url(): String { + val path = path() + val parameters = parameters() + + return when { + path.isNotEmpty() && parameters.isNotEmpty() -> "$path?$parameters" + path.isNotEmpty() -> path + parameters.isNotEmpty() -> parameters + else -> "" + } +} + +private fun Node<*>.path(): String { + val segments = ArrayList() + collectPath(segments) + + return segments.joinToString(prefix = "/", separator = "/") +} + +private fun Node<*>.collectPath(segments: MutableList) { + if (path.isNotEmpty()) { + segments += path.trim('/') + } + + children.lastOrNull()?.collectPath(segments) +} + +private fun Node<*>.parameters(): String { + val parameters = LinkedHashMap() + collectParameters(parameters) + + return parameters.entries.joinToString(separator = "&") { (name, value) -> + "${encodeURIComponent(name)}=${encodeURIComponent(value)}" + } +} + +private fun Node<*>.collectParameters(parameters: MutableMap) { + parameters += this.parameters + children.lastOrNull()?.collectParameters(parameters) +} + +private fun WebNavigation.nodeTreeOf(item: HistoryItem): Node = + Node( + item = item, + serializer = serializer, + children = item.child?.webNavigation?.nodeHistory() ?: emptyList(), + ) + +private fun WebNavigation.nodeHistory(): NodeHistory { + val items = history.value + check(items.isNotEmpty()) + + return items.map(::nodeTreeOf) +} + +private fun WebNavigation.nodeOf( + item: HistoryItem, + children: NodeHistory<*> = emptyList(), +): Node = + Node( + item = item, + serializer = serializer, + children = children, + ) + +private fun WebNavigation.historyOf(item: HistoryItem): NodeHistory = + item.child + ?.webNavigation + ?.history() + ?.map { Node(item = item, serializer = serializer, children = it) } + ?: listOf(Node(item = item, serializer = serializer)) + +private fun WebNavigation.history(): List> { + val nodes = ArrayList>() + val historyNodes = ArrayList>() + history.value.forEach { item -> + historyOf(item).forEach { node -> + historyNodes += node + nodes += historyNodes.toList() + } + } + + return nodes +} + +@Serializable +private data class SerializableNode( + val data: SerializableContainer, + val children: List, +) { + companion object { + val listSerializer: KSerializer> by lazy { ListSerializer(serializer()) } + } +} + +private data class Node( + val path: String, + val parameters: Map, + val key: T, + val serializer: KSerializer, + val children: NodeHistory<*>, +) + +private fun Node( + item: HistoryItem, + serializer: KSerializer, + children: NodeHistory<*> = emptyList(), +): Node = + Node( + path = item.path, + parameters = item.parameters, + key = item.key, + serializer = serializer, + children = children, + ) + +private typealias NodeHistory = List> + +@Serializable +private class HistoryState( + val index: Int, + val nodesContainer: SerializableContainer, +) + +private fun HistoryState(index: Int, nodes: List): HistoryState = + HistoryState( + index = index, + nodesContainer = SerializableContainer(value = nodes, strategy = SerializableNode.listSerializer), + ) diff --git a/decompose/src/webTest/kotlin/com/arkivanov/decompose/UrlEncodeTest.kt b/decompose/src/webTest/kotlin/com/arkivanov/decompose/UrlEncodeTest.kt new file mode 100644 index 000000000..537f9bdcc --- /dev/null +++ b/decompose/src/webTest/kotlin/com/arkivanov/decompose/UrlEncodeTest.kt @@ -0,0 +1,16 @@ +package com.arkivanov.decompose + +import kotlin.test.Test +import kotlin.test.assertEquals + +class UrlEncodeTest { + + @Test + fun encode_decode() { + val original = "asd кек" + + val decoded = decodeURIComponent(encodeURIComponent(original)) + + assertEquals(original, decoded) + } +} diff --git a/decompose/src/webTest/kotlin/com/arkivanov/decompose/router/webhistory/TestWebNavigation.kt b/decompose/src/webTest/kotlin/com/arkivanov/decompose/router/webhistory/TestWebNavigation.kt new file mode 100644 index 000000000..944e14b10 --- /dev/null +++ b/decompose/src/webTest/kotlin/com/arkivanov/decompose/router/webhistory/TestWebNavigation.kt @@ -0,0 +1,89 @@ +package com.arkivanov.decompose.router.webhistory + +import com.arkivanov.decompose.router.webhistory.WebNavigation.HistoryItem +import com.arkivanov.decompose.value.MutableValue +import com.arkivanov.decompose.value.Value +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlin.test.assertContentEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class TestWebNavigation( + initialHistory: List, + private val onBeforeNavigate: () -> Boolean = { true }, + private val childFactory: (Int) -> TestWebNavigation? = { null }, +) : WebNavigation { + + override val serializer: KSerializer = Int.serializer() + + private val _history = MutableValue>>(emptyList()) + override val history: Value>> = _history + + private var children = HashMap() + + init { + navigate(initialHistory) + } + + fun requireChild(config: Int): TestWebNavigation = + requireNotNull(children[config]) + + override fun onBeforeNavigate(): Boolean = + onBeforeNavigate.invoke() + + override fun navigate(history: List) { + children.keys.removeAll { it !in history } + + history.forEach { config -> + if (config !in children) { + val child = childFactory(config) + if (child != null) { + children[config] = child + } + } + } + + _history.value = + history.map { config -> + HistoryItem( + key = config, + path = config.toString(), + parameters = emptyMap(), + child = children[config]?.let(TestWebNavigation::Owner), + ) + } + } + + fun assertHistory(configs: Iterable) { + assertContentEquals(configs, _history.value.map { it.key }) + } + + fun assertHistory(urls: List) { + val configToChildUrlsMap = + urls + .map { it.removePrefix("/") } + .groupBy( + keySelector = { it.substringBefore(delimiter = "/").toInt() }, + valueTransform = { it.substringAfter(delimiter = "/", missingDelimiterValue = "") }, + ) + .mapValues { (_, childUrls) -> childUrls.filterNot(String::isEmpty) } + + assertHistory(configToChildUrlsMap.keys) + + configToChildUrlsMap.entries.forEachIndexed { index, (_, childUrls) -> + val child = _history.value[index].child?.webNavigation as TestWebNavigation? + if (childUrls.isEmpty()) { + assertNull(child) + } else { + assertNotNull(child).assertHistory(childUrls) + } + } + } + + private class Owner(override val webNavigation: TestWebNavigation) : WebNavigationOwner { + override fun toString(): String { + return webNavigation.toString() + } + } +} diff --git a/decompose/src/webTest/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigationTest.kt b/decompose/src/webTest/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigationTest.kt new file mode 100644 index 000000000..4914e8934 --- /dev/null +++ b/decompose/src/webTest/kotlin/com/arkivanov/decompose/router/webhistory/WebHistoryNavigationTest.kt @@ -0,0 +1,768 @@ +package com.arkivanov.decompose.router.webhistory + +import kotlin.test.Test +import kotlin.test.assertFailsWith + +@Suppress("TestFunctionName") +class WebHistoryNavigationTest { + + private val history = TestBrowserHistory() + + @Test + fun WHEN_created_with_no_items_in_root_THEN_ISE_thrown() { + val nav = TestWebNavigation(initialHistory = emptyList()) + + assertFailsWith { + enableWebHistory(nav, history) + } + } + + @Test + fun WHEN_created_with_no_items_in_child_THEN_ISE_thrown() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = emptyList()) + else -> null + } + }, + ) + + assertFailsWith { + enableWebHistory(nav, history) + } + } + + @Test + fun WHEN_removed_all_items_from_child_THEN_ISE_thrown() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + + assertFailsWith { + nav.requireChild(config = 1).navigate(emptyList()) + } + } + + @Test + fun WHEN_created_with_one_item_THEN_one_item_in_browser_history() { + val nav = TestWebNavigation(initialHistory = listOf(1)) + enableWebHistory(nav, history) + + assertHistory(nav = nav, urls = listOf("/1")) + } + + @Test + fun WHEN_created_with_two_items_THEN_one_item_in_browser_history() { + val nav = TestWebNavigation(initialHistory = listOf(1, 2)) + enableWebHistory(nav, history) + + history.assertStack(urls = listOf("/2")) + nav.assertHistory(listOf(1, 2)) + } + + @Test + fun WHEN_pushed_one_item_THEN_two_items_in_browser_history() { + val nav = TestWebNavigation(initialHistory = listOf(1)) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + + assertHistory(nav = nav, urls = listOf("/1", "/2")) + } + + @Test + fun WHEN_pushed_one_item_and_popped_one_item_THEN_two_items_in_browser_history_and_first_item_active() { + val nav = TestWebNavigation(initialHistory = listOf(1)) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + nav.navigate(listOf(1)) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1", "/2"), index = 0) + } + + @Test + fun WHEN_pushed_one_item_and_popped_one_item_and_pushed_another_item_THEN_two_items_in_browser_history() { + val nav = TestWebNavigation(initialHistory = listOf(1)) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + nav.navigate(listOf(1)) + history.runPendingOperations() + nav.navigate(listOf(1, 3)) + + assertHistory(nav = nav, urls = listOf("/1", "/3")) + } + + @Test + fun WHEN_pushed_two_items_and_popped_two_items_THEN_three_items_in_browser_history_and_first_item_active() { + val nav = TestWebNavigation(initialHistory = listOf(1)) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2, 3)) + nav.navigate(listOf(1)) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1", "/2", "/3"), index = 0) + } + + @Test + fun WHEN_pushed_two_items_and_popped_two_items_and_pushed_another_item_THEN_two_items_in_browser_history() { + val nav = TestWebNavigation(initialHistory = listOf(1)) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2, 3)) + nav.navigate(listOf(1)) + history.runPendingOperations() + nav.navigate(listOf(1, 4)) + + assertHistory(nav = nav, urls = listOf("/1", "/4")) + } + + @Test + fun WHEN_pushed_one_item_and_history_go_back_one_item_THEN_two_items_in_browser_history_and_first_item_active() { + val nav = TestWebNavigation(initialHistory = listOf(1)) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + history.navigate(delta = -1) + + assertHistory(nav = nav, urls = listOf("/1", "/2"), index = 0) + } + + @Test + fun WHEN_pushed_one_item_and_history_go_back_one_item_and_history_go_forward_one_item_THEN_two_items_in_browser_history() { + val nav = TestWebNavigation(initialHistory = listOf(1)) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + history.navigate(delta = -1) + history.navigate(delta = 1) + + assertHistory(nav = nav, urls = listOf("/1", "/2")) + } + + @Test + fun WHEN_pushed_one_item_and_popped_one_item_and_history_go_forward_one_item_THEN_two_items_in_browser_history() { + val nav = TestWebNavigation(initialHistory = listOf(1)) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + nav.navigate(listOf(1)) + history.runPendingOperations() + history.navigate(delta = 1) + + assertHistory(nav = nav, urls = listOf("/1", "/2")) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_THEN_two_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22")) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_child_popped_one_item_THEN_two_items_in_browser_history_and_first_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.requireChild(config = 1).navigate(listOf(11)) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22"), index = 0) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_root_pushed_one_item_THEN_two_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + + assertHistory(nav = nav, urls = listOf("/1/11", "/2")) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_root_pushed_one_item_and_root_popped_one_item_THEN_two_items_in_browser_history_and_first_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + nav.navigate(listOf(1)) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1/11", "/2"), index = 0) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_THEN_three_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2")) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_and_root_popped_item_THEN_three_items_in_browser_history_and_second_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.navigate(listOf(1)) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2"), index = 1) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_and_root_popped_one_item_and_child_popped_one_item_THEN_three_items_in_browser_history_and_first_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.navigate(listOf(1)) + history.runPendingOperations() + nav.requireChild(config = 1).navigate(listOf(11)) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2"), index = 0) + } + + @Test + fun WHEN_created_with_one_root_item_and_root_pushed_one_item_with_one_child_item_THEN_two_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 2 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + + assertHistory(nav = nav, urls = listOf("/1", "/2/11")) + } + + @Test + fun WHEN_created_with_one_root_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_THEN_three_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 2 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(11, 22)) + + assertHistory(nav = nav, urls = listOf("/1", "/2/11", "/2/22")) + } + + @Test + fun WHEN_created_with_one_root_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_child_popped_one_item_THEN_three_items_in_browser_history_and_second_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 2 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(11, 22)) + nav.requireChild(config = 2).navigate(listOf(11)) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1", "/2/11", "/2/22"), index = 1) + } + + @Test + fun WHEN_created_with_one_root_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_root_popped_one_item_THEN_three_items_in_browser_history_and_first_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 2 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(11, 22)) + nav.navigate(listOf(1)) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1", "/2/11", "/2/22"), index = 0) + } + + @Test + fun WHEN_created_with_one_root_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_root_popped_one_item_and_root_pushed_one_item_THEN_two_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 2 -> TestWebNavigation(initialHistory = listOf(11)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(11, 22)) + nav.navigate(listOf(1)) + history.runPendingOperations() + nav.navigate(listOf(1, 2)) + + assertHistory(nav = nav, urls = listOf("/1", "/2/11")) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_THEN_four_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + 2 -> TestWebNavigation(initialHistory = listOf(111)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(111, 222)) + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2/111", "/2/222")) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_root_popped_one_item_THEN_four_items_in_browser_history_and_second_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + 2 -> TestWebNavigation(initialHistory = listOf(111)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(111, 222)) + nav.navigate(listOf(1)) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2/111", "/2/222"), index = 1) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_root_popped_one_item_and_root_pushed_one_item_THEN_three_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + 2 -> TestWebNavigation(initialHistory = listOf(111)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(111, 222)) + nav.navigate(listOf(1)) + history.runPendingOperations() + nav.navigate(listOf(1, 2)) + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2/111")) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_history_go_back_one_item_THEN_four_items_in_browser_history_and_third_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + 2 -> TestWebNavigation(initialHistory = listOf(111)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(111, 222)) + history.navigate(delta = -1) + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2/111", "/2/222"), index = 2) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_history_go_back_two_items_THEN_four_items_in_browser_history_and_second_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + 2 -> TestWebNavigation(initialHistory = listOf(111)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(111, 222)) + history.navigate(delta = -2) + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2/111", "/2/222"), index = 1) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_history_go_back_three_items_THEN_four_items_in_browser_history_and_first_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + 2 -> TestWebNavigation(initialHistory = listOf(111)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(111, 222)) + history.navigate(delta = -3) + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2/111", "/2/222"), index = 0) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_history_go_back_three_items_and_history_go_forward_one_item_THEN_four_items_in_browser_history_and_second_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + 2 -> TestWebNavigation(initialHistory = listOf(111)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(111, 222)) + history.navigate(delta = -3) + history.navigate(delta = 1) + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2/111", "/2/222"), index = 1) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_history_go_back_three_items_and_history_go_forward_two_items_THEN_four_items_in_browser_history_and_third_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + 2 -> TestWebNavigation(initialHistory = listOf(111)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(111, 222)) + history.navigate(delta = -3) + history.navigate(delta = 2) + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2/111", "/2/222"), index = 2) + } + + @Test + fun WHEN_created_with_one_root_item_and_one_child_item_and_child_pushed_one_item_and_root_pushed_one_item_with_one_child_item_and_child_pushed_one_item_and_history_go_back_three_items_and_history_go_forward_three_items_THEN_four_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11)) + 2 -> TestWebNavigation(initialHistory = listOf(111)) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.navigate(listOf(1, 2)) + nav.requireChild(config = 2).navigate(listOf(111, 222)) + history.navigate(delta = -3) + + history.navigate(delta = 3) + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/2/111", "/2/222")) + } + + @Test + fun WHEN_created_one_item_and_pushed_one_item_and_history_go_back_one_item_and_onBeforeNavigate_false_THEN_two_items_in_browser_history() { + val nav = TestWebNavigation(initialHistory = listOf(1), onBeforeNavigate = { false }) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + history.navigate(delta = -1) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1", "/2")) + } + + @Test + fun WHEN_created_one_item_and_pushed_two_items_and_history_go_back_one_item_and_onBeforeNavigate_false_THEN_three_items_in_browser_history() { + val nav = TestWebNavigation(initialHistory = listOf(1), onBeforeNavigate = { false }) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2, 3)) + history.navigate(delta = -2) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1", "/2", "/3")) + } + + @Test + fun WHEN_created_one_item_and_pushed_one_item_and_and_popped_one_item_and_history_go_forward_one_item_and_onBeforeNavigate_false_THEN_two_items_in_browser_history_and_first_item_active() { + val nav = TestWebNavigation(initialHistory = listOf(1), onBeforeNavigate = { false }) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2)) + nav.navigate(listOf(1)) + history.runPendingOperations() + history.navigate(delta = 1) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1", "/2"), index = 0) + } + + @Test + fun WHEN_created_one_item_and_pushed_two_items_and_and_popped_two_items_and_history_go_forward_two_items_and_onBeforeNavigate_false_THEN_three_items_in_browser_history_and_first_item_active() { + val nav = TestWebNavigation(initialHistory = listOf(1), onBeforeNavigate = { false }) + enableWebHistory(nav, history) + nav.navigate(listOf(1, 2, 3)) + nav.navigate(listOf(1)) + history.runPendingOperations() + history.navigate(delta = 2) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1", "/2", "/3"), index = 0) + } + + @Test + fun WHEN_created_one_root_item_and_one_child_item_and_child_pushed_one_item_and_history_go_back_one_item_and_child_onBeforeNavigate_false_THEN_two_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11), onBeforeNavigate = { false }) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + history.navigate(delta = -1) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22")) + } + + @Test + fun WHEN_created_one_root_item_and_one_child_item_and_child_pushed_two_items_and_history_go_back_two_items_and_child_onBeforeNavigate_false_THEN_three_items_in_browser_history() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11), onBeforeNavigate = { false }) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22, 33)) + history.navigate(delta = -2) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/1/33")) + } + + @Test + fun WHEN_created_one_root_item_and_one_child_item_and_child_pushed_one_item_and_child_popped_one_item_and_history_go_forward_one_item_and_child_onBeforeNavigate_false_THEN_two_items_in_browser_history_and_first_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11), onBeforeNavigate = { false }) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22)) + nav.requireChild(config = 1).navigate(listOf(11)) + history.runPendingOperations() + history.navigate(delta = 1) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22"), index = 0) + } + + @Test + fun WHEN_created_one_root_item_and_one_child_item_and_child_pushed_two_items_and_child_popped_two_items_and_history_go_forward_two_items_and_child_onBeforeNavigate_false_THEN_three_items_in_browser_history_and_first_item_active() { + val nav = + TestWebNavigation( + initialHistory = listOf(1), + childFactory = { config -> + when (config) { + 1 -> TestWebNavigation(initialHistory = listOf(11), onBeforeNavigate = { false }) + else -> null + } + }, + ) + + enableWebHistory(nav, history) + nav.requireChild(config = 1).navigate(listOf(11, 22, 33)) + nav.requireChild(config = 1).navigate(listOf(11)) + history.runPendingOperations() + history.navigate(delta = 2) + history.runPendingOperations() + + assertHistory(nav = nav, urls = listOf("/1/11", "/1/22", "/1/33"), index = 0) + } + + private fun assertHistory(nav: TestWebNavigation, urls: List, index: Int = urls.lastIndex) { + history.assertStack(urls = urls, index = index) + nav.assertHistory(urls = urls.slice(0..index)) + } +} diff --git a/sample/app-ios-compose/app-ios-compose.xcodeproj/project.xcworkspace/xcuserdata/arkivanov.xcuserdatad/UserInterfaceState.xcuserstate b/sample/app-ios-compose/app-ios-compose.xcodeproj/project.xcworkspace/xcuserdata/arkivanov.xcuserdatad/UserInterfaceState.xcuserstate index 6f0bd201ff5a079f740c7eaac8069822cd192480..c46f8191b73b8872645015ea4b931463dc5b56e2 100644 GIT binary patch delta 21168 zcmaic2V4}#`~J>sxh*J7L{L$x(vcz{AiaswrAp_dNN)myw}W)Vb{b1;V2MGai6t=_ zdyPh8iM^ZHqQ=-wH1R)sfF;TA|JBd#aXYiK`#kUSzVDRXdvFF`KM!Z6fmO59O%033l67zx~g2N(@Jfj3KoME;B(Lg)`AUS z6W9#4fN#MLuoLV8`+#CUH~CS+4X3~wSPNTVs{*#c>98Fxgp1&(a4}p0SHP9sZo>{ z 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 Bool { diff --git a/sample/app-ios/app-ios/app_iosApp.swift b/sample/app-ios/app-ios/app_iosApp.swift index ca882e164..3b505487e 100644 --- a/sample/app-ios/app-ios/app_iosApp.swift +++ b/sample/app-ios/app-ios/app_iosApp.swift @@ -31,8 +31,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { backHandler: nil ), featureInstaller: DefaultFeatureInstaller.shared, - deepLink: DefaultRootComponentDeepLinkNone.shared, - webHistoryController: nil + deepLinkUrl: nil ) func application(_ application: UIApplication, shouldSaveSecureApplicationState coder: NSCoder) -> Bool { diff --git a/sample/app-js-compose/src/jsMain/kotlin/com/arkivanov/decompose/sample/app/Main.kt b/sample/app-js-compose/src/jsMain/kotlin/com/arkivanov/decompose/sample/app/Main.kt index 50168a24c..7758ef872 100644 --- a/sample/app-js-compose/src/jsMain/kotlin/com/arkivanov/decompose/sample/app/Main.kt +++ b/sample/app-js-compose/src/jsMain/kotlin/com/arkivanov/decompose/sample/app/Main.kt @@ -6,30 +6,31 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.window.CanvasBasedWindow import com.arkivanov.decompose.DefaultComponentContext import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.router.stack.webhistory.DefaultWebHistoryController +import com.arkivanov.decompose.router.webhistory.withWebHistory import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.arkivanov.essenty.lifecycle.resume import com.arkivanov.essenty.lifecycle.stop +import com.arkivanov.sample.shared.Url import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DefaultFeatureInstaller import com.arkivanov.sample.shared.root.DefaultRootComponent import com.arkivanov.sample.shared.root.RootContent -import kotlinx.browser.window import org.jetbrains.skiko.wasm.onWasmReady import web.dom.DocumentVisibilityState import web.dom.document import web.events.EventType -@OptIn(ExperimentalDecomposeApi::class, ExperimentalComposeUiApi::class) +@OptIn(ExperimentalComposeUiApi::class, ExperimentalDecomposeApi::class) fun main() { val lifecycle = LifecycleRegistry() val root = - DefaultRootComponent( - componentContext = DefaultComponentContext(lifecycle = lifecycle), - featureInstaller = DefaultFeatureInstaller, - deepLink = DefaultRootComponent.DeepLink.Web(path = window.location.pathname), - webHistoryController = DefaultWebHistoryController(), - ) + withWebHistory { stateKeeper, deepLink -> + DefaultRootComponent( + componentContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = stateKeeper), + featureInstaller = DefaultFeatureInstaller, + deepLinkUrl = deepLink?.let(::Url), + ) + } lifecycle.attachToDocument() @@ -53,4 +54,3 @@ private fun LifecycleRegistry.attachToDocument() { document.addEventListener(type = EventType("visibilitychange"), callback = { onVisibilityChanged() }) } - diff --git a/sample/app-js-compose/src/jsMain/resources/index.html b/sample/app-js-compose/src/jsMain/resources/index.html index 9de8a8680..85ee5afe9 100644 --- a/sample/app-js-compose/src/jsMain/resources/index.html +++ b/sample/app-js-compose/src/jsMain/resources/index.html @@ -2,6 +2,7 @@ + Title diff --git a/sample/app-js/src/main/kotlin/com/arkivanov/sample/app/Main.kt b/sample/app-js/src/main/kotlin/com/arkivanov/sample/app/Main.kt index 9e4586417..5e5f4a417 100644 --- a/sample/app-js/src/main/kotlin/com/arkivanov/sample/app/Main.kt +++ b/sample/app-js/src/main/kotlin/com/arkivanov/sample/app/Main.kt @@ -2,14 +2,14 @@ package com.arkivanov.sample.app import com.arkivanov.decompose.DefaultComponentContext import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.router.stack.webhistory.DefaultWebHistoryController +import com.arkivanov.decompose.router.webhistory.withWebHistory import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.arkivanov.essenty.lifecycle.resume import com.arkivanov.essenty.lifecycle.stop +import com.arkivanov.sample.shared.Url import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DefaultFeatureInstaller import com.arkivanov.sample.shared.root.DefaultRootComponent import com.arkivanov.sample.shared.root.RootContent -import kotlinx.browser.window import react.create import react.dom.client.createRoot import web.dom.DocumentVisibilityState @@ -21,12 +21,15 @@ fun main() { val lifecycle = LifecycleRegistry() val root = - DefaultRootComponent( - componentContext = DefaultComponentContext(lifecycle = lifecycle), - featureInstaller = DefaultFeatureInstaller, - deepLink = DefaultRootComponent.DeepLink.Web(path = window.location.pathname), - webHistoryController = DefaultWebHistoryController(), - ) + withWebHistory { stateKeeper, deepLink -> + DefaultRootComponent( + componentContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = stateKeeper), + featureInstaller = DefaultFeatureInstaller, + deepLinkUrl = deepLink?.let(::Url), + ) + } + + console.log(root.stack.value) lifecycle.attachToDocument() diff --git a/sample/app-js/src/main/resources/index.html b/sample/app-js/src/main/resources/index.html index 22dc914c1..fa7bee4af 100644 --- a/sample/app-js/src/main/resources/index.html +++ b/sample/app-js/src/main/resources/index.html @@ -2,6 +2,7 @@ + Decompose Counter sample diff --git a/sample/shared/compose/build.gradle.kts b/sample/shared/compose/build.gradle.kts index c6e2b930b..3cf4295e2 100644 --- a/sample/shared/compose/build.gradle.kts +++ b/sample/shared/compose/build.gradle.kts @@ -49,9 +49,13 @@ kotlin { } setupSourceSets { + val nonWeb by bundle() val jvm by bundle() val ios by bundle() + val js by bundle() + nonWeb dependsOn common + (allSet - js) dependsOn nonWeb ios dependsOn common iosSet dependsOn ios diff --git a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/CardsContent.kt b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/CardsContent.kt index 92a9500c6..4d4970d41 100644 --- a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/CardsContent.kt +++ b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/CardsContent.kt @@ -51,10 +51,13 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.sample.shared.cards.card.CardComponent import com.arkivanov.sample.shared.cards.card.CardContent import com.arkivanov.sample.shared.utils.TopAppBar +import com.arkivanov.sample.shared.utils.WebDocumentTitle import com.arkivanov.sample.shared.utils.toPx @Composable internal fun CardsContent(component: CardsComponent, modifier: Modifier = Modifier) { + WebDocumentTitle(title = "Cards") + val stack by component.stack.subscribeAsState() Column(modifier = modifier) { diff --git a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersContent.kt b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersContent.kt index a6d23dc26..dcdd43fb9 100644 --- a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersContent.kt +++ b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersContent.kt @@ -19,10 +19,13 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.scale import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.sample.shared.counters.counter.CounterContent import com.arkivanov.sample.shared.utils.TopAppBar +import com.arkivanov.sample.shared.utils.WebDocumentTitle @OptIn(ExperimentalDecomposeApi::class) @Composable internal fun CountersContent(component: CountersComponent, modifier: Modifier = Modifier) { + WebDocumentTitle(title = "Counters") + Column(modifier = modifier) { TopAppBar(title = "Counters") diff --git a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/menu/MenuContent.kt b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/menu/MenuContent.kt index 90c7a4c5e..65c389baf 100644 --- a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/menu/MenuContent.kt +++ b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/menu/MenuContent.kt @@ -17,12 +17,15 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.arkivanov.sample.shared.utils.TopAppBar +import com.arkivanov.sample.shared.utils.WebDocumentTitle @Composable internal fun MenuContent( component: MenuComponent, modifier: Modifier = Modifier, ) { + WebDocumentTitle(title = "Menu") + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { TopAppBar(title = "Decompose Sample") diff --git a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/MultiPaneContent.kt b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/MultiPaneContent.kt index 4867aa2d8..2d5a490f4 100644 --- a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/MultiPaneContent.kt +++ b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/MultiPaneContent.kt @@ -29,10 +29,13 @@ import com.arkivanov.sample.shared.multipane.author.ArticleAuthorContent import com.arkivanov.sample.shared.multipane.details.ArticleDetailsContent import com.arkivanov.sample.shared.multipane.list.ArticleListContent import com.arkivanov.sample.shared.utils.TopAppBar +import com.arkivanov.sample.shared.utils.WebDocumentTitle @OptIn(ExperimentalDecomposeApi::class) @Composable internal fun MultiPaneContent(component: MultiPaneComponent, modifier: Modifier = Modifier) { + WebDocumentTitle(title = "Multi-Pane Layout") + val panels by component.panels.subscribeAsState() Column(modifier = modifier) { diff --git a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/gallery/GalleryContent.kt b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/gallery/GalleryContent.kt index b491cf1d3..16e5657de 100644 --- a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/gallery/GalleryContent.kt +++ b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/gallery/GalleryContent.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import com.arkivanov.sample.shared.painterResource import com.arkivanov.sample.shared.utils.TopAppBar +import com.arkivanov.sample.shared.utils.WebDocumentTitle @OptIn(ExperimentalSharedTransitionApi::class) @Composable @@ -29,6 +30,8 @@ internal fun SharedTransitionScope.GalleryContent( animatedVisibilityScope: AnimatedVisibilityScope, modifier: Modifier = Modifier, ) { + WebDocumentTitle(title = "Shared Transitions Gallery") + Column(modifier = modifier) { TopAppBar(title = "Photo Gallery", onCloseClick = component::onCloseClicked) diff --git a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/PhotoContent.kt b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/PhotoContent.kt index be8b7c18f..edcb778a6 100644 --- a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/PhotoContent.kt +++ b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/PhotoContent.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import com.arkivanov.sample.shared.painterResource import com.arkivanov.sample.shared.utils.TopAppBar +import com.arkivanov.sample.shared.utils.WebDocumentTitle @OptIn(ExperimentalSharedTransitionApi::class) @Composable @@ -25,6 +26,8 @@ internal fun SharedTransitionScope.PhotoContent( animatedVisibilityScope: AnimatedVisibilityScope, modifier: Modifier = Modifier, ) { + WebDocumentTitle(title = "Shared Transitions ${component.image.resourceId.name}") + Column(modifier = modifier) { TopAppBar(title = "Photo ${component.image.id}", onCloseClick = component::onCloseClicked) diff --git a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/utils/Utils.kt b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/utils/Utils.kt index c0812cb34..b64354e57 100644 --- a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/utils/Utils.kt +++ b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/utils/Utils.kt @@ -7,3 +7,6 @@ import androidx.compose.ui.unit.Dp @Composable internal fun Dp.toPx(): Float = with(LocalDensity.current) { toPx() } + +@Composable +internal expect fun WebDocumentTitle(title: String) diff --git a/sample/shared/compose/src/jsMain/kotlin/com/arkivanov/sample/shared/utils/Utils.js.kt b/sample/shared/compose/src/jsMain/kotlin/com/arkivanov/sample/shared/utils/Utils.js.kt new file mode 100644 index 000000000..325433343 --- /dev/null +++ b/sample/shared/compose/src/jsMain/kotlin/com/arkivanov/sample/shared/utils/Utils.js.kt @@ -0,0 +1,13 @@ +package com.arkivanov.sample.shared.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import kotlinx.browser.document + +@Composable +internal actual fun WebDocumentTitle(title: String) { + DisposableEffect(Unit) { + document.title = title + onDispose {} + } +} diff --git a/sample/shared/compose/src/nonWebMain/kotlin/com/arkivanov/sample/shared/utils/Utils.nonWeb.kt b/sample/shared/compose/src/nonWebMain/kotlin/com/arkivanov/sample/shared/utils/Utils.nonWeb.kt new file mode 100644 index 000000000..670d3bed7 --- /dev/null +++ b/sample/shared/compose/src/nonWebMain/kotlin/com/arkivanov/sample/shared/utils/Utils.nonWeb.kt @@ -0,0 +1,8 @@ +package com.arkivanov.sample.shared.utils + +import androidx.compose.runtime.Composable + +@Composable +internal actual fun WebDocumentTitle(title: String) { + // no-op +} diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/Url.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/Url.kt new file mode 100644 index 000000000..6a061db0a --- /dev/null +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/Url.kt @@ -0,0 +1,36 @@ +package com.arkivanov.sample.shared + +import kotlinx.serialization.Serializable + +@Serializable +data class Url( + val pathSegments: List, + val parameters: Map, +) + +fun Url(url: String): Url { + var path: String = url.substringAfter(delimiter = "://").substringAfter(delimiter = "/") + var parameters: Map = emptyMap() + + if ('?' in path) { + parameters = + path.substringAfter(delimiter = "?") + .split("&") + .map { it.split("=") } + .associate { (key, value) -> key to value } + + path = path.substringBefore(delimiter = "?") + } + + return Url(pathSegments = path.split("/"), parameters = parameters) +} + +fun Url.consumePathSegment(): Pair = + pathSegments.firstOrNull() to copy(pathSegments = pathSegments.drop(1)) + + +internal fun Any.path(): String = + this::class.simpleName?.snakeCase() ?: "" + +internal inline fun pathSegmentOf(): String = + T::class.simpleName?.snakeCase() ?: "" diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/Utils.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/Utils.kt index 912381836..ffeaa8e15 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/Utils.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/Utils.kt @@ -16,3 +16,14 @@ private fun Value.asObservableInternal(): Observable = val cancellation = subscribe(emitter::onNext) emitter.setCancellable(cancellation::cancel) } + +internal fun String.snakeCase(): String = + buildString { + for (c in this@snakeCase) { + if (c.isUpperCase() && isNotEmpty()) { + append('_') + } + + append(c.lowercaseChar()) + } + } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DefaultMultiPaneComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DefaultMultiPaneComponent.kt index 2b18a0467..8403cafd3 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DefaultMultiPaneComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DefaultMultiPaneComponent.kt @@ -9,12 +9,15 @@ import com.arkivanov.decompose.router.panels.Panels import com.arkivanov.decompose.router.panels.PanelsNavigation import com.arkivanov.decompose.router.panels.activateExtra import com.arkivanov.decompose.router.panels.childPanels +import com.arkivanov.decompose.router.panels.childPanelsWebNavigation import com.arkivanov.decompose.router.panels.isDual import com.arkivanov.decompose.router.panels.isSingle import com.arkivanov.decompose.router.panels.navigate import com.arkivanov.decompose.router.panels.pop +import com.arkivanov.decompose.router.webhistory.WebNavigation import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.lifecycle.reaktive.disposableScope +import com.arkivanov.sample.shared.Url import com.arkivanov.sample.shared.multipane.author.ArticleAuthorComponent import com.arkivanov.sample.shared.multipane.author.DefaultArticleAuthorComponent import com.arkivanov.sample.shared.multipane.database.DefaultArticleDatabase @@ -32,6 +35,7 @@ import kotlinx.serialization.builtins.serializer @OptIn(ExperimentalDecomposeApi::class) internal class DefaultMultiPaneComponent( componentContext: ComponentContext, + deepLinkUrl: Url?, ) : MultiPaneComponent, ComponentContext by componentContext, DisposableScope by componentContext.disposableScope() { private val database = DefaultArticleDatabase() @@ -39,10 +43,10 @@ internal class DefaultMultiPaneComponent( private val _navState = BehaviorSubject?>(null) private val navState = _navState.notNull() - override val panels: Value> = + private val _panels = childPanels( source = navigation, - initialPanels = { Panels(main = Unit) }, + initialPanels = { getInitialPanels(deepLinkUrl) }, serializers = Triple(Unit.serializer(), Details.serializer(), Extra.serializer()), onStateChanged = { newState, _ -> _navState.onNext(newState) }, handleBackButton = true, @@ -51,6 +55,22 @@ internal class DefaultMultiPaneComponent( extraFactory = ::authorComponent, ) + override val panels: Value> = _panels + + override val webNavigation: WebNavigation<*> = + childPanelsWebNavigation( + navigator = navigation, + panels = _panels, + mainSerializer = Unit.serializer(), + detailsSerializer = Details.serializer(), + extraSerializer = Extra.serializer(), + parametersMapper = { panels -> + panels.details?.let { + mapOf(KEY_ARTICLE_ID to it.configuration.articleId.toString()) + } + }, + ) + private fun listComponent(componentContext: ComponentContext): ArticleListComponent = DefaultArticleListComponent( componentContext = componentContext, @@ -100,6 +120,19 @@ internal class DefaultMultiPaneComponent( navigation.pop() } + private fun getInitialPanels(deepLinkUrl: Url?): Panels { + val parameters = deepLinkUrl?.parameters ?: return Panels(main = Unit) + + return Panels( + main = Unit, + details = parameters[KEY_ARTICLE_ID]?.toLongOrNull()?.let { Details(articleId = it) }, + ) + } + + private companion object { + private const val KEY_ARTICLE_ID = "articleId" + } + @Serializable private data class Details(val articleId: Long) diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/MultiPaneComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/MultiPaneComponent.kt index 8442e41b7..6283b57f4 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/MultiPaneComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/MultiPaneComponent.kt @@ -3,6 +3,7 @@ package com.arkivanov.sample.shared.multipane import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.panels.ChildPanels import com.arkivanov.decompose.router.panels.ChildPanelsMode +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.backhandler.BackHandlerOwner import com.arkivanov.sample.shared.multipane.author.ArticleAuthorComponent @@ -10,7 +11,7 @@ import com.arkivanov.sample.shared.multipane.details.ArticleDetailsComponent import com.arkivanov.sample.shared.multipane.list.ArticleListComponent @OptIn(ExperimentalDecomposeApi::class) -interface MultiPaneComponent : BackHandlerOwner { +interface MultiPaneComponent : BackHandlerOwner, WebNavigationOwner { val panels: Value> diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/PreviewMultiPaneComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/PreviewMultiPaneComponent.kt index 049c47934..7dba960bd 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/PreviewMultiPaneComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/PreviewMultiPaneComponent.kt @@ -5,6 +5,7 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.panels.ChildPanels import com.arkivanov.decompose.router.panels.ChildPanelsMode +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner import com.arkivanov.decompose.value.MutableValue import com.arkivanov.decompose.value.Value import com.arkivanov.sample.shared.PreviewComponentContext @@ -17,9 +18,10 @@ import com.arkivanov.sample.shared.multipane.list.PreviewArticleListComponent @OptIn(ExperimentalDecomposeApi::class) class PreviewMultiPaneComponent( isMultiPane: Boolean = false, -) : MultiPaneComponent, ComponentContext by PreviewComponentContext { +) : MultiPaneComponent, + ComponentContext by PreviewComponentContext, + WebNavigationOwner.NoOp { - @OptIn(ExperimentalDecomposeApi::class) override val panels: Value> = MutableValue( ChildPanels( diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/DefaultPagesComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/DefaultPagesComponent.kt index 7396eb98f..0b9c7fa4f 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/DefaultPagesComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/DefaultPagesComponent.kt @@ -6,31 +6,45 @@ import com.arkivanov.decompose.router.pages.ChildPages import com.arkivanov.decompose.router.pages.Pages import com.arkivanov.decompose.router.pages.PagesNavigation import com.arkivanov.decompose.router.pages.childPages +import com.arkivanov.decompose.router.pages.childPagesWebNavigation import com.arkivanov.decompose.router.pages.select import com.arkivanov.decompose.router.pages.selectNext import com.arkivanov.decompose.router.pages.selectPrev +import com.arkivanov.decompose.router.webhistory.WebNavigation import com.arkivanov.decompose.value.Value import com.arkivanov.sample.shared.ImageResourceId +import com.arkivanov.sample.shared.Url +import com.arkivanov.sample.shared.consumePathSegment import com.arkivanov.sample.shared.customnavigation.DefaultKittenComponent import com.arkivanov.sample.shared.customnavigation.KittenComponent -import kotlinx.serialization.serializer -@OptIn(ExperimentalDecomposeApi::class) class DefaultPagesComponent( componentContext: ComponentContext, + deepLinkUrl: Url?, private val onFinished: () -> Unit, ) : PagesComponent, ComponentContext by componentContext { private val nav = PagesNavigation() - override val pages: Value> = + private val _pages: Value> = childPages( source = nav, - serializer = serializer(), - initialPages = { Pages(items = ImageResourceId.entries, selectedIndex = 0) }, + serializer = ImageResourceId.serializer(), + initialPages = { Pages(items = ImageResourceId.entries, selectedIndex = getInitialPageIndex(deepLinkUrl)) }, childFactory = { imageType, ctx -> DefaultKittenComponent(ctx, imageType) }, ) + override val pages: Value> = _pages + + @OptIn(ExperimentalDecomposeApi::class) + override val webNavigation: WebNavigation<*> = + childPagesWebNavigation( + navigator = nav, + pages = _pages, + serializer = ImageResourceId.serializer(), + pathMapper = { it.items.getOrNull(it.selectedIndex)?.configuration?.name }, + ) + override fun selectPage(index: Int) { nav.select(index = index) } @@ -46,4 +60,10 @@ class DefaultPagesComponent( override fun onCloseClicked() { onFinished() } + + private fun getInitialPageIndex(deepLinkUrl: Url?): Int { + val (path) = deepLinkUrl?.consumePathSegment() ?: return 0 + + return ImageResourceId.entries.indexOfFirst { it.name == path }.takeIf { it >= 0 } ?: 0 + } } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesComponent.kt index e0fdb5cca..2778fbe9f 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesComponent.kt @@ -1,13 +1,12 @@ package com.arkivanov.sample.shared.pages -import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.pages.ChildPages +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner import com.arkivanov.decompose.value.Value import com.arkivanov.sample.shared.customnavigation.KittenComponent -interface PagesComponent { +interface PagesComponent : WebNavigationOwner { - @OptIn(ExperimentalDecomposeApi::class) val pages: Value> fun selectPage(index: Int) diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt index a112bb5f6..176ba0a99 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt @@ -5,30 +5,34 @@ import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.childStackWebNavigation import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.popTo import com.arkivanov.decompose.router.stack.pushNew -import com.arkivanov.decompose.router.stack.webhistory.WebHistoryController +import com.arkivanov.decompose.router.webhistory.WebNavigation import com.arkivanov.decompose.value.Value +import com.arkivanov.sample.shared.Url +import com.arkivanov.sample.shared.consumePathSegment import com.arkivanov.sample.shared.customnavigation.DefaultCustomNavigationComponent import com.arkivanov.sample.shared.dynamicfeatures.DefaultDynamicFeaturesComponent import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.FeatureInstaller import com.arkivanov.sample.shared.pages.DefaultPagesComponent +import com.arkivanov.sample.shared.path +import com.arkivanov.sample.shared.pathSegmentOf import com.arkivanov.sample.shared.root.RootComponent.Child import com.arkivanov.sample.shared.root.RootComponent.Child.CustomNavigationChild import com.arkivanov.sample.shared.root.RootComponent.Child.DynamicFeaturesChild import com.arkivanov.sample.shared.root.RootComponent.Child.PagesChild +import com.arkivanov.sample.shared.root.RootComponent.Child.SharedTransitionsChild import com.arkivanov.sample.shared.root.RootComponent.Child.TabsChild import com.arkivanov.sample.shared.sharedtransitions.DefaultSharedTransitionsComponent import com.arkivanov.sample.shared.tabs.DefaultTabsComponent import kotlinx.serialization.Serializable -@OptIn(ExperimentalDecomposeApi::class) class DefaultRootComponent( componentContext: ComponentContext, private val featureInstaller: FeatureInstaller, - deepLink: DeepLink = DeepLink.None, - webHistoryController: WebHistoryController? = null, + deepLinkUrl: Url? = null, ) : RootComponent, ComponentContext by componentContext { private val nav = StackNavigation() @@ -37,21 +41,29 @@ class DefaultRootComponent( childStack( source = nav, serializer = Config.serializer(), - initialStack = { getInitialStack(webHistoryPaths = webHistoryController?.historyPaths, deepLink = deepLink) }, + initialStack = { getInitialStack(deepLinkUrl) }, childFactory = ::child, ) override val stack: Value> = _stack - init { - webHistoryController?.attach( + @OptIn(ExperimentalDecomposeApi::class) + override val webNavigation: WebNavigation<*> = + childStackWebNavigation( navigator = nav, - serializer = Config.serializer(), stack = _stack, - getPath = ::getPathForConfig, - getConfiguration = ::getConfigForPath, + serializer = Config.serializer(), + pathMapper = { it.configuration.path() }, + childSelector = { + when (val child = it.instance) { + is CustomNavigationChild -> null + is TabsChild -> child.component + is DynamicFeaturesChild -> null + is PagesChild -> child.component + is SharedTransitionsChild -> child.component + } + }, ) - } private fun child(config: Config, componentContext: ComponentContext): Child = when (config) { @@ -59,10 +71,11 @@ class DefaultRootComponent( TabsChild( DefaultTabsComponent( componentContext = componentContext, + deepLinkUrl = config.deepLinkUrl, onDynamicFeaturesItemSelected = { nav.pushNew(Config.DynamicFeatures) }, onCustomNavigationItemSelected = { nav.pushNew(Config.CustomNavigation) }, - onPagesItemSelected = { nav.pushNew(Config.Pages) }, - onSharedTransitionsItemSelected = { nav.pushNew(Config.SharedTransitions) }, + onPagesItemSelected = { nav.pushNew(Config.Pages()) }, + onSharedTransitionsItemSelected = { nav.pushNew(Config.SharedTransitions()) }, ) ) @@ -87,14 +100,16 @@ class DefaultRootComponent( PagesChild( DefaultPagesComponent( componentContext = componentContext, + deepLinkUrl = config.deepLinkUrl, onFinished = nav::pop, ) ) is Config.SharedTransitions -> - Child.SharedTransitionsChild( + SharedTransitionsChild( DefaultSharedTransitionsComponent( componentContext = componentContext, + deepLinkUrl = config.deepLinkUrl, onFinished = nav::pop, ) ) @@ -108,47 +123,22 @@ class DefaultRootComponent( nav.popTo(index = toIndex) } - private companion object { - private const val WEB_PATH_DYNAMIC_FEATURES = "dynamic-features" - private const val WEB_PATH_CUSTOM_NAVIGATION = "custom-navigation" - private const val WEB_PATH_PAGES = "pages" - private const val WEB_PATH_SHARED_TRANSITIONS = "shared-transitions" - - private fun getInitialStack(webHistoryPaths: List?, deepLink: DeepLink): List = - webHistoryPaths - ?.takeUnless(List<*>::isEmpty) - ?.map(::getConfigForPath) - ?: getInitialStack(deepLink) - - private fun getInitialStack(deepLink: DeepLink): List = - when (deepLink) { - is DeepLink.None -> listOf(Config.Tabs) - is DeepLink.Web -> listOf(Config.Tabs, getConfigForPath(deepLink.path)).distinct() - } - - private fun getPathForConfig(config: Config): String = - when (config) { - Config.Tabs -> "" - Config.DynamicFeatures -> "/$WEB_PATH_DYNAMIC_FEATURES" - Config.CustomNavigation -> "/$WEB_PATH_CUSTOM_NAVIGATION" - Config.Pages -> "/$WEB_PATH_PAGES" - Config.SharedTransitions -> "/$WEB_PATH_SHARED_TRANSITIONS" - } - - private fun getConfigForPath(path: String): Config = - when (path.removePrefix("/")) { - WEB_PATH_DYNAMIC_FEATURES -> Config.DynamicFeatures - WEB_PATH_CUSTOM_NAVIGATION -> Config.CustomNavigation - WEB_PATH_PAGES -> Config.Pages - WEB_PATH_SHARED_TRANSITIONS -> Config.SharedTransitions - else -> Config.Tabs - } + private fun getInitialStack(deepLinkUrl: Url?): List { + val (path, childUrl) = deepLinkUrl?.consumePathSegment() ?: return listOf(Config.Tabs()) + + return when (path) { + pathSegmentOf() -> listOf(Config.Tabs(), Config.DynamicFeatures) + pathSegmentOf() -> listOf(Config.Tabs(), Config.CustomNavigation) + pathSegmentOf() -> listOf(Config.Tabs(), Config.Pages(deepLinkUrl = childUrl)) + pathSegmentOf() -> listOf(Config.Tabs(), Config.SharedTransitions(deepLinkUrl = childUrl)) + else -> listOf(Config.Tabs(deepLinkUrl = childUrl)) + } } @Serializable private sealed interface Config { @Serializable - data object Tabs : Config + data class Tabs(val deepLinkUrl: Url? = null) : Config @Serializable data object DynamicFeatures : Config @@ -157,14 +147,9 @@ class DefaultRootComponent( data object CustomNavigation : Config @Serializable - data object Pages : Config + data class Pages(val deepLinkUrl: Url? = null) : Config @Serializable - data object SharedTransitions : Config - } - - sealed interface DeepLink { - data object None : DeepLink - class Web(val path: String) : DeepLink + data class SharedTransitions(val deepLinkUrl: Url? = null) : Config } } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/PreviewRootComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/PreviewRootComponent.kt index c5716f9a1..89cfe9403 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/PreviewRootComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/PreviewRootComponent.kt @@ -1,7 +1,9 @@ package com.arkivanov.sample.shared.root import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner import com.arkivanov.decompose.value.MutableValue import com.arkivanov.decompose.value.Value import com.arkivanov.sample.shared.PreviewComponentContext @@ -9,7 +11,11 @@ import com.arkivanov.sample.shared.root.RootComponent.Child import com.arkivanov.sample.shared.root.RootComponent.Child.TabsChild import com.arkivanov.sample.shared.tabs.PreviewTabsComponent -class PreviewRootComponent : RootComponent, ComponentContext by PreviewComponentContext { +@OptIn(ExperimentalDecomposeApi::class) +class PreviewRootComponent : + RootComponent, + ComponentContext by PreviewComponentContext, + WebNavigationOwner.NoOp { override val stack: Value> = MutableValue( diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt index 022a55ac8..3905385bb 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt @@ -1,6 +1,7 @@ package com.arkivanov.sample.shared.root import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.backhandler.BackHandlerOwner import com.arkivanov.sample.shared.customnavigation.CustomNavigationComponent @@ -9,7 +10,7 @@ import com.arkivanov.sample.shared.pages.PagesComponent import com.arkivanov.sample.shared.sharedtransitions.SharedTransitionsComponent import com.arkivanov.sample.shared.tabs.TabsComponent -interface RootComponent : BackHandlerOwner { +interface RootComponent : BackHandlerOwner, WebNavigationOwner { val stack: Value> @@ -17,10 +18,10 @@ interface RootComponent : BackHandlerOwner { fun onBackClicked(toIndex: Int) sealed class Child { - class TabsChild(val component: TabsComponent) : Child() + class TabsChild(val component: TabsComponent) : Child() // /tabs class DynamicFeaturesChild(val component: DynamicFeaturesComponent) : Child() class CustomNavigationChild(val component: CustomNavigationComponent) : Child() class PagesChild(val component: PagesComponent) : Child() - class SharedTransitionsChild(val component: SharedTransitionsComponent) : Child() + class SharedTransitionsChild(val component: SharedTransitionsComponent) : Child() // /transitions } } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/DefaultSharedTransitionsComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/DefaultSharedTransitionsComponent.kt index c712dfd6c..e2fd666e4 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/DefaultSharedTransitionsComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/DefaultSharedTransitionsComponent.kt @@ -1,12 +1,20 @@ package com.arkivanov.sample.shared.sharedtransitions import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.childStackWebNavigation import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.pushNew +import com.arkivanov.decompose.router.webhistory.WebNavigation import com.arkivanov.decompose.value.Value +import com.arkivanov.sample.shared.ImageResourceId +import com.arkivanov.sample.shared.Url +import com.arkivanov.sample.shared.consumePathSegment +import com.arkivanov.sample.shared.path +import com.arkivanov.sample.shared.pathSegmentOf import com.arkivanov.sample.shared.sharedtransitions.SharedTransitionsComponent.Child import com.arkivanov.sample.shared.sharedtransitions.SharedTransitionsComponent.Child.GalleryChild import com.arkivanov.sample.shared.sharedtransitions.SharedTransitionsComponent.Child.PhotoChild @@ -17,26 +25,54 @@ import kotlinx.serialization.Serializable class DefaultSharedTransitionsComponent( componentContext: ComponentContext, + deepLinkUrl: Url?, private val onFinished: () -> Unit, ) : SharedTransitionsComponent, ComponentContext by componentContext { + private val images = + List(100) { index -> + Image( + id = index, + resourceId = ImageResourceId.entries[index % ImageResourceId.entries.size], + ) + } + private val nav = StackNavigation() - override val stack: Value> = + private val _stack = childStack( source = nav, serializer = Config.serializer(), - initialConfiguration = Config.Gallery, + initialStack = { getInitialStack(deepLinkUrl) }, handleBackButton = true, childFactory = { config, _ -> child(config) }, ) + override val stack: Value> = _stack + + @OptIn(ExperimentalDecomposeApi::class) + override val webNavigation: WebNavigation<*> = + childStackWebNavigation( + navigator = nav, + stack = _stack, + serializer = Config.serializer(), + pathMapper = { it.configuration.path() }, + parametersMapper = { child -> + when (val config = child.configuration) { + is Config.Gallery -> emptyMap() + is Config.Photo -> mapOf("id" to config.id.toString()) + } + }, + onBeforeNavigate = { false }, + ) + private fun child(config: Config): Child = when (config) { is Config.Gallery -> GalleryChild( DefaultGalleryComponent( - onImageSelected = { nav.pushNew(Config.Photo(it)) }, + images = images, + onImageSelected = { nav.pushNew(Config.Photo(id = it)) }, onFinished = onFinished, ) ) @@ -44,7 +80,7 @@ class DefaultSharedTransitionsComponent( is Config.Photo -> PhotoChild( DefaultPhotoComponent( - image = config.image, + image = images.first { it.id == config.id }, onFinished = nav::pop, ) ) @@ -54,12 +90,30 @@ class DefaultSharedTransitionsComponent( nav.pop() } + private fun getInitialStack(deepLinkUrl: Url?): List { + if (deepLinkUrl == null) { + return listOf(Config.Gallery) + } + + val (path) = deepLinkUrl.consumePathSegment() + + return when (path) { + pathSegmentOf() -> + listOfNotNull( + Config.Gallery, + deepLinkUrl.parameters["id"]?.toIntOrNull()?.let { Config.Photo(id = it) }, + ) + + else -> listOf(Config.Gallery) + } + } + @Serializable private sealed interface Config { @Serializable data object Gallery : Config @Serializable - data class Photo(val image: Image) : Config + data class Photo(val id: Int) : Config } } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/SharedTransitionsComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/SharedTransitionsComponent.kt index 34e806817..46b0a1a5b 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/SharedTransitionsComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/SharedTransitionsComponent.kt @@ -1,12 +1,13 @@ package com.arkivanov.sample.shared.sharedtransitions import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.backhandler.BackHandlerOwner import com.arkivanov.sample.shared.sharedtransitions.gallery.GalleryComponent import com.arkivanov.sample.shared.sharedtransitions.photo.PhotoComponent -interface SharedTransitionsComponent : BackHandlerOwner { +interface SharedTransitionsComponent : BackHandlerOwner, WebNavigationOwner { val stack: Value> diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/gallery/DefaultGalleryComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/gallery/DefaultGalleryComponent.kt index c0f13e5bb..e1fee6bec 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/gallery/DefaultGalleryComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/gallery/DefaultGalleryComponent.kt @@ -1,23 +1,15 @@ package com.arkivanov.sample.shared.sharedtransitions.gallery -import com.arkivanov.sample.shared.ImageResourceId import com.arkivanov.sample.shared.sharedtransitions.photo.Image class DefaultGalleryComponent( - private val onImageSelected: (Image) -> Unit, + override val images: List, + private val onImageSelected: (id: Int) -> Unit, private val onFinished: () -> Unit, ) : GalleryComponent { - override val images: List = - List(100) { index -> - Image( - id = index, - resourceId = ImageResourceId.entries[index % ImageResourceId.entries.size], - ) - } - - override fun onImageClicked(index: Int) { - onImageSelected(images[index]) + override fun onImageClicked(id: Int) { + onImageSelected(id) } override fun onCloseClicked() { diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/Image.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/Image.kt index 646822184..4e4ab1e04 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/Image.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/Image.kt @@ -1,9 +1,7 @@ package com.arkivanov.sample.shared.sharedtransitions.photo import com.arkivanov.sample.shared.ImageResourceId -import kotlinx.serialization.Serializable -@Serializable data class Image( val id: Int, val resourceId: ImageResourceId, diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/DefaultTabsComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/DefaultTabsComponent.kt index 0cd2b1b14..9b92b2ca1 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/DefaultTabsComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/DefaultTabsComponent.kt @@ -1,15 +1,22 @@ package com.arkivanov.sample.shared.tabs import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.bringToFront import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.childStackWebNavigation +import com.arkivanov.decompose.router.webhistory.WebNavigation import com.arkivanov.decompose.value.Value +import com.arkivanov.sample.shared.Url import com.arkivanov.sample.shared.cards.DefaultCardsComponent +import com.arkivanov.sample.shared.consumePathSegment import com.arkivanov.sample.shared.counters.DefaultCountersComponent import com.arkivanov.sample.shared.menu.DefaultMenuComponent import com.arkivanov.sample.shared.multipane.DefaultMultiPaneComponent +import com.arkivanov.sample.shared.path +import com.arkivanov.sample.shared.pathSegmentOf import com.arkivanov.sample.shared.tabs.TabsComponent.Child import com.arkivanov.sample.shared.tabs.TabsComponent.Child.CardsChild import com.arkivanov.sample.shared.tabs.TabsComponent.Child.CountersChild @@ -19,6 +26,7 @@ import kotlinx.serialization.Serializable internal class DefaultTabsComponent( componentContext: ComponentContext, + deepLinkUrl: Url?, private val onDynamicFeaturesItemSelected: () -> Unit, private val onCustomNavigationItemSelected: () -> Unit, private val onPagesItemSelected: () -> Unit, @@ -27,14 +35,34 @@ internal class DefaultTabsComponent( private val nav = StackNavigation() - override val stack: Value> = + private val _stack: Value> = childStack( source = nav, serializer = Config.serializer(), - initialConfiguration = Config.Menu, + initialConfiguration = getInitialConfig(deepLinkUrl), childFactory = ::child, ) + override val stack: Value> = _stack + + @OptIn(ExperimentalDecomposeApi::class) + override val webNavigation: WebNavigation<*> = + childStackWebNavigation( + navigator = nav, + stack = _stack, + serializer = Config.serializer(), + enableHistory = false, + pathMapper = { it.configuration.path() }, + childSelector = { + when (val child = it.instance) { + is CardsChild -> null + is CountersChild -> null + is MenuChild -> null + is MultiPaneChild -> child.component + } + }, + ) + private fun child(config: Config, componentContext: ComponentContext): Child = when (config) { is Config.Menu -> @@ -48,7 +76,7 @@ internal class DefaultTabsComponent( ) is Config.Counters -> CountersChild(DefaultCountersComponent(componentContext)) - is Config.MultiPane -> MultiPaneChild(DefaultMultiPaneComponent(componentContext)) + is Config.MultiPane -> MultiPaneChild(DefaultMultiPaneComponent(componentContext, config.deepLinkUrl)) is Config.Cards -> CardsChild(DefaultCardsComponent(componentContext)) } @@ -65,7 +93,19 @@ internal class DefaultTabsComponent( } override fun onMultiPaneTabClicked() { - nav.bringToFront(Config.MultiPane) + nav.bringToFront(Config.MultiPane()) + } + + private fun getInitialConfig(deepLinkUrl: Url?): Config { + // TODO: path childUrl + val (path, childUrl) = deepLinkUrl?.consumePathSegment() ?: return Config.Menu + + return when (path) { + pathSegmentOf() -> Config.Counters + pathSegmentOf() -> Config.Cards + pathSegmentOf() -> Config.MultiPane(deepLinkUrl = childUrl) + else -> Config.Menu + } } @Serializable @@ -80,6 +120,6 @@ internal class DefaultTabsComponent( data object Cards : Config @Serializable - data object MultiPane : Config + data class MultiPane(val deepLinkUrl: Url? = null) : Config } } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/PreviewTabsComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/PreviewTabsComponent.kt index ad21833c7..c0adc3537 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/PreviewTabsComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/PreviewTabsComponent.kt @@ -1,12 +1,15 @@ package com.arkivanov.sample.shared.tabs +import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner import com.arkivanov.decompose.value.MutableValue import com.arkivanov.decompose.value.Value import com.arkivanov.sample.shared.counters.PreviewCountersComponent import com.arkivanov.sample.shared.tabs.TabsComponent.Child.CountersChild -class PreviewTabsComponent : TabsComponent { +@OptIn(ExperimentalDecomposeApi::class) +class PreviewTabsComponent : TabsComponent, WebNavigationOwner.NoOp { override val stack: Value> = MutableValue( diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/TabsComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/TabsComponent.kt index 845544783..d3f2e6130 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/TabsComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/TabsComponent.kt @@ -1,13 +1,14 @@ package com.arkivanov.sample.shared.tabs import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.webhistory.WebNavigationOwner import com.arkivanov.decompose.value.Value import com.arkivanov.sample.shared.cards.CardsComponent import com.arkivanov.sample.shared.counters.CountersComponent import com.arkivanov.sample.shared.menu.MenuComponent import com.arkivanov.sample.shared.multipane.MultiPaneComponent -interface TabsComponent { +interface TabsComponent : WebNavigationOwner { val stack: Value> diff --git a/sample/shared/shared/src/commonTest/kotlin/com/arkivanov/sample/shared/root/RootComponentIntegrationTest.kt b/sample/shared/shared/src/commonTest/kotlin/com/arkivanov/sample/shared/root/RootComponentIntegrationTest.kt index a1e2613a3..baa603925 100644 --- a/sample/shared/shared/src/commonTest/kotlin/com/arkivanov/sample/shared/root/RootComponentIntegrationTest.kt +++ b/sample/shared/shared/src/commonTest/kotlin/com/arkivanov/sample/shared/root/RootComponentIntegrationTest.kt @@ -1,10 +1,9 @@ package com.arkivanov.sample.shared.root -import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.sample.shared.Url import com.arkivanov.sample.shared.assertActiveInstance import com.arkivanov.sample.shared.createComponent import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.TestFeatureInstaller -import com.arkivanov.sample.shared.root.DefaultRootComponent.DeepLink import com.arkivanov.sample.shared.root.RootComponent.Child.CustomNavigationChild import com.arkivanov.sample.shared.root.RootComponent.Child.DynamicFeaturesChild import com.arkivanov.sample.shared.root.RootComponent.Child.PagesChild @@ -15,49 +14,48 @@ import kotlin.test.Test class RootComponentIntegrationTest { @Test - fun WHEN_created_with_deeplink_Web_empty_THEN_TabsChild_active() { - val component = createComponent(deepLink = DeepLink.Web(path = "")) + fun WHEN_created_with_deeplink_empty_THEN_TabsChild_active() { + val component = createComponent(deepLink = Url(url = "https://example.com")) component.stack.assertActiveInstance() } @Test - fun WHEN_created_with_deeplink_Web_unrecognized_THEN_TabsChild_active() { - val component = createComponent(deepLink = DeepLink.Web(path = "/xyz")) + fun WHEN_created_with_deeplink_unrecognized_THEN_TabsChild_active() { + val component = createComponent(deepLink = Url(url = "https://example.com/xyz")) component.stack.assertActiveInstance() } @Test - fun WHEN_created_with_deeplink_Web_DynamicFeatures_THEN_DynamicFeaturesChild_active() { - val component = createComponent(deepLink = DeepLink.Web(path = "/dynamic-features")) + fun WHEN_created_with_deeplink_DynamicFeatures_THEN_DynamicFeaturesChild_active() { + val component = createComponent(deepLink = Url(url = "https://example.com/dynamic_features")) component.stack.assertActiveInstance() } @Test - fun WHEN_created_with_deeplink_Web_CustomNavigationChild_THEN_CustomNavigationChild_active() { - val component = createComponent(deepLink = DeepLink.Web(path = "/custom-navigation")) + fun WHEN_created_with_deeplink_CustomNavigationChild_THEN_CustomNavigationChild_active() { + val component = createComponent(deepLink = Url(url = "https://example.com/custom_navigation")) component.stack.assertActiveInstance() } @Test - fun WHEN_created_with_deeplink_Web_PagesChild_THEN_PagesChild_active() { - val component = createComponent(deepLink = DeepLink.Web(path = "/pages")) + fun WHEN_created_with_deeplink_PagesChild_THEN_PagesChild_active() { + val component = createComponent(deepLink = Url(url = "https://example.com/pages")) component.stack.assertActiveInstance() } - @OptIn(ExperimentalDecomposeApi::class) private fun createComponent( - deepLink: DeepLink = DeepLink.None, + deepLink: Url? = null, ): RootComponent = createComponent { componentContext -> DefaultRootComponent( componentContext = componentContext, featureInstaller = TestFeatureInstaller(), - deepLink = deepLink, + deepLinkUrl = deepLink, ) } } diff --git a/sample/shared/shared/src/commonTest/kotlin/com/arkivanov/sample/shared/tabs/TabsComponentIntegrationTest.kt b/sample/shared/shared/src/commonTest/kotlin/com/arkivanov/sample/shared/tabs/TabsComponentIntegrationTest.kt index 31bbf4feb..44e204972 100644 --- a/sample/shared/shared/src/commonTest/kotlin/com/arkivanov/sample/shared/tabs/TabsComponentIntegrationTest.kt +++ b/sample/shared/shared/src/commonTest/kotlin/com/arkivanov/sample/shared/tabs/TabsComponentIntegrationTest.kt @@ -109,6 +109,7 @@ class TabsComponentIntegrationTest { createComponent { componentContext -> DefaultTabsComponent( componentContext = componentContext, + deepLinkUrl = null, onDynamicFeaturesItemSelected = onDynamicFeaturesItemSelected, onCustomNavigationItemSelected = onCustomNavigationItemSelected, onPagesItemSelected = onPagesItemSelected,