├── .editorconfig
├── .github
├── FUNDING.yml
└── workflows
│ ├── build.yml
│ ├── documentation.yml
│ └── publish.yml
├── .gitignore
├── .idea
└── icon.png
├── LICENSE
├── README.md
├── build.gradle.kts
├── decompose
├── .gitignore
├── api
│ ├── android
│ │ └── decompose.api
│ ├── decompose.klib.api
│ └── jvm
│ │ └── decompose.api
├── build.gradle.kts
├── consumer-rules.pro
├── dependencies
│ └── androidReleaseRuntimeClasspath.txt
├── proguard-rules.pro
└── src
│ ├── androidMain
│ ├── AndroidManifest.xml
│ ├── kotlin
│ │ └── com
│ │ │ └── arkivanov
│ │ │ └── decompose
│ │ │ ├── DeeplinkUtils.kt
│ │ │ ├── DefaultComponentContextBuilder.kt
│ │ │ ├── Lock.kt
│ │ │ ├── RetainedComponent.kt
│ │ │ ├── errorhandler
│ │ │ └── PrintError.kt
│ │ │ └── mainthread
│ │ │ └── CheckMainThread.kt
│ └── resources
│ │ └── META-INF
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ └── decompose-android
│ │ └── verification.properties
│ ├── androidUnitTest
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── AndroidTestUtils.kt
│ │ ├── DefaultComponentContextBuilderTest.kt
│ │ ├── RetainedComponentMultipleTest.kt
│ │ ├── RetainedComponentSingleTest.kt
│ │ └── TestOwner.kt
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── Annotations.kt
│ │ ├── Cancellation.kt
│ │ ├── Child.kt
│ │ ├── ComponentContext.kt
│ │ ├── ComponentContextExt.kt
│ │ ├── ComponentContextFactory.kt
│ │ ├── ComponentContextFactoryOwner.kt
│ │ ├── DecomposeExperimentFlags.kt
│ │ ├── DefaultComponentContext.kt
│ │ ├── GenericComponentContext.kt
│ │ ├── GettingList.kt
│ │ ├── Lock.kt
│ │ ├── Ref.kt
│ │ ├── Relay.kt
│ │ ├── Utils.kt
│ │ ├── backhandler
│ │ ├── ChildBackHandler.kt
│ │ └── DefaultChildBackHandler.kt
│ │ ├── errorhandler
│ │ ├── ErrorHandlers.kt
│ │ └── PrintError.kt
│ │ ├── instancekeeper
│ │ ├── ChildInstanceKeeper.kt
│ │ └── InstanceKeeperExt.kt
│ │ ├── lifecycle
│ │ └── MergedLifecycle.kt
│ │ ├── mainthread
│ │ ├── CheckMainThread.kt
│ │ └── NotOnMainThreadException.kt
│ │ ├── router
│ │ ├── children
│ │ │ ├── ChildController.kt
│ │ │ ├── ChildControllerExt.kt
│ │ │ ├── ChildItem.kt
│ │ │ ├── ChildItemFactory.kt
│ │ │ ├── ChildNavState.kt
│ │ │ ├── ChildrenFactory.kt
│ │ │ ├── ChildrenNavigator.kt
│ │ │ ├── DefaultChildItemFactory.kt
│ │ │ ├── NavState.kt
│ │ │ ├── NavStateSaver.kt
│ │ │ ├── NavigationSource.kt
│ │ │ ├── SimpleChildNavState.kt
│ │ │ ├── SimpleNavigation.kt
│ │ │ └── TransientNavStateSaver.kt
│ │ ├── items
│ │ │ ├── ChildItems.kt
│ │ │ ├── ChildItemsFactory.kt
│ │ │ ├── DefaultItemsNavigation.kt
│ │ │ ├── DefaultLazyChildItems.kt
│ │ │ ├── Items.kt
│ │ │ ├── ItemsController.kt
│ │ │ ├── ItemsNavigation.kt
│ │ │ ├── ItemsNavigator.kt
│ │ │ ├── ItemsNavigatorExt.kt
│ │ │ └── LazyChildItems.kt
│ │ ├── pages
│ │ │ ├── ChildPages.kt
│ │ │ ├── ChildPagesFactory.kt
│ │ │ ├── DefaultPagesNavigation.kt
│ │ │ ├── Pages.kt
│ │ │ ├── PagesNavigation.kt
│ │ │ ├── PagesNavigator.kt
│ │ │ ├── PagesNavigatorExt.kt
│ │ │ └── PagesWebNavigation.kt
│ │ ├── panels
│ │ │ ├── ChildPanels.kt
│ │ │ ├── ChildPanelsFactory.kt
│ │ │ ├── ChildPanelsMode.kt
│ │ │ ├── DefaultPanelsNavigation.kt
│ │ │ ├── Panels.kt
│ │ │ ├── PanelsNavigation.kt
│ │ │ ├── PanelsNavigator.kt
│ │ │ ├── PanelsNavigatorExt.kt
│ │ │ └── PanelsWebNavigation.kt
│ │ ├── slot
│ │ │ ├── ChildSlot.kt
│ │ │ ├── ChildSlotFactory.kt
│ │ │ ├── DefaultSlotNavigation.kt
│ │ │ ├── SlotNavigation.kt
│ │ │ ├── SlotNavigator.kt
│ │ │ ├── SlotNavigatorExt.kt
│ │ │ └── ValueExt.kt
│ │ ├── stack
│ │ │ ├── ChildStack.kt
│ │ │ ├── ChildStackFactory.kt
│ │ │ ├── DefaultStackNavigation.kt
│ │ │ ├── StackNavigation.kt
│ │ │ ├── StackNavigator.kt
│ │ │ ├── StackNavigatorExt.kt
│ │ │ ├── StackWebNavigation.kt
│ │ │ ├── ValueExt.kt
│ │ │ └── webhistory
│ │ │ │ └── WebHistoryController.kt
│ │ └── webhistory
│ │ │ ├── NoOpWebNavigation.kt
│ │ │ ├── WebNavigation.kt
│ │ │ └── WebNavigationOwner.kt
│ │ ├── statekeeper
│ │ └── ChildStateKeeper.kt
│ │ └── value
│ │ ├── MutableValue.kt
│ │ ├── MutableValueBuilder.kt
│ │ ├── MutableValueExt.kt
│ │ ├── Value.kt
│ │ ├── ValueExt.kt
│ │ └── operator
│ │ └── Map.kt
│ ├── commonTest
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── ChildContextWithBackHandlerPriorityTest.kt
│ │ ├── ChildContextWithLifecycleTest.kt
│ │ ├── ChildContextWithoutLifecycleTest.kt
│ │ ├── KeyHashStringTest.kt
│ │ ├── RelayTest.kt
│ │ ├── TestBackCallback.kt
│ │ ├── TestComponentContext.kt
│ │ ├── TestUtils.kt
│ │ ├── backhandler
│ │ ├── DefaultChildBackHandlerTest.kt
│ │ └── TestBackDispatcher.kt
│ │ ├── lifecycle
│ │ ├── MergedLifecycleTest.kt
│ │ └── TestLifecycleCallbacks.kt
│ │ ├── router
│ │ ├── Component.kt
│ │ ├── TestInstance.kt
│ │ ├── children
│ │ │ ├── ChildControllerTest.kt
│ │ │ ├── ChildrenBackPressedTest.kt
│ │ │ ├── ChildrenBasicTest.kt
│ │ │ ├── ChildrenLifecycleTest.kt
│ │ │ ├── ChildrenRetainedInstanceTest.kt
│ │ │ ├── ChildrenSavedStateTest.kt
│ │ │ ├── ChildrenTestBase.kt
│ │ │ └── TransientNavStateSaverTest.kt
│ │ ├── items
│ │ │ ├── LazyComponentState.kt
│ │ │ ├── LazyItemsBackButtonTest.kt
│ │ │ ├── LazyItemsLazinessTest.kt
│ │ │ ├── LazyItemsNavigationTest.kt
│ │ │ ├── LazyItemsRetainedInstanceTest.kt
│ │ │ ├── LazyItemsSavedStateTest.kt
│ │ │ └── TestLazyChildItems.kt
│ │ ├── pages
│ │ │ ├── BaseChildPagesTest.kt
│ │ │ ├── ChildPagesBackButtonTest.kt
│ │ │ ├── ChildPagesClearTest.kt
│ │ │ ├── ChildPagesIntegrationTest.kt
│ │ │ ├── ChildPagesSavedStateTest.kt
│ │ │ ├── ChildPagesSelectFirstTest.kt
│ │ │ ├── ChildPagesSelectLastTest.kt
│ │ │ ├── ChildPagesSelectNextTest.kt
│ │ │ ├── ChildPagesSelectPrevTest.kt
│ │ │ ├── ChildPagesSelectTest.kt
│ │ │ └── ChildPagesSetItemsTest.kt
│ │ ├── panels
│ │ │ ├── BaseChildPanelsTest.kt
│ │ │ ├── ChildPanelsBackButtonTest.kt
│ │ │ ├── ChildPanelsNavigationExtTest.kt
│ │ │ ├── ChildPanelsNavigationTest.kt
│ │ │ ├── ChildPanelsSavedStateTest.kt
│ │ │ └── TestPanelsNavigator.kt
│ │ ├── slot
│ │ │ └── ChildSlotIntegrationTest.kt
│ │ └── stack
│ │ │ ├── ChildStackIntegrationTest.kt
│ │ │ ├── ChildStackTest.kt
│ │ │ ├── RouterBringToFrontTest.kt
│ │ │ ├── RouterPopTest.kt
│ │ │ ├── RouterPopToFirstTest.kt
│ │ │ ├── RouterPopToTest.kt
│ │ │ ├── RouterPopWhileTest.kt
│ │ │ ├── RouterPushNewTest.kt
│ │ │ ├── RouterPushTest.kt
│ │ │ ├── RouterPushToFrontTest.kt
│ │ │ ├── RouterReplaceAllTest.kt
│ │ │ ├── RouterReplaceCurrentTest.kt
│ │ │ ├── TestStackNavigator.kt
│ │ │ └── TestStackRouter.kt
│ │ ├── statekeeper
│ │ └── TestStateKeeperDispatcher.kt
│ │ └── value
│ │ ├── MutableValueTest.kt
│ │ └── operator
│ │ └── ValueMapTest.kt
│ ├── darwinMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── Lock.kt
│ │ ├── errorhandler
│ │ └── PrintError.kt
│ │ └── mainthread
│ │ └── CheckMainThread.kt
│ ├── jsMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── Utils.kt
│ │ └── router
│ │ └── webhistory
│ │ ├── DefaultBrowserHistory.kt
│ │ └── WebHistoryNavigation.kt
│ ├── jsTest
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ └── TestUtils.kt
│ ├── jvmMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── Lock.kt
│ │ ├── errorhandler
│ │ └── PrintError.kt
│ │ └── mainthread
│ │ ├── CheckMainThread.kt
│ │ ├── MainThreadChecker.kt
│ │ └── MainThreadCheckerProvider.kt
│ ├── jvmTest
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── RelayThreadingTest.kt
│ │ └── value
│ │ ├── MutableValueThreadingTest.kt
│ │ └── operator
│ │ └── ValueMapThreadingTest.kt
│ ├── macosArm64Test
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── RelayThreadingTest.kt
│ │ └── value
│ │ ├── MutableValueThreadingTest.kt
│ │ └── operator
│ │ └── ValueMapThreadingTest.kt
│ ├── macosX64Test
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── RelayThreadingTest.kt
│ │ └── value
│ │ ├── MutableValueThreadingTest.kt
│ │ └── operator
│ │ └── ValueMapThreadingTest.kt
│ ├── nonWebMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ └── Utils.nonJs.kt
│ ├── nonWebTest
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── AbstractThreadingTest.kt
│ │ ├── RelayThreadingTest.kt
│ │ └── value
│ │ ├── AbstractMutableValueThreadingTest.kt
│ │ └── operator
│ │ └── AbstractValueMapThreadingTest.kt
│ ├── wasmJsMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── Utils.kt
│ │ └── router
│ │ └── webhistory
│ │ ├── DefaultBrowserHistory.kt
│ │ └── WebHistoryNavigation.kt
│ ├── wasmJsTest
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ └── TestUtils.kt
│ ├── webMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ ├── Lock.kt
│ │ ├── Utils.kt
│ │ ├── errorhandler
│ │ └── PrintError.kt
│ │ ├── mainthread
│ │ └── CheckMainThread.kt
│ │ └── router
│ │ ├── stack
│ │ ├── Utils.kt
│ │ └── webhistory
│ │ │ ├── DefaultWebHistoryController.kt
│ │ │ └── Utils.kt
│ │ └── webhistory
│ │ ├── BrowserHistory.kt
│ │ ├── DefaultBrowserHistory.kt
│ │ └── WebHistoryNavigation.kt
│ └── webTest
│ └── kotlin
│ └── com
│ └── arkivanov
│ └── decompose
│ ├── TestUtils.kt
│ ├── UrlEncodeTest.kt
│ └── router
│ ├── stack
│ └── webhistory
│ │ └── DefaultWebHistoryControllerTest.kt
│ └── webhistory
│ ├── TestBrowserHistory.kt
│ ├── TestWebNavigation.kt
│ └── WebHistoryNavigationTest.kt
├── deps.versions.toml
├── docs
├── community.md
├── component
│ ├── back-button.md
│ ├── child-components.md
│ ├── custom-component-context.md
│ ├── instance-retaining.md
│ ├── lifecycle.md
│ ├── overview.md
│ ├── scopes.md
│ └── state-preservation.md
├── extensions
│ ├── android.md
│ ├── compose.md
│ └── overview.md
├── faq.md
├── getting-started
│ ├── contributing.md
│ ├── installation.md
│ ├── license.md
│ └── quick-start.md
├── index.md
├── media
│ ├── BackGestureAndroid.mp4
│ ├── BackGestureIos.mp4
│ ├── BackGestureMaterial.mp4
│ ├── ComponentHierarchy.png
│ ├── ComponentStructure.png
│ ├── ComposeAnimationFade.gif
│ ├── ComposeAnimationFadeScale.gif
│ ├── ComposeAnimationSeparate.gif
│ ├── ComposeAnimationSlide.gif
│ ├── DecomposeMeme.png
│ ├── LifecycleStates.png
│ ├── PluggableUiHierarchy.png
│ ├── SampleCardsAndroid.gif
│ ├── SampleComponentHierarchy.png
│ ├── SampleCountersAndroid.gif
│ ├── SampleCountersDesktop.png
│ ├── SampleCountersIos.png
│ ├── SampleCountersWeb.png
│ ├── SampleCustomNavigationDesktop.gif
│ ├── SampleCustomNavigationIos.mp4
│ ├── SampleGreetingsDemo.gif
│ ├── SampleMenuAndroid.gif
│ ├── SampleMenuAndroid.mp4
│ ├── SampleMenuIos.gif
│ ├── SampleMultiPaneDesktop.png
│ ├── SampleMultiPaneDetailsAndroid.png
│ ├── SampleMultiPaneIos.png
│ ├── SampleMultiPaneListAndroid.png
│ ├── SampleMultiPaneWeb.gif
│ ├── SampleTodoStructure.png
│ └── logo
│ │ ├── circle-1.png
│ │ ├── circle-2.png
│ │ ├── logo-titled-dark.png
│ │ ├── logo-titled.psd
│ │ ├── logo.ai
│ │ ├── logo.psd
│ │ ├── titled-1.png
│ │ ├── titled-2.png
│ │ ├── transparent-1.png
│ │ ├── transparent-1.svg
│ │ └── transparent-2.png
├── navigation
│ ├── children
│ │ └── overview.md
│ ├── items
│ │ ├── navigation.md
│ │ └── overview.md
│ ├── overview.md
│ ├── pages
│ │ ├── navigation.md
│ │ └── overview.md
│ ├── panels
│ │ ├── navigation.md
│ │ └── overview.md
│ ├── slot
│ │ ├── navigation.md
│ │ └── overview.md
│ ├── stack
│ │ ├── browser-history.md
│ │ ├── deeplinking.md
│ │ ├── navigation.md
│ │ └── overview.md
│ └── web-navigation.md
├── samples.md
└── tips-tricks
│ ├── composable-viewmodel.md
│ ├── navigation-compose-component.md
│ └── overview.md
├── extensions-android
├── .gitignore
├── api
│ └── extensions-android.api
├── build.gradle.kts
├── dependencies
│ └── releaseRuntimeClasspath.txt
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ └── extensions
│ │ └── android
│ │ ├── DefaultViewContext.kt
│ │ ├── Utils.kt
│ │ ├── ViewContext.kt
│ │ ├── ViewContextExt.kt
│ │ └── stack
│ │ └── StackRouterView.kt
│ └── res
│ └── values
│ └── ids.xml
├── extensions-compose-experimental
├── .gitignore
├── api
│ ├── android
│ │ └── extensions-compose-experimental.api
│ ├── extensions-compose-experimental.klib.api
│ └── jvm
│ │ └── extensions-compose-experimental.api
├── build.gradle.kts
├── dependencies
│ └── androidReleaseRuntimeClasspath.txt
└── src
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ └── extensions
│ │ └── compose
│ │ └── experimental
│ │ ├── BroadcastBackHandler.kt
│ │ ├── Utils.kt
│ │ ├── panels
│ │ ├── ChildPanels.kt
│ │ ├── ChildPanelsAnimators.kt
│ │ ├── ChildPanelsLayout.kt
│ │ └── HorizontalChildPanelsLayout.kt
│ │ └── stack
│ │ ├── ChildStack.kt
│ │ ├── SimpleAnimatedVisibilityScope.kt
│ │ ├── Utils.kt
│ │ └── animation
│ │ ├── DefaultStackAnimation.kt
│ │ ├── DefaultStackAnimator.kt
│ │ ├── EmptyStackAnimation.kt
│ │ ├── Fade.kt
│ │ ├── PredictiveBackParams.kt
│ │ ├── Scale.kt
│ │ ├── Slide.kt
│ │ ├── StackAnimation.kt
│ │ ├── StackAnimationProvider.kt
│ │ └── StackAnimator.kt
│ └── jvmTest
│ └── kotlin
│ └── com
│ └── arkivanov
│ └── decompose
│ └── extensions
│ └── compose
│ └── experimental
│ ├── TestUtils.kt
│ └── stack
│ ├── ChildStackTest.kt
│ └── animation
│ ├── DefaultStackAnimationTest.kt
│ ├── GetFadeAlphaTest.kt
│ ├── PredictiveBackGestureTest.kt
│ └── StackAnimationDirectionsTest.kt
├── extensions-compose
├── .gitignore
├── api
│ ├── android
│ │ └── extensions-compose.api
│ ├── extensions-compose.klib.api
│ └── jvm
│ │ └── extensions-compose.api
├── build.gradle.kts
├── dependencies
│ └── androidReleaseRuntimeClasspath.txt
└── src
│ ├── androidMain
│ ├── AndroidManifest.xml
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ └── extensions
│ │ └── compose
│ │ └── stack
│ │ └── animation
│ │ └── predictiveback
│ │ └── LayoutCorners.android.kt
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ └── extensions
│ │ └── compose
│ │ ├── SubscribeAsState.kt
│ │ ├── lazyitems
│ │ └── ChildItemsLifecycleController.kt
│ │ ├── pages
│ │ ├── ChildPages.kt
│ │ └── PagesScrollAnimation.kt
│ │ ├── stack
│ │ ├── Children.kt
│ │ └── animation
│ │ │ ├── AbstractStackAnimation.kt
│ │ │ ├── DefaultStackAnimator.kt
│ │ │ ├── Direction.kt
│ │ │ ├── EmptyStackAnimation.kt
│ │ │ ├── EmptyStackAnimator.kt
│ │ │ ├── Fade.kt
│ │ │ ├── MovableStackAnimation.kt
│ │ │ ├── Scale.kt
│ │ │ ├── SimpleStackAnimation.kt
│ │ │ ├── Slide.kt
│ │ │ ├── StackAnimation.kt
│ │ │ ├── StackAnimationProvider.kt
│ │ │ ├── StackAnimator.kt
│ │ │ └── predictiveback
│ │ │ ├── AndroidPredictiveBackAnimatable.kt
│ │ │ ├── BackGestureHandler.kt
│ │ │ ├── DefaultPredictiveBackAnimatable.kt
│ │ │ ├── LayoutCorners.kt
│ │ │ ├── MaterialPredictiveBackAnimatable.kt
│ │ │ ├── PredictiveBackAnimatable.kt
│ │ │ └── PredictiveBackAnimation.kt
│ │ └── utils
│ │ ├── Icon.kt
│ │ └── InputConsumingOverlay.kt
│ ├── jvmMain
│ ├── kotlin
│ │ └── com
│ │ │ └── arkivanov
│ │ │ └── decompose
│ │ │ └── extensions
│ │ │ └── compose
│ │ │ ├── lifecycle
│ │ │ └── LifecycleController.kt
│ │ │ └── mainthread
│ │ │ └── SwingMainThreadChecker.kt
│ └── resources
│ │ └── META-INF
│ │ └── services
│ │ └── com.arkivanov.decompose.mainthread.MainThreadChecker
│ ├── jvmTest
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── decompose
│ │ └── extensions
│ │ └── compose
│ │ ├── Utils.kt
│ │ ├── lifecycle
│ │ └── LifecycleControllerTest.kt
│ │ ├── pages
│ │ └── ChildPagesTest.kt
│ │ └── stack
│ │ ├── ChildrenTest.kt
│ │ └── animation
│ │ ├── GetFadeAlphaTest.kt
│ │ ├── SimpleStackAnimationTest.kt
│ │ └── StackAnimationDirectionsTest.kt
│ └── nonAndroidMain
│ └── kotlin
│ └── com
│ └── arkivanov
│ └── decompose
│ └── extensions
│ └── compose
│ └── stack
│ └── animation
│ └── predictiveback
│ ├── LayoutCorners.nonAndroid.kt
│ ├── PredictiveBackGestureIcon.kt
│ └── PredictiveBackGestureOverlay.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── kotlin-js-store
└── yarn.lock
├── mkdocs.yml
├── sample
├── app-android
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── app
│ │ │ └── MainActivity.kt
│ │ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ └── main_activity.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
├── app-desktop
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── jvmMain
│ │ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── sample
│ │ └── app
│ │ ├── Main.kt
│ │ └── Utils.kt
├── app-ios-compose
│ ├── .gitignore
│ ├── app-ios-compose.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ ├── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ └── xcuserdata
│ │ │ │ └── arkivanov.xcuserdatad
│ │ │ │ └── UserInterfaceState.xcuserstate
│ │ └── xcuserdata
│ │ │ └── arkivanov.xcuserdatad
│ │ │ └── xcschemes
│ │ │ └── xcschememanagement.plist
│ └── app-ios-compose
│ │ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ │ ├── Info.plist
│ │ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ │ ├── RootView.swift
│ │ ├── app_ios_compose.entitlements
│ │ └── iOSApp.swift
├── app-ios
│ ├── .gitignore
│ ├── app-ios.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── app-ios
│ │ ├── ArticleAuthorView.swift
│ │ ├── ArticleDetailsView.swift
│ │ ├── ArticleListView.swift
│ │ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── cat1.imageset
│ │ │ ├── Contents.json
│ │ │ └── cat1.jpg
│ │ ├── cat2.imageset
│ │ │ ├── Contents.json
│ │ │ └── cat2.jpg
│ │ ├── cat3.imageset
│ │ │ ├── Contents.json
│ │ │ └── cat3.jpg
│ │ ├── cat4.imageset
│ │ │ ├── Contents.json
│ │ │ └── cat4.jpg
│ │ └── cat5.imageset
│ │ │ ├── Contents.json
│ │ │ └── cat5.jpg
│ │ ├── CounterView.swift
│ │ ├── CountersView.swift
│ │ ├── CustomNavigationView.swift
│ │ ├── DecomposeHelpers
│ │ ├── MutableValue.swift
│ │ ├── ObservableValue.swift
│ │ ├── SimpleChildStack.swift
│ │ ├── StackView.swift
│ │ └── StateValue.swift
│ │ ├── KittenView.swift
│ │ ├── MenuView.swift
│ │ ├── MultiPaneView.swift
│ │ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ │ ├── RootView.swift
│ │ ├── TabsView.swift
│ │ └── app_iosApp.swift
├── app-js-compose
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── src
│ │ └── jsMain
│ │ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── arkivanov
│ │ │ │ └── decompose
│ │ │ │ └── sample
│ │ │ │ └── app
│ │ │ │ └── Main.kt
│ │ │ └── resources
│ │ │ └── index.html
│ └── webpack.config.d
│ │ └── devServerConfig.js
├── app-js
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── src
│ │ └── main
│ │ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── arkivanov
│ │ │ │ └── sample
│ │ │ │ └── app
│ │ │ │ └── Main.kt
│ │ │ └── resources
│ │ │ └── index.html
│ └── webpack.config.d
│ │ └── devServerConfig.js
└── shared
│ ├── compose
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ ├── androidMain
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ └── dynamicfeatures
│ │ │ ├── FeatureContentFactory.kt
│ │ │ ├── feature1
│ │ │ └── Feature1ContentFactoryAndroid.kt
│ │ │ └── feature2
│ │ │ └── Feature2ContentFactoryAndroid.kt
│ │ ├── commonMain
│ │ ├── composeResources
│ │ │ └── drawable
│ │ │ │ ├── cat1.jpg
│ │ │ │ ├── cat2.jpg
│ │ │ │ ├── cat3.jpg
│ │ │ │ ├── cat4.jpg
│ │ │ │ └── cat5.jpg
│ │ └── kotlin
│ │ │ ├── androidx
│ │ │ └── compose
│ │ │ │ └── desktop
│ │ │ │ └── ui
│ │ │ │ └── tooling
│ │ │ │ └── preview
│ │ │ │ └── Preview.kt
│ │ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ ├── Icons.kt
│ │ │ ├── PainterResource.kt
│ │ │ ├── cards
│ │ │ ├── CardsContent.kt
│ │ │ └── card
│ │ │ │ └── CardContent.kt
│ │ │ ├── counters
│ │ │ ├── CountersContent.kt
│ │ │ └── counter
│ │ │ │ └── CounterContent.kt
│ │ │ ├── customnavigation
│ │ │ ├── CustomNavigationContent.kt
│ │ │ └── KittenContent.kt
│ │ │ ├── dialog
│ │ │ └── DialogContent.kt
│ │ │ ├── dynamicfeatures
│ │ │ ├── DynamicFeaturesContent.kt
│ │ │ ├── dynamicfeature
│ │ │ │ └── DynamicFeatureContent.kt
│ │ │ ├── feature1
│ │ │ │ └── Feature1ContentFactory.kt
│ │ │ └── feature2
│ │ │ │ └── Feature2ContentFactory.kt
│ │ │ ├── menu
│ │ │ └── MenuContent.kt
│ │ │ ├── multipane
│ │ │ ├── MultiPaneContent.kt
│ │ │ ├── author
│ │ │ │ └── ArticleAuthorContent.kt
│ │ │ ├── details
│ │ │ │ └── ArticleDetailsContent.kt
│ │ │ └── list
│ │ │ │ └── ArticleListContent.kt
│ │ │ ├── pages
│ │ │ └── PagesContent.kt
│ │ │ ├── root
│ │ │ └── RootContent.kt
│ │ │ ├── sharedtransitions
│ │ │ ├── SharedTransitionsContent.kt
│ │ │ ├── gallery
│ │ │ │ └── GalleryContent.kt
│ │ │ ├── photo
│ │ │ │ └── PhotoContent.kt
│ │ │ └── thumbnail
│ │ │ │ └── ThumbnailContent.kt
│ │ │ ├── tabs
│ │ │ └── TabsContent.kt
│ │ │ └── utils
│ │ │ ├── Utils.kt
│ │ │ ├── Vector.kt
│ │ │ └── Views.kt
│ │ ├── iosMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ ├── dynamicfeatures
│ │ │ ├── feature1
│ │ │ │ └── Feature1ContentFactoryIos.kt
│ │ │ └── feature2
│ │ │ │ └── Feature2ContentFactoryIos.kt
│ │ │ └── root
│ │ │ └── RootViewController.kt
│ │ ├── jsMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ ├── dynamicfeatures
│ │ │ ├── feature1
│ │ │ │ └── Feature1ContentFactoryIos.kt
│ │ │ └── feature2
│ │ │ │ └── Feature2ContentFactoryIos.kt
│ │ │ └── utils
│ │ │ └── Utils.js.kt
│ │ ├── jvmMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ └── dynamicfeatures
│ │ │ ├── feature1
│ │ │ └── Feature1ContentFactoryJvm.kt
│ │ │ └── feature2
│ │ │ └── Feature2ContentFactoryJvm.kt
│ │ ├── jvmTest
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ └── counters
│ │ │ └── counter
│ │ │ ├── CounterContentTest.kt
│ │ │ └── TestCounterComponent.kt
│ │ └── nonWebMain
│ │ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── sample
│ │ └── shared
│ │ └── utils
│ │ └── Utils.nonWeb.kt
│ ├── dynamic-features
│ ├── api
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ ├── androidMain
│ │ │ └── AndroidManifest.xml
│ │ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ └── dynamicfeatures
│ │ │ ├── feature1
│ │ │ └── Feature1.kt
│ │ │ └── feature2
│ │ │ └── Feature2.kt
│ ├── compose-api
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ ├── androidMain
│ │ │ └── AndroidManifest.xml
│ │ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ └── dynamicfeatures
│ │ │ └── DynamicFeatureContent.kt
│ ├── feature1Impl
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ ├── androidMain
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── arkivanov
│ │ │ │ │ └── sample
│ │ │ │ │ └── shared
│ │ │ │ │ └── dynamicfeatures
│ │ │ │ │ └── feature1
│ │ │ │ │ └── Feature1Content.kt
│ │ │ └── res
│ │ │ │ └── values
│ │ │ │ └── strings.xml
│ │ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── arkivanov
│ │ │ │ └── sample
│ │ │ │ └── shared
│ │ │ │ └── dynamicfeatures
│ │ │ │ └── feature1
│ │ │ │ └── Feature1Component.kt
│ │ │ └── jvmMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ └── dynamicfeatures
│ │ │ └── feature1
│ │ │ └── Feature1Content.kt
│ └── feature2Impl
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ └── src
│ │ ├── androidMain
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── arkivanov
│ │ │ │ └── sample
│ │ │ │ └── shared
│ │ │ │ └── dynamicfeatures
│ │ │ │ └── feature2
│ │ │ │ └── Feature2Content.kt
│ │ └── res
│ │ │ └── values
│ │ │ └── strings.xml
│ │ ├── commonMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ └── dynamicfeatures
│ │ │ └── feature2
│ │ │ └── Feature2Component.kt
│ │ └── jvmMain
│ │ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── sample
│ │ └── shared
│ │ └── dynamicfeatures
│ │ └── feature2
│ │ └── Feature2Content.kt
│ └── shared
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ ├── androidMain
│ ├── AndroidManifest.xml
│ ├── kotlin
│ │ └── com
│ │ │ └── arkivanov
│ │ │ └── sample
│ │ │ └── shared
│ │ │ ├── Transitions.kt
│ │ │ ├── counters
│ │ │ ├── CountersView.kt
│ │ │ └── counter
│ │ │ │ └── CounterView.kt
│ │ │ ├── dynamicfeatures
│ │ │ ├── FeatureFactory.kt
│ │ │ ├── dynamicfeature
│ │ │ │ └── DefaultFeatureInstaller.kt
│ │ │ ├── feature1
│ │ │ │ └── Feature1FactoryAndroid.kt
│ │ │ └── feature2
│ │ │ │ └── Feature2FactoryAndroid.kt
│ │ │ ├── root
│ │ │ ├── NotImplementedView.kt
│ │ │ └── RootView.kt
│ │ │ └── tabs
│ │ │ └── TabsView.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_arrow_back.xml
│ │ ├── ic_tab_cards.xml
│ │ ├── ic_tab_counters.xml
│ │ ├── ic_tab_menu.xml
│ │ └── ic_tab_multipane.xml
│ │ ├── layout
│ │ ├── counter.xml
│ │ ├── counters.xml
│ │ ├── not_implemented.xml
│ │ ├── root.xml
│ │ └── tabs.xml
│ │ └── menu
│ │ └── root_tabs.xml
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── sample
│ │ └── shared
│ │ ├── ImageResourceId.kt
│ │ ├── Json.kt
│ │ ├── PreviewComponentContext.kt
│ │ ├── SimpleLazyChildItems.kt
│ │ ├── Url.kt
│ │ ├── Utils.kt
│ │ ├── cards
│ │ ├── CardsComponent.kt
│ │ ├── DefaultCardsComponent.kt
│ │ ├── PreviewCardsComponent.kt
│ │ └── card
│ │ │ ├── CardComponent.kt
│ │ │ ├── DefaultCardComponent.kt
│ │ │ └── PreviewCardComponent.kt
│ │ ├── counters
│ │ ├── CountersComponent.kt
│ │ ├── DefaultCountersComponent.kt
│ │ ├── PreviewCountersComponent.kt
│ │ └── counter
│ │ │ ├── CounterComponent.kt
│ │ │ ├── DefaultCounterComponent.kt
│ │ │ └── PreviewCounterComponent.kt
│ │ ├── customnavigation
│ │ ├── CustomNavigationComponent.kt
│ │ ├── DefaultCustomNavigationComponent.kt
│ │ ├── DefaultKittenComponent.kt
│ │ ├── KittenComponent.kt
│ │ └── PreviewKittenComponent.kt
│ │ ├── dialog
│ │ ├── DefaultDialogComponent.kt
│ │ └── DialogComponent.kt
│ │ ├── dynamicfeatures
│ │ ├── DefaultDynamicFeaturesComponent.kt
│ │ ├── DynamicFeaturesComponent.kt
│ │ ├── dynamicfeature
│ │ │ ├── DefaultDynamicFeatureComponent.kt
│ │ │ ├── DynamicFeatureComponent.kt
│ │ │ └── FeatureInstaller.kt
│ │ ├── feature1
│ │ │ └── Feature1Factory.kt
│ │ └── feature2
│ │ │ └── Feature2Factory.kt
│ │ ├── menu
│ │ ├── DefaultMenuComponent.kt
│ │ ├── MenuComponent.kt
│ │ └── PreviewMenuComponent.kt
│ │ ├── multipane
│ │ ├── DefaultMultiPaneComponent.kt
│ │ ├── MultiPaneComponent.kt
│ │ ├── PreviewMultiPaneComponent.kt
│ │ ├── author
│ │ │ ├── ArticleAuthorComponent.kt
│ │ │ └── DefaultArticleAuthorComponent.kt
│ │ ├── database
│ │ │ ├── ArticleDatabase.kt
│ │ │ ├── ArticleEntity.kt
│ │ │ ├── AuthorEntity.kt
│ │ │ ├── DefaultArticleDatabase.kt
│ │ │ └── LorenIpsumGenerator.kt
│ │ ├── details
│ │ │ ├── ArticleDetailsComponent.kt
│ │ │ ├── DefaultArticleDetailsComponent.kt
│ │ │ └── PreviewArticleDetailsComponent.kt
│ │ └── list
│ │ │ ├── ArticleListComponent.kt
│ │ │ ├── DefaultArticleListComponent.kt
│ │ │ └── PreviewArticleListComponent.kt
│ │ ├── pages
│ │ ├── DefaultPagesComponent.kt
│ │ └── PagesComponent.kt
│ │ ├── root
│ │ ├── DefaultRootComponent.kt
│ │ ├── PreviewRootComponent.kt
│ │ └── RootComponent.kt
│ │ ├── sharedtransitions
│ │ ├── DefaultSharedTransitionsComponent.kt
│ │ ├── Image.kt
│ │ ├── SharedTransitionsComponent.kt
│ │ ├── gallery
│ │ │ ├── DefaultGalleryComponent.kt
│ │ │ ├── GalleryComponent.kt
│ │ │ └── PreviewGalleryComponent.kt
│ │ ├── photo
│ │ │ ├── DefaultPhotoComponent.kt
│ │ │ ├── PhotoComponent.kt
│ │ │ └── PreviewPhotoComponent.kt
│ │ └── thumbnail
│ │ │ ├── DefaultThumbnailComponent.kt
│ │ │ ├── PreviewThumbnailComponent.kt
│ │ │ └── ThumbnailComponent.kt
│ │ └── tabs
│ │ ├── DefaultTabsComponent.kt
│ │ ├── PreviewTabsComponent.kt
│ │ └── TabsComponent.kt
│ ├── commonTest
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── sample
│ │ └── shared
│ │ ├── TestUtils.kt
│ │ ├── counters
│ │ ├── CountersComponentIntegrationTest.kt
│ │ └── counter
│ │ │ └── CounterComponentTest.kt
│ │ ├── dynamicfeatures
│ │ └── dynamicfeature
│ │ │ └── TestFeatureInstaller.kt
│ │ ├── root
│ │ └── RootComponentIntegrationTest.kt
│ │ └── tabs
│ │ └── TabsComponentIntegrationTest.kt
│ ├── iosMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── sample
│ │ └── shared
│ │ └── StateKeeperUtils.kt
│ ├── jsMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── sample
│ │ └── shared
│ │ ├── RProps.kt
│ │ ├── Scaffold.kt
│ │ ├── UniqueId.kt
│ │ ├── Utils.kt
│ │ ├── counters
│ │ ├── CountersContent.kt
│ │ └── counter
│ │ │ └── CounterContent.kt
│ │ ├── dialog
│ │ └── DialogContent.kt
│ │ ├── dynamicfeatures
│ │ ├── DynamicFeaturesContent.kt
│ │ ├── dynamicfeature
│ │ │ └── DynamicFeatureContent.kt
│ │ ├── feature1
│ │ │ └── Feature1Content.kt
│ │ └── feature2
│ │ │ └── Feature2Content.kt
│ │ ├── multipane
│ │ ├── MultiPaneContent.kt
│ │ ├── author
│ │ │ └── ArticleAuthorContent.kt
│ │ ├── details
│ │ │ └── ArticleDetailsContent.kt
│ │ └── list
│ │ │ └── ArticleListContent.kt
│ │ ├── root
│ │ ├── NotImplementedContent.kt
│ │ └── RootContent.kt
│ │ └── tabs
│ │ └── TabsContent.kt
│ ├── jvmMain
│ └── kotlin
│ │ └── com
│ │ └── arkivanov
│ │ └── sample
│ │ └── shared
│ │ └── StateKeeperUtils.kt
│ └── nonAndroidMain
│ └── kotlin
│ └── com
│ └── arkivanov
│ └── sample
│ └── shared
│ └── dynamicfeatures
│ ├── dynamicfeature
│ └── DefaultFeatureInstaller.kt
│ ├── feature1
│ └── Feature1FactoryNonAndroid.kt
│ └── feature2
│ └── Feature2FactoryNonAndroid.kt
├── settings.gradle.kts
└── tools
└── check-publication
├── .gitignore
├── build.gradle.kts
└── src
├── androidMain
└── AndroidManifest.xml
└── commonMain
└── kotlin
└── com
└── arkivanov
└── decompose
└── tools
└── checkpublication
└── Dummy.kt
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: arkivanov
4 | custom: ["https://www.buymeacoffee.com/arkivanov", "https://btc.com/1DXjn9e6rmbVvac3TH8hG3LdLtoA1CUvsM", "https://etherscan.io/address/0xf027f5738f45676a54c15cf7753a0f66553947b9"]
5 |
--------------------------------------------------------------------------------
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
1 | name: build and deploy material mkdocs to gh-pages
2 | on:
3 | push:
4 | branches:
5 | - master
6 | - main
7 | workflow_dispatch:
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-python@v2
14 | with:
15 | python-version: 3.x
16 | - run: pip install mkdocs-material
17 | - run: pip install mkdocs-minify-plugin
18 | - run: mkdocs gh-deploy --force
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | .DS_Store
12 | /node_modules
13 | /npm-debug.log*
14 | __pycache__
15 | venv
16 | /MANIFEST
17 | /manifest.json
18 | /site
19 | /dist
20 | /mkdocs_material.egg-info
21 | .vscode
22 | .kotlin
23 |
--------------------------------------------------------------------------------
/.idea/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/.idea/icon.png
--------------------------------------------------------------------------------
/decompose/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/decompose/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/decompose/consumer-rules.pro
--------------------------------------------------------------------------------
/decompose/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/decompose/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/decompose/src/androidMain/kotlin/com/arkivanov/decompose/Lock.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | internal actual class Lock actual constructor() {
4 |
5 | actual inline fun synchronizedImpl(block: () -> T): T =
6 | synchronized(this, block)
7 | }
8 |
--------------------------------------------------------------------------------
/decompose/src/androidMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.errorhandler
2 |
3 | import android.util.Log
4 |
5 | private const val LOG_TAG = "Decompose"
6 | private var isLogCatEnabled = true
7 |
8 | internal actual fun printError(exception: Exception) {
9 | if (isLogCatEnabled) {
10 | try {
11 | Log.e(LOG_TAG, exception.message ?: "An occurred in Decompose", exception)
12 | } catch (e: Exception) {
13 | isLogCatEnabled = false
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/decompose/src/androidMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.mainthread
2 |
3 | import android.os.Looper
4 | import com.arkivanov.decompose.errorhandler.onDecomposeError
5 |
6 | private val mainThreadId: Long? by lazy {
7 | try {
8 | Looper.getMainLooper().thread.id
9 | } catch (e: Exception) {
10 | null
11 | }
12 | }
13 |
14 | internal actual fun checkMainThread() {
15 | if ((mainThreadId != null) && (Thread.currentThread().id != mainThreadId)) {
16 | onDecomposeError(NotOnMainThreadException(currentThreadName = Thread.currentThread().name))
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/decompose/src/androidMain/resources/META-INF/com/arkivanov/decompose/decompose-android/verification.properties:
--------------------------------------------------------------------------------
1 | #This is the verification token for the com.arkivanov.decompose:decompose-android SDK.
2 | #Tue Sep 03 14:32:43 PDT 2024
3 | token=5PI3YZEPWRCA5BGPLBZPVRKDYQ
4 |
--------------------------------------------------------------------------------
/decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/AndroidTestUtils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import com.arkivanov.essenty.lifecycle.Lifecycle
4 | import com.arkivanov.essenty.lifecycle.subscribe
5 |
6 | fun Lifecycle.logEvents(): MutableList =
7 | ArrayList().apply {
8 | subscribe(
9 | onCreate = { add("onCreate") },
10 | onStart = { add("onStart") },
11 | onResume = { add("onResume") },
12 | onPause = { add("onPause") },
13 | onStop = { add("onStop") },
14 | onDestroy = { add("onDestroy") },
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Cancellation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | /** A cancellation handle, returned by various functions where cancellation is required. */
4 | fun interface Cancellation {
5 |
6 | fun cancel()
7 | }
8 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/ComponentContext.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | /**
4 | * A default component context interface provided by Decompose.
5 | * Should be passed to component classes via their constructors to
6 | * enable features like lifecycle handling, state preservation, navigation,
7 | * etc.
8 | *
9 | * It is also possible to define your own interface with additional
10 | * properties or methods, and use that instead of this one.
11 | */
12 | interface ComponentContext : GenericComponentContext
13 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/ComponentContextFactory.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import com.arkivanov.essenty.backhandler.BackHandler
4 | import com.arkivanov.essenty.instancekeeper.InstanceKeeper
5 | import com.arkivanov.essenty.lifecycle.Lifecycle
6 | import com.arkivanov.essenty.statekeeper.StateKeeper
7 |
8 | /**
9 | * Represents a factory that creates new instances of component contexts of type [T].
10 | * Used by various navigation models that require creating child component contexts.
11 | */
12 | fun interface ComponentContextFactory {
13 |
14 | /**
15 | * Creates a new instance of component context of type [T], not attached to any
16 | * parent component context.
17 | */
18 | operator fun invoke(
19 | lifecycle: Lifecycle,
20 | stateKeeper: StateKeeper,
21 | instanceKeeper: InstanceKeeper,
22 | backHandler: BackHandler,
23 | ): T
24 | }
25 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/ComponentContextFactoryOwner.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | /**
4 | * Represents a holder of [ComponentContextFactory].
5 | */
6 | interface ComponentContextFactoryOwner {
7 |
8 | /**
9 | * Returns a [ComponentContextFactory] that creates new instances of
10 | * component context of type [T], not attached to any parent component context.
11 | *
12 | * This property is usually used by various navigation models (such as
13 | * [childStack][com.arkivanov.decompose.router.stack.childStack]) for creating
14 | * component contexts for child components. If you need to create a child
15 | * component context, see [childContext].
16 | */
17 | val componentContextFactory: ComponentContextFactory
18 | }
19 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/DecomposeExperimentFlags.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | @ExperimentalDecomposeApi
4 | object DecomposeExperimentFlags {
5 |
6 | var duplicateConfigurationsEnabled: Boolean = false
7 | }
8 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/GenericComponentContext.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import com.arkivanov.essenty.backhandler.BackHandlerOwner
4 | import com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner
5 | import com.arkivanov.essenty.lifecycle.LifecycleOwner
6 | import com.arkivanov.essenty.statekeeper.StateKeeperOwner
7 |
8 | /**
9 | * A generic component context that extends [LifecycleOwner], [StateKeeperOwner],
10 | * [InstanceKeeperOwner] and [BackHandlerOwner] interfaces, and also able to create
11 | * new instances of itself via [ComponentContextFactory].
12 | */
13 | interface GenericComponentContext :
14 | LifecycleOwner,
15 | StateKeeperOwner,
16 | InstanceKeeperOwner,
17 | BackHandlerOwner,
18 | ComponentContextFactoryOwner
19 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/GettingList.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | internal class GettingList(
4 | override val size: Int,
5 | private val get: (Int) -> T,
6 | ) : AbstractList() {
7 |
8 | override fun get(index: Int): T =
9 | get.invoke(index)
10 | }
11 |
12 | internal inline fun List.mapped(crossinline mapper: (T) -> R): List =
13 | GettingList(size = size) { mapper(get(it)) }
14 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Lock.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import kotlin.contracts.ExperimentalContracts
4 | import kotlin.contracts.InvocationKind
5 | import kotlin.contracts.contract
6 |
7 | internal expect class Lock() {
8 |
9 | inline fun synchronizedImpl(block: () -> T): T
10 | }
11 |
12 | @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")
13 | @OptIn(ExperimentalContracts::class)
14 | internal inline fun Lock.synchronized(block: () -> T): T {
15 | contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
16 |
17 | return synchronizedImpl(block)
18 | }
19 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Ref.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | @InternalDecomposeApi
4 | class Ref(var value: T)
5 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/ErrorHandlers.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.errorhandler
2 |
3 | /**
4 | * Called when a non-fatal error has occurred in Decompose.
5 | */
6 | var onDecomposeError: (Exception) -> Unit = ::printError
7 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.errorhandler
2 |
3 | internal expect fun printError(exception: Exception)
4 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/instancekeeper/ChildInstanceKeeper.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.instancekeeper
2 |
3 | import com.arkivanov.decompose.isDestroyed
4 | import com.arkivanov.essenty.instancekeeper.InstanceKeeper
5 | import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
6 | import com.arkivanov.essenty.instancekeeper.getOrCreate
7 | import com.arkivanov.essenty.lifecycle.Lifecycle
8 | import com.arkivanov.essenty.lifecycle.doOnDestroy
9 |
10 | internal fun InstanceKeeper.child(key: String, lifecycle: Lifecycle? = null): InstanceKeeper =
11 | if ((lifecycle == null) || !lifecycle.isDestroyed) {
12 | val registry = getOrCreate(key, ::ChildInstanceKeeperProvider).instanceKeeperRegistry
13 |
14 | lifecycle?.doOnDestroy {
15 | remove(key)?.onDestroy()
16 | }
17 |
18 | registry
19 | } else {
20 | InstanceKeeperDispatcher()
21 | }
22 |
23 | private class ChildInstanceKeeperProvider : InstanceKeeper.Instance {
24 | val instanceKeeperRegistry = InstanceKeeperDispatcher()
25 |
26 | override fun onDestroy() {
27 | instanceKeeperRegistry.destroy()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/instancekeeper/InstanceKeeperExt.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.instancekeeper
2 |
3 | import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
4 | import com.arkivanov.essenty.lifecycle.Lifecycle
5 | import com.arkivanov.essenty.lifecycle.doOnDestroy
6 |
7 | internal fun InstanceKeeperDispatcher.attachTo(lifecycle: Lifecycle): InstanceKeeperDispatcher {
8 | lifecycle.doOnDestroy(::destroy)
9 |
10 | return this
11 | }
12 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.mainthread
2 |
3 | internal expect fun checkMainThread()
4 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/mainthread/NotOnMainThreadException.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.mainthread
2 |
3 | internal class NotOnMainThreadException(currentThreadName: String?) : Exception(
4 | "Expected to be called on the main thread, but was ${currentThreadName ?: "unknown"}"
5 | )
6 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildControllerExt.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.children
2 |
3 | import com.arkivanov.essenty.lifecycle.Lifecycle
4 |
5 | internal fun ChildController.isActive(configuration: C): Boolean =
6 | getLifecycleState(configuration)?.takeIf { it > Lifecycle.State.DESTROYED } != null
7 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildItemFactory.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.children
2 |
3 | import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
4 | import com.arkivanov.essenty.statekeeper.SerializableContainer
5 |
6 | internal interface ChildItemFactory {
7 |
8 | operator fun invoke(
9 | configuration: C,
10 | savedState: SerializableContainer? = null,
11 | instanceKeeperDispatcher: InstanceKeeperDispatcher? = null,
12 | ): ChildItem.Created
13 | }
14 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/NavState.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.children
2 |
3 | /**
4 | * Represents an entire navigation state.
5 | */
6 | interface NavState {
7 |
8 | /**
9 | * A list of child navigation states. Every [ChildNavState.configuration] must be unique by equality.
10 | */
11 | val children: List>
12 | }
13 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/NavigationSource.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.children
2 |
3 | import com.arkivanov.decompose.Cancellation
4 |
5 | /**
6 | * Represents a generic source of navigation events.
7 | *
8 | * @see [children]
9 | */
10 | interface NavigationSource {
11 |
12 | fun subscribe(observer: (T) -> Unit): Cancellation
13 | }
14 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/SimpleChildNavState.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.children
2 |
3 | /**
4 | * A simple implementation of the [ChildNavState] interface.
5 | */
6 | data class SimpleChildNavState(
7 | override val configuration: C,
8 | override val status: ChildNavState.Status,
9 | ) : ChildNavState
10 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/SimpleNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.children
2 |
3 | import com.arkivanov.decompose.Cancellation
4 | import com.arkivanov.decompose.Relay
5 |
6 | /**
7 | * A simple implementation of the [NavigationSource] interface.
8 | * Broadcasts navigation events to every subscribed observer.
9 | */
10 | class SimpleNavigation : NavigationSource {
11 |
12 | private val relay = Relay()
13 |
14 | override fun subscribe(observer: (T) -> Unit): Cancellation =
15 | relay.subscribe(observer)
16 |
17 | /**
18 | * Broadcasts the navigation event to every subscribed observer.
19 | *
20 | * Should be called on the main thread.
21 | */
22 | fun navigate(event: T) {
23 | relay.accept(event)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/items/ChildItems.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.items
2 |
3 | import com.arkivanov.decompose.ExperimentalDecomposeApi
4 | import com.arkivanov.decompose.router.items.Items.ActiveLifecycleState
5 |
6 | /**
7 | * A state holder for Child Items.
8 | *
9 | * @param items a list of child configurations, can be empty.
10 | * @param activeItems a map of instantiated child components and their lifecycle states.
11 | * See [ActiveLifecycleState].
12 | */
13 | @ExperimentalDecomposeApi
14 | data class ChildItems(
15 | val items: List = emptyList(),
16 | val activeItems: Map> = emptyMap(),
17 | )
18 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/items/DefaultItemsNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.items
2 |
3 | import com.arkivanov.decompose.Cancellation
4 | import com.arkivanov.decompose.Relay
5 | import com.arkivanov.decompose.router.items.ItemsNavigation.Event
6 |
7 | internal class DefaultItemsNavigation : ItemsNavigation {
8 |
9 | private val relay = Relay>()
10 |
11 | override fun subscribe(observer: (Event) -> Unit): Cancellation =
12 | relay.subscribe(observer)
13 |
14 | override fun navigate(
15 | transformer: (Items) -> Items,
16 | onComplete: (newItems: Items, oldItems: Items) -> Unit,
17 | ) {
18 | relay.accept(Event(transformer, onComplete))
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/items/Items.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.items
2 |
3 | import com.arkivanov.decompose.ExperimentalDecomposeApi
4 | import kotlinx.serialization.Serializable
5 |
6 | /**
7 | * Represents a state of Child Items navigation model.
8 | *
9 | * @param items a list of child configurations, can be empty. Must be unique even if the
10 | * [com.arkivanov.decompose.DecomposeExperimentFlags.duplicateConfigurationsEnabled] flag is enabled.
11 | * @param activeItems a map of lifecycle states of the instantiated (active) components.
12 | * Child components whose configurations are not present in this map are destroyed.
13 | * Configurations in the map should also be present in the [items] list,
14 | * otherwise the behavior is undefined.
15 | * See [ActiveLifecycleState].
16 | */
17 | @ExperimentalDecomposeApi
18 | @Serializable
19 | data class Items(
20 | val items: List = emptyList(),
21 | val activeItems: Map = emptyMap(),
22 | ) {
23 | enum class ActiveLifecycleState {
24 | CREATED,
25 | STARTED,
26 | RESUMED,
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/items/ItemsNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.items
2 |
3 | import com.arkivanov.decompose.ExperimentalDecomposeApi
4 | import com.arkivanov.decompose.router.children.NavigationSource
5 | import com.arkivanov.decompose.router.items.ItemsNavigation.Event
6 |
7 | /**
8 | * Represents [ItemsNavigator] and [NavigationSource] at the same time.
9 | */
10 | @ExperimentalDecomposeApi
11 | interface ItemsNavigation : ItemsNavigator, NavigationSource> {
12 |
13 | class Event(
14 | val transformer: (Items) -> Items,
15 | val onComplete: (newItems: Items, oldItems: Items) -> Unit = { _, _ -> },
16 | )
17 | }
18 |
19 | /**
20 | * Returns a default implementation of [ItemsNavigation].
21 | * Broadcasts navigation events to all subscribed observers.
22 | */
23 | @ExperimentalDecomposeApi
24 | fun ItemsNavigation(): ItemsNavigation =
25 | DefaultItemsNavigation()
26 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/ChildPages.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.pages
2 |
3 | import com.arkivanov.decompose.Child
4 |
5 | /**
6 | * A state holder for Child Pages.
7 | *
8 | * @param items a list of child components.
9 | * @param selectedIndex an index of the selected child component.
10 | * Must be within the range of [items] indices if [items] is not empty, otherwise can be any number.
11 | */
12 | data class ChildPages(
13 | val items: List>,
14 | val selectedIndex: Int,
15 | ) {
16 |
17 | /**
18 | * Creates empty [ChildPages].
19 | */
20 | constructor() : this(items = emptyList(), selectedIndex = -1)
21 |
22 | init {
23 | if (items.isNotEmpty()) {
24 | require(selectedIndex in items.indices)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/DefaultPagesNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.pages
2 |
3 | import com.arkivanov.decompose.Cancellation
4 | import com.arkivanov.decompose.Relay
5 | import com.arkivanov.decompose.router.pages.PagesNavigation.Event
6 |
7 | internal class DefaultPagesNavigation : PagesNavigation {
8 |
9 | private val relay = Relay>()
10 |
11 | override fun subscribe(observer: (Event) -> Unit): Cancellation =
12 | relay.subscribe(observer)
13 |
14 | override fun navigate(transformer: (Pages) -> Pages, onComplete: (newPages: Pages, oldPages: Pages) -> Unit) {
15 | relay.accept(Event(transformer, onComplete))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/Pages.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.pages
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | /**
6 | * Represents a state of Child Pages navigation model.
7 | *
8 | * @param items a list of child configurations, must be unique, can be empty.
9 | * @param selectedIndex an index of the selected child configuration.
10 | * Must be within the range of [items] indices if [items] is not empty, otherwise can be any number.
11 | */
12 | @Serializable
13 | data class Pages(
14 | val items: List,
15 | val selectedIndex: Int,
16 | ) {
17 |
18 | /**
19 | * Creates empty [Pages].
20 | */
21 | constructor() : this(items = emptyList(), selectedIndex = -1)
22 |
23 | init {
24 | if (items.isNotEmpty()) {
25 | require(selectedIndex in items.indices) {
26 | "The selectedIndex argument must be with the range: ${items.indices}. Actual: $selectedIndex."
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.pages
2 |
3 | import com.arkivanov.decompose.router.children.NavigationSource
4 | import com.arkivanov.decompose.router.pages.PagesNavigation.Event
5 |
6 | /**
7 | * Represents [PagesNavigator] and [NavigationSource] at the same time.
8 | */
9 | interface PagesNavigation : PagesNavigator, NavigationSource> {
10 |
11 | class Event(
12 | val transformer: (Pages) -> Pages,
13 | val onComplete: (newPages: Pages, oldPages: Pages) -> Unit = { _, _ -> },
14 | )
15 | }
16 |
17 | /**
18 | * Returns a default implementation of [PagesNavigation].
19 | * Broadcasts navigation events to all subscribed observers.
20 | */
21 | fun PagesNavigation(): PagesNavigation =
22 | DefaultPagesNavigation()
23 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/DefaultPanelsNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.panels
2 |
3 | import com.arkivanov.decompose.Cancellation
4 | import com.arkivanov.decompose.Relay
5 | import com.arkivanov.decompose.router.panels.PanelsNavigation.Event
6 |
7 | internal class DefaultPanelsNavigation : PanelsNavigation {
8 |
9 | private val relay = Relay>()
10 |
11 | override fun subscribe(observer: (Event) -> Unit): Cancellation =
12 | relay.subscribe(observer)
13 |
14 | override fun navigate(
15 | transformer: (Panels) -> Panels,
16 | onComplete: (newState: Panels, oldState: Panels) -> Unit,
17 | ) {
18 | relay.accept(Event(transformer, onComplete))
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/Panels.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.panels
2 |
3 | import com.arkivanov.decompose.ExperimentalDecomposeApi
4 | import kotlinx.serialization.Serializable
5 |
6 | /**
7 | * Represents a state of Child Panels navigation model.
8 | *
9 | * @param main a configuration of the Main panel.
10 | * @param details an optional configuration of the Details panel, default value is `null`.
11 | * @param extra an optional configuration of the Extra panel, default value is `null`.
12 | * @param mode determines how lifecycles of the panels within the Child Panels navigation model are changing,
13 | * default value is [ChildPanelsMode.SINGLE], see [ChildPanelsMode].
14 | * @param MC a type of the `Main` panel configuration.
15 | * @param DC a type of the `Details` panel configuration.
16 | * @param EC a type of the `Extra` panel configuration. Use `Nothing` if the panel is not required.
17 | */
18 | @ExperimentalDecomposeApi
19 | @Serializable
20 | data class Panels(
21 | val main: MC,
22 | val details: DC? = null,
23 | val extra: EC? = null,
24 | val mode: ChildPanelsMode = ChildPanelsMode.SINGLE,
25 | )
26 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/PanelsNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.panels
2 |
3 | import com.arkivanov.decompose.ExperimentalDecomposeApi
4 | import com.arkivanov.decompose.router.children.NavigationSource
5 | import com.arkivanov.decompose.router.panels.PanelsNavigation.Event
6 |
7 | /**
8 | * Represents [PanelsNavigator] and [NavigationSource] at the same time.
9 | */
10 | @ExperimentalDecomposeApi
11 | interface PanelsNavigation : PanelsNavigator, NavigationSource> {
12 |
13 | class Event(
14 | val transformer: (Panels) -> Panels,
15 | val onComplete: (newState: Panels, oldState: Panels) -> Unit = { _, _ -> },
16 | )
17 | }
18 |
19 | /**
20 | * Returns a default implementation of [PanelsNavigation].
21 | * Broadcasts navigation events to all subscribed observers.
22 | */
23 | @ExperimentalDecomposeApi
24 | fun PanelsNavigation(): PanelsNavigation =
25 | DefaultPanelsNavigation()
26 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ChildSlot.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.slot
2 |
3 | import com.arkivanov.decompose.Child
4 |
5 | /**
6 | * A state holder for `Child Slot`.
7 | */
8 | data class ChildSlot(
9 | val child: Child.Created? = null,
10 | )
11 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/DefaultSlotNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.slot
2 |
3 | import com.arkivanov.decompose.Cancellation
4 | import com.arkivanov.decompose.Relay
5 | import com.arkivanov.decompose.router.slot.SlotNavigation.Event
6 |
7 | internal class DefaultSlotNavigation : SlotNavigation {
8 |
9 | private val relay = Relay>()
10 |
11 | override fun navigate(
12 | transformer: (configuration: C?) -> C?,
13 | onComplete: (newConfiguration: C?, oldConfiguration: C?) -> Unit,
14 | ) {
15 | relay.accept(Event(transformer, onComplete))
16 | }
17 |
18 | override fun subscribe(observer: (Event) -> Unit): Cancellation =
19 | relay.subscribe(observer)
20 | }
21 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/SlotNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.slot
2 |
3 | import com.arkivanov.decompose.router.children.NavigationSource
4 | import com.arkivanov.decompose.router.slot.SlotNavigation.Event
5 |
6 | /**
7 | * Represents [SlotNavigator] and [NavigationSource] at the same time.
8 | */
9 | interface SlotNavigation : SlotNavigator, NavigationSource> {
10 |
11 | class Event(
12 | val transformer: (configuration: C?) -> C?,
13 | val onComplete: (newConfiguration: C?, oldConfiguration: C?) -> Unit = { _, _ -> },
14 | )
15 | }
16 |
17 | /**
18 | * Returns a default implementation of [SlotNavigation].
19 | * Broadcasts navigation events to all subscribed observers.
20 | */
21 | fun SlotNavigation(): SlotNavigation =
22 | DefaultSlotNavigation()
23 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ValueExt.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.slot
2 |
3 | import com.arkivanov.decompose.Child
4 | import com.arkivanov.decompose.value.Value
5 |
6 | val Value>.child: Child.Created? get() = value.child
7 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/DefaultStackNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack
2 |
3 | import com.arkivanov.decompose.Cancellation
4 | import com.arkivanov.decompose.Relay
5 | import com.arkivanov.decompose.router.stack.StackNavigation.Event
6 |
7 | internal class DefaultStackNavigation : StackNavigation {
8 |
9 | private val relay = Relay>()
10 |
11 | override fun navigate(transformer: (stack: List) -> List, onComplete: (newStack: List, oldStack: List) -> Unit) {
12 | relay.accept(Event(transformer, onComplete))
13 | }
14 |
15 | override fun subscribe(observer: (Event) -> Unit): Cancellation =
16 | relay.subscribe(observer)
17 | }
18 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack
2 |
3 | import com.arkivanov.decompose.router.children.NavigationSource
4 | import com.arkivanov.decompose.router.stack.StackNavigation.Event
5 |
6 | /**
7 | * Represents [StackNavigator] and [NavigationSource] at the same time.
8 | */
9 | interface StackNavigation : StackNavigator, NavigationSource> {
10 |
11 | class Event(
12 | val transformer: (stack: List) -> List,
13 | val onComplete: (newStack: List, oldStack: List) -> Unit = { _, _ -> },
14 | )
15 | }
16 |
17 | /**
18 | * Returns a default implementation of [StackNavigation].
19 | * Broadcasts navigation events to all subscribed observers.
20 | */
21 | fun StackNavigation(): StackNavigation =
22 | DefaultStackNavigation()
23 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ValueExt.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack
2 |
3 | import com.arkivanov.decompose.Child
4 | import com.arkivanov.decompose.value.Value
5 |
6 | val Value>.active: Child.Created get() = value.active
7 |
8 | val Value>.backStack: List> get() = value.backStack
9 |
10 | val Value>.items: List> get() = value.items
11 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/NoOpWebNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.webhistory
2 |
3 | import com.arkivanov.decompose.router.webhistory.WebNavigation.HistoryItem
4 | import com.arkivanov.decompose.value.MutableValue
5 | import com.arkivanov.decompose.value.Value
6 | import kotlinx.serialization.ExperimentalSerializationApi
7 | import kotlinx.serialization.KSerializer
8 | import kotlinx.serialization.builtins.NothingSerializer
9 |
10 | internal object NoOpWebNavigation : WebNavigation {
11 |
12 | @OptIn(ExperimentalSerializationApi::class)
13 | override val serializer: KSerializer get() = NothingSerializer()
14 |
15 | override val history: Value>> = MutableValue(emptyList())
16 |
17 | override fun onBeforeNavigate(): Boolean = true
18 |
19 | override fun navigate(history: List) {
20 | // No-op
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/webhistory/WebNavigationOwner.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.webhistory
2 |
3 | import com.arkivanov.decompose.ExperimentalDecomposeApi
4 |
5 | /**
6 | * Represents a holder of [WebNavigation], typically implemented by
7 | * a Decompose component.
8 | */
9 | @ExperimentalDecomposeApi
10 | interface WebNavigationOwner {
11 |
12 | val webNavigation: WebNavigation<*>
13 |
14 | /**
15 | * A no-op interface that extends [WebNavigationOwner]. Can be useful
16 | * for fake or preview component implementations.
17 | */
18 | interface NoOp : WebNavigationOwner {
19 | override val webNavigation: WebNavigation<*> get() = NoOpWebNavigation
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/statekeeper/ChildStateKeeper.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.statekeeper
2 |
3 | import com.arkivanov.decompose.isDestroyed
4 | import com.arkivanov.essenty.lifecycle.Lifecycle
5 | import com.arkivanov.essenty.lifecycle.doOnDestroy
6 | import com.arkivanov.essenty.statekeeper.SerializableContainer
7 | import com.arkivanov.essenty.statekeeper.StateKeeper
8 | import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher
9 |
10 | internal fun StateKeeper.child(key: String, lifecycle: Lifecycle? = null): StateKeeper {
11 | check(!isRegistered(key = key)) { "The key \"$key\" is already in use." }
12 |
13 | val stateKeeper = StateKeeperDispatcher(consume(key = key, strategy = SerializableContainer.serializer()))
14 |
15 | if (lifecycle == null) {
16 | register(key = key, strategy = SerializableContainer.serializer(), supplier = stateKeeper::save)
17 | } else if (!lifecycle.isDestroyed) {
18 | register(key = key, strategy = SerializableContainer.serializer(), supplier = stateKeeper::save)
19 | lifecycle.doOnDestroy { unregister(key) }
20 | }
21 |
22 | return stateKeeper
23 | }
24 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/value/MutableValue.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.value
2 |
3 | /**
4 | * A mutable variant of [Value].
5 | *
6 | * @see Value
7 | */
8 | abstract class MutableValue : Value() {
9 |
10 | /**
11 | * When read - returns the current value.
12 | *
13 | * When assigned - replaces the stored value with the new one, and notifies all registered observers.
14 | *
15 | * @see Value.value
16 | */
17 | abstract override var value: T
18 |
19 | /**
20 | * Atomically compares the current value with [expected] and replaces it with the [new] value if successful.
21 | * The comparison is preformed by reference. Returns `true` if the value was updated, `false` otherwise.
22 | */
23 | abstract fun compareAndSet(expected: T, new: T): Boolean
24 | }
25 |
--------------------------------------------------------------------------------
/decompose/src/commonMain/kotlin/com/arkivanov/decompose/value/Value.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.value
2 |
3 | import com.arkivanov.decompose.Cancellation
4 |
5 | /**
6 | * An observable value holder. Used in Decompose to avoid the direct dependency on coroutines/Reaktive.
7 | * Also, since [Value] is a class (not an interface) with a generic type parameter, it is useful to
8 | * expose state streams to ObjC/Swift.
9 | */
10 | abstract class Value {
11 |
12 | /**
13 | * Returns the current value.
14 | */
15 | abstract val value: T
16 |
17 | /**
18 | * Subscribes the provided [observer] for value updates. The current value is emitted synchronously on subscription.
19 | *
20 | * @return [Cancellation] token to cancel the subscription.
21 | */
22 | abstract fun subscribe(observer: (T) -> Unit): Cancellation
23 | }
24 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/KeyHashStringTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 |
6 | class KeyHashStringTest {
7 |
8 | @Test
9 | fun keyed_keyHashString_returns_distinct_values() {
10 | val configs = listOf(Config.A(id = 1), Config.B(id = 1), Config.A(id = 1))
11 |
12 | val keyStrings =
13 | configs
14 | .keyed { it }
15 | .map { (key, config) -> Child.Destroyed(configuration = config, key = key) }
16 | .map { it.keyHashString() }
17 |
18 | assertEquals(configs.size, keyStrings.distinct().size)
19 | }
20 |
21 | private sealed interface Config {
22 | data class A(val id: Int) : Config
23 | data class B(val id: Int) : Config
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/TestBackCallback.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import com.arkivanov.essenty.backhandler.BackCallback
4 | import kotlin.test.assertEquals
5 |
6 | class TestBackCallback(
7 | isEnabled: Boolean = true,
8 | priority: Int = PRIORITY_DEFAULT,
9 | ) : BackCallback(isEnabled = isEnabled, priority = priority) {
10 |
11 | val events: MutableList = ArrayList()
12 |
13 | override fun onBack() {
14 | events += Event.OnBack
15 | }
16 |
17 | sealed interface Event {
18 | data object OnBack : Event
19 | }
20 | }
21 |
22 | fun TestBackCallback.assertNoEvents() {
23 | assertEvents()
24 | }
25 |
26 | fun TestBackCallback.assertEvents(vararg events: TestBackCallback.Event) {
27 | assertEquals(events.asList(), this.events)
28 | }
29 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/TestBackDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.backhandler
2 |
3 | import com.arkivanov.essenty.backhandler.BackCallback
4 | import com.arkivanov.essenty.backhandler.BackDispatcher
5 |
6 | internal class TestBackDispatcher(
7 | private val delegate: BackDispatcher = BackDispatcher()
8 | ) : BackDispatcher by delegate {
9 |
10 | var size: Int = 0
11 |
12 | override fun register(callback: BackCallback) {
13 | delegate.register(callback)
14 | size++
15 | }
16 |
17 | override fun unregister(callback: BackCallback) {
18 | delegate.unregister(callback)
19 | size--
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/Component.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router
2 |
3 | import com.arkivanov.decompose.ComponentContext
4 |
5 | class Component(
6 | val config: T,
7 | componentContext: ComponentContext,
8 | ) : ComponentContext by componentContext
9 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/TestInstance.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router
2 |
3 | import com.arkivanov.essenty.instancekeeper.InstanceKeeper
4 |
5 | class TestInstance(
6 | var onDestroyed: () -> Unit = {},
7 | ) : InstanceKeeper.Instance {
8 |
9 | var isDestroyed: Boolean = false
10 |
11 | override fun onDestroy() {
12 | isDestroyed = true
13 | onDestroyed()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/TransientNavStateSaverTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.children
2 |
3 | import com.arkivanov.decompose.serializeAndDeserialize
4 | import kotlin.test.Test
5 | import kotlin.test.assertEquals
6 | import kotlin.test.assertNull
7 |
8 | @Suppress("TestFunctionName")
9 | class TransientNavStateSaverTest {
10 |
11 | @Test
12 | fun WHEN_not_persistent_THEN_restores_saved_state() {
13 | val saver = transientNavStateSaver()
14 |
15 | val savedState = saver.saveState(state = 1)?.let(saver::restoreState)
16 |
17 | assertEquals(1, savedState)
18 | }
19 |
20 | @Test
21 | fun WHEN_persistent_THEN_does_not_restore_saved_state() {
22 | val saver = transientNavStateSaver()
23 |
24 | val savedState = saver.saveState(state = 1)?.serializeAndDeserialize()?.let(saver::restoreState)
25 |
26 | assertNull(savedState)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/items/LazyComponentState.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.items
2 |
3 | enum class LazyComponentState {
4 | PENDING,
5 | REMOVED,
6 | DESTROYED,
7 | CREATED,
8 | STARTED,
9 | RESUMED,
10 | }
11 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/panels/TestPanelsNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.panels
2 |
3 | import kotlin.test.assertEquals
4 |
5 | class TestPanelsNavigator(
6 | private var panels: Panels,
7 | ) : PanelsNavigator {
8 |
9 | fun assertPanels(panels: Panels) {
10 | assertEquals(panels, this.panels)
11 | }
12 |
13 | override fun navigate(
14 | transformer: (Panels) -> Panels,
15 | onComplete: (newState: Panels, oldState: Panels) -> Unit,
16 | ) {
17 | val oldPanels = panels
18 | val newPanels = transformer(panels)
19 | panels = newPanels
20 | onComplete(newPanels, oldPanels)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterPopWhileTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 | import kotlin.test.assertTrue
6 |
7 | @Suppress("TestFunctionName")
8 | class RouterPopWhileTest {
9 |
10 | @Test
11 | fun WHEN_popWhile_THEN_popped() {
12 | val navigator = TestStackNavigator(listOf(1, 2, 3, 4))
13 |
14 | navigator.popWhile { it != 2 }
15 |
16 | assertEquals(listOf(1, 2), navigator.configurations)
17 | }
18 |
19 | @Test
20 | fun WHEN_popWhile_THEN_onComplete_called() {
21 | val navigator = TestStackNavigator(listOf(1, 2, 3, 4))
22 | var isCalled = false
23 |
24 | navigator.popWhile(predicate = { it != 2 }, onComplete = { isCalled = true })
25 |
26 | assertTrue(isCalled)
27 | }
28 |
29 | @Test
30 | fun WHEN_popWhile_all_THEN_first_child_not_removed() {
31 | val navigator = TestStackNavigator(listOf(1, 2, 3, 4))
32 |
33 | navigator.popWhile { true }
34 |
35 | assertEquals(listOf(1), navigator.configurations)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterPushTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack
2 |
3 | import com.arkivanov.decompose.DelicateDecomposeApi
4 | import kotlin.test.Test
5 | import kotlin.test.assertEquals
6 | import kotlin.test.assertTrue
7 |
8 | @OptIn(DelicateDecomposeApi::class)
9 | @Suppress("TestFunctionName")
10 | class RouterPushTest {
11 |
12 | @Test
13 | fun WHEN_push_THEN_pushed() {
14 | val navigator = TestStackNavigator(listOf(1, 2))
15 |
16 | navigator.push(3)
17 |
18 | assertEquals(listOf(1, 2, 3), navigator.configurations)
19 | }
20 |
21 | @Test
22 | fun WHEN_push_THEN_onComplete_called() {
23 | val navigator = TestStackNavigator(listOf(1, 2))
24 | var isCalled = false
25 |
26 | navigator.push(3) { isCalled = true }
27 |
28 | assertTrue(isCalled)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterReplaceAllTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 | import kotlin.test.assertTrue
6 |
7 | @Suppress("TestFunctionName")
8 | class RouterReplaceAllTest {
9 |
10 | @Test
11 | fun WHEN_replaceAll_THEN_configurations_correctly_applied() {
12 | val navigator = TestStackNavigator(listOf(1, 2, 3))
13 |
14 | navigator.replaceAll(3, 4, 5)
15 |
16 | assertEquals(listOf(3, 4, 5), navigator.configurations)
17 | }
18 |
19 | @Test
20 | fun WHEN_replace_all_THEN_onComplete_called() {
21 | val navigator = TestStackNavigator(listOf(1, 2, 3))
22 | var isCalled = false
23 |
24 | navigator.replaceAll(4) { isCalled = true }
25 |
26 | assertTrue(isCalled)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterReplaceCurrentTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 | import kotlin.test.assertTrue
6 |
7 | @Suppress("TestFunctionName")
8 | class RouterReplaceCurrentTest {
9 |
10 | @Test
11 | fun WHEN_replaceCurrent_THEN_last_configuration_replaced() {
12 | val navigator = TestStackNavigator(listOf(1, 2, 3))
13 |
14 | navigator.replaceCurrent(4)
15 |
16 | assertEquals(listOf(1, 2, 4), navigator.configurations)
17 | }
18 |
19 | @Test
20 | fun WHEN_replaceCurrent_THEN_onComplete_called() {
21 | val navigator = TestStackNavigator(listOf(1, 2, 3))
22 | var isCalled = false
23 |
24 | navigator.replaceCurrent(4) { isCalled = true }
25 |
26 | assertTrue(isCalled)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/TestStackNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack
2 |
3 | import kotlin.properties.Delegates
4 |
5 | open class TestStackNavigator(
6 | configurations: List,
7 | ) : StackNavigator {
8 |
9 | var configurations: List by Delegates.observable(configurations) { _, _, newValue ->
10 | onConfigurationsChanged(newValue)
11 | }
12 |
13 | protected open fun onConfigurationsChanged(configurations: List) {
14 | }
15 |
16 | override fun navigate(
17 | transformer: (stack: List) -> List,
18 | onComplete: (newStack: List, oldStack: List) -> Unit
19 | ) {
20 | val oldStack = configurations
21 | val newStack = transformer(configurations)
22 | configurations = newStack
23 | onComplete(newStack, oldStack)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/TestStackRouter.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack
2 |
3 | import com.arkivanov.decompose.Child
4 | import com.arkivanov.decompose.value.MutableValue
5 | import com.arkivanov.decompose.value.Value
6 |
7 | class TestStackRouter(stack: List) : TestStackNavigator(stack) {
8 |
9 | private val _stack = MutableValue(stack.toRouterState())
10 | val stack: Value> = _stack
11 |
12 | override fun onConfigurationsChanged(configurations: List) {
13 | super.onConfigurationsChanged(configurations)
14 |
15 | _stack.value = configurations.toRouterState()
16 | }
17 |
18 | private fun List.toRouterState(): ChildStack =
19 | ChildStack(
20 | active = Child.Created(
21 | configuration = last(),
22 | instance = last(),
23 | ),
24 | backStack = dropLast(1).map {
25 | Child.Created(
26 | configuration = it,
27 | instance = it,
28 | )
29 | }
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/decompose/src/darwinMain/kotlin/com/arkivanov/decompose/Lock.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import platform.Foundation.NSRecursiveLock
4 |
5 | internal actual class Lock actual constructor() : NSRecursiveLock() {
6 |
7 | actual inline fun synchronizedImpl(block: () -> T): T {
8 | lock()
9 | try {
10 | return block()
11 | } finally {
12 | unlock()
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/decompose/src/darwinMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.errorhandler
2 |
3 | internal actual fun printError(exception: Exception) {
4 | exception.printStackTrace()
5 | }
6 |
--------------------------------------------------------------------------------
/decompose/src/darwinMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.mainthread
2 |
3 | import com.arkivanov.decompose.errorhandler.onDecomposeError
4 | import platform.Foundation.NSThread
5 |
6 | internal actual fun checkMainThread() {
7 | if (!NSThread.isMainThread()) {
8 | onDecomposeError(NotOnMainThreadException(currentThreadName = NSThread.currentThread.description()))
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/decompose/src/jsMain/kotlin/com/arkivanov/decompose/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import kotlin.reflect.KClass
4 |
5 | internal actual val KClass<*>.uniqueName: String?
6 | get() = js.name
7 |
--------------------------------------------------------------------------------
/decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/webhistory/DefaultBrowserHistory.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.webhistory
2 |
3 | import kotlinx.browser.window
4 |
5 | internal actual object DefaultBrowserHistory : BrowserHistory {
6 |
7 | actual override val state: String? get() = window.history.state?.unsafeCast()
8 |
9 | actual override fun go(delta: Int) {
10 | window.history.go(delta = delta)
11 | }
12 |
13 | actual override fun pushState(data: String?, url: String?) {
14 | window.history.pushState(data = data, title = "", url = url)
15 | }
16 |
17 | actual override fun replaceState(data: String?, url: String?) {
18 | window.history.replaceState(data = data, title = "", url = url)
19 | }
20 |
21 | actual override fun setOnPopStateListener(listener: (state: String?) -> Unit) {
22 | window.onpopstate = { listener(it.state?.unsafeCast()) }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/decompose/src/jsTest/kotlin/com/arkivanov/decompose/TestUtils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | actual fun isNodeJs(): Boolean =
4 | jsTypeOf(kotlinx.browser.window) == "undefined"
5 |
--------------------------------------------------------------------------------
/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/Lock.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | internal actual class Lock actual constructor() {
4 |
5 | actual inline fun synchronizedImpl(block: () -> T): T =
6 | synchronized(this, block)
7 | }
8 |
--------------------------------------------------------------------------------
/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.errorhandler
2 |
3 | internal actual fun printError(exception: Exception) {
4 | exception.printStackTrace()
5 | }
6 |
--------------------------------------------------------------------------------
/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.mainthread
2 |
3 | import com.arkivanov.decompose.errorhandler.onDecomposeError
4 |
5 | internal actual fun checkMainThread() {
6 | if (mainThreadChecker?.isMainThread() == false) {
7 | onDecomposeError(NotOnMainThreadException(currentThreadName = Thread.currentThread().name))
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/MainThreadChecker.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.mainthread
2 |
3 | import com.arkivanov.decompose.InternalDecomposeApi
4 |
5 | @InternalDecomposeApi
6 | interface MainThreadChecker {
7 |
8 | fun isMainThread(): Boolean
9 | }
10 |
--------------------------------------------------------------------------------
/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/MainThreadCheckerProvider.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.mainthread
2 |
3 | import java.util.ServiceLoader
4 |
5 | internal val mainThreadChecker: MainThreadChecker? by lazy {
6 | ServiceLoader.load(MainThreadChecker::class.java).firstOrNull()
7 | }
8 |
--------------------------------------------------------------------------------
/decompose/src/jvmTest/kotlin/com/arkivanov/decompose/RelayThreadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | class RelayThreadingTest : AbstractRelayThreadingTest() {
4 |
5 | override val iterationCount: Int = 100000
6 | }
7 |
--------------------------------------------------------------------------------
/decompose/src/jvmTest/kotlin/com/arkivanov/decompose/value/MutableValueThreadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.value
2 |
3 | class MutableValueThreadingTest : AbstractMutableValueThreadingTest() {
4 |
5 | override val iterationCount: Int = 100000
6 | }
7 |
--------------------------------------------------------------------------------
/decompose/src/jvmTest/kotlin/com/arkivanov/decompose/value/operator/ValueMapThreadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.value.operator
2 |
3 | class ValueMapThreadingTest : AbstractValueMapThreadingTest() {
4 |
5 | override val iterationCount: Int = 100000
6 | }
7 |
--------------------------------------------------------------------------------
/decompose/src/macosArm64Test/kotlin/com/arkivanov/decompose/RelayThreadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | class RelayThreadingTest : AbstractRelayThreadingTest() {
4 |
5 | override val iterationCount: Int = 1000
6 | }
7 |
--------------------------------------------------------------------------------
/decompose/src/macosArm64Test/kotlin/com/arkivanov/decompose/value/MutableValueThreadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.value
2 |
3 | class MutableValueThreadingTest : AbstractMutableValueThreadingTest() {
4 |
5 | override val iterationCount: Int = 1000
6 | }
7 |
--------------------------------------------------------------------------------
/decompose/src/macosArm64Test/kotlin/com/arkivanov/decompose/value/operator/ValueMapThreadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.value.operator
2 |
3 | class ValueMapThreadingTest : AbstractValueMapThreadingTest() {
4 |
5 | override val iterationCount: Int = 1000
6 | }
7 |
--------------------------------------------------------------------------------
/decompose/src/macosX64Test/kotlin/com/arkivanov/decompose/RelayThreadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | class RelayThreadingTest : AbstractRelayThreadingTest() {
4 |
5 | override val iterationCount: Int = 1000
6 | }
7 |
--------------------------------------------------------------------------------
/decompose/src/macosX64Test/kotlin/com/arkivanov/decompose/value/MutableValueThreadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.value
2 |
3 | class MutableValueThreadingTest : AbstractMutableValueThreadingTest() {
4 |
5 | override val iterationCount: Int = 1000
6 | }
7 |
--------------------------------------------------------------------------------
/decompose/src/macosX64Test/kotlin/com/arkivanov/decompose/value/operator/ValueMapThreadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.value.operator
2 |
3 | class ValueMapThreadingTest : AbstractValueMapThreadingTest() {
4 |
5 | override val iterationCount: Int = 1000
6 | }
7 |
--------------------------------------------------------------------------------
/decompose/src/nonWebMain/kotlin/com/arkivanov/decompose/Utils.nonJs.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import kotlin.reflect.KClass
4 |
5 | internal actual val KClass<*>.uniqueName: String?
6 | get() = qualifiedName
7 |
--------------------------------------------------------------------------------
/decompose/src/nonWebTest/kotlin/com/arkivanov/decompose/RelayThreadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 |
6 | abstract class AbstractRelayThreadingTest : AbstractThreadingTest() {
7 |
8 | protected abstract val iterationCount: Int
9 |
10 | @Test
11 | fun emits_all_values_synchronized() {
12 | repeat(10) {
13 | val relay = Relay()
14 |
15 | val values = HashSet()
16 | var count = 0
17 | relay.subscribe {
18 | values += it
19 | count++
20 | }
21 |
22 | race { threadIndex ->
23 | repeat(iterationCount) { index ->
24 | relay.accept(threadIndex * iterationCount + index)
25 | }
26 | }
27 |
28 | val expectedCount = threadCount * iterationCount
29 | assertEquals(expectedCount, values.size)
30 | assertEquals(expectedCount, count)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/decompose/src/wasmJsMain/kotlin/com/arkivanov/decompose/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import kotlin.reflect.KClass
4 |
5 | internal actual val KClass<*>.uniqueName: String?
6 | get() = qualifiedName
7 |
--------------------------------------------------------------------------------
/decompose/src/wasmJsMain/kotlin/com/arkivanov/decompose/router/webhistory/DefaultBrowserHistory.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.webhistory
2 |
3 | import kotlinx.browser.window
4 |
5 | internal actual object DefaultBrowserHistory : BrowserHistory {
6 |
7 | actual override val state: String? get() = window.history.state?.toString()
8 |
9 | actual override fun go(delta: Int) {
10 | window.history.go(delta = delta)
11 | }
12 |
13 | actual override fun pushState(data: String?, url: String?) {
14 | window.history.pushState(data = data?.toJsString(), title = "", url = url)
15 | }
16 |
17 | actual override fun replaceState(data: String?, url: String?) {
18 | window.history.replaceState(data = data?.toJsString(), title = "", url = url)
19 | }
20 |
21 | actual override fun setOnPopStateListener(listener: (state: String?) -> Unit) {
22 | window.onpopstate = { listener(it.state?.toString()) }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/decompose/src/wasmJsTest/kotlin/com/arkivanov/decompose/TestUtils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | actual fun isNodeJs(): Boolean =
4 | false // Decompose doesn't support wasmJs for NodeJs yet
5 |
--------------------------------------------------------------------------------
/decompose/src/webMain/kotlin/com/arkivanov/decompose/Lock.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | internal actual class Lock actual constructor() {
4 |
5 | actual inline fun synchronizedImpl(block: () -> T): T =
6 | block()
7 | }
8 |
--------------------------------------------------------------------------------
/decompose/src/webMain/kotlin/com/arkivanov/decompose/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import com.arkivanov.essenty.statekeeper.SerializableContainer
4 | import kotlinx.serialization.json.Json
5 |
6 | internal val Json =
7 | Json {
8 | allowStructuredMapKeys = true
9 | }
10 |
11 | internal fun SerializableContainer.encodeToJson(): JsonString =
12 | JsonString(Json.encodeToString(SerializableContainer.serializer(), this))
13 |
14 | internal fun JsonString.decodeContainer(): SerializableContainer? =
15 | try {
16 | Json.decodeFromString(SerializableContainer.serializer(), value)
17 | } catch (e: Exception) {
18 | null
19 | }
20 |
21 | internal value class JsonString(val value: String)
22 |
23 | internal external fun encodeURIComponent(str: String): String
24 |
25 | internal external fun decodeURIComponent(str: String): String
26 |
--------------------------------------------------------------------------------
/decompose/src/webMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.errorhandler
2 |
3 | internal actual fun printError(exception: Exception) {
4 | exception.printStackTrace()
5 | }
6 |
--------------------------------------------------------------------------------
/decompose/src/webMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.mainthread
2 |
3 | internal actual fun checkMainThread() {
4 | // No-op
5 | }
6 |
--------------------------------------------------------------------------------
/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/stack/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack
2 |
3 | import com.arkivanov.decompose.Cancellation
4 | import com.arkivanov.decompose.value.Value
5 | import kotlin.math.min
6 |
7 | internal fun List.startsWith(other: List): Boolean {
8 | if (other.size > size) {
9 | return false
10 | }
11 |
12 | for (i in other.indices) {
13 | if (this[i] != other[i]) {
14 | return false
15 | }
16 | }
17 |
18 | return true
19 | }
20 |
21 | internal fun List.findFirstDifferentIndex(other: List): Int {
22 | val minSize = min(size, other.size)
23 |
24 | if (minSize <= 0) {
25 | return -1;
26 | }
27 |
28 | var i = 0;
29 | while ((i < minSize) && (this[i] == other[i])) {
30 | i++
31 | }
32 |
33 | return i
34 | }
35 |
36 | internal fun Value.subscribe(observer: (new: T, old: T) -> Unit): Cancellation {
37 | var old = value
38 |
39 | return subscribe callback@{ new ->
40 | val tmp = old
41 | old = new
42 | observer(new, tmp)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/stack/webhistory/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.stack.webhistory
2 |
3 | import com.arkivanov.decompose.router.stack.ChildStack
4 |
5 | internal fun ChildStack.configurations(): List =
6 | items.map { it.configuration }
7 |
--------------------------------------------------------------------------------
/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/webhistory/BrowserHistory.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.webhistory
2 |
3 | internal interface BrowserHistory {
4 |
5 | val state: String?
6 |
7 | fun go(delta: Int)
8 | fun pushState(data: String?, url: String?)
9 | fun replaceState(data: String?, url: String?)
10 | fun setOnPopStateListener(listener: (state: String?) -> Unit)
11 | }
12 |
--------------------------------------------------------------------------------
/decompose/src/webMain/kotlin/com/arkivanov/decompose/router/webhistory/DefaultBrowserHistory.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.router.webhistory
2 |
3 | internal expect object DefaultBrowserHistory : BrowserHistory {
4 |
5 | override val state: String?
6 |
7 | override fun go(delta: Int)
8 | override fun pushState(data: String?, url: String?)
9 | override fun replaceState(data: String?, url: String?)
10 | override fun setOnPopStateListener(listener: (state: String?) -> Unit)
11 | }
12 |
--------------------------------------------------------------------------------
/decompose/src/webTest/kotlin/com/arkivanov/decompose/TestUtils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | expect fun isNodeJs(): Boolean
4 |
--------------------------------------------------------------------------------
/decompose/src/webTest/kotlin/com/arkivanov/decompose/UrlEncodeTest.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 |
6 | class UrlEncodeTest {
7 |
8 | @Test
9 | fun encode_decode() {
10 | val original = "asd кек"
11 |
12 | val decoded = decodeURIComponent(encodeURIComponent(original))
13 |
14 | assertEquals(original, decoded)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/docs/extensions/overview.md:
--------------------------------------------------------------------------------
1 | # Extensions Overview
2 |
3 | Decompose provides extension modules for various popular libraries and frameworks:
4 |
5 | - [Extensions for Jetpack/JetBrains Compose](compose.md)
6 | - [Extensions for Android views](android.md)
7 |
--------------------------------------------------------------------------------
/docs/getting-started/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Documentation
4 |
5 | All of the documentation is stored in the `docs/` folder of this repository and is all written in markdown. The documentation is generated with [Material MkDocs](https://squidfunk.github.io/mkdocs-material/), so if you want to see what the changes look like locally it is recommended to use [Docker](https://www.docker.com) with the [Material MkDocs docker image](https://squidfunk.github.io/mkdocs-material/getting-started/#with-docker).
6 |
7 | ```bash
8 | # download the image
9 | docker pull squidfunk/mkdocs-material
10 |
11 | # run the server locally
12 | docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material
13 | ```
14 |
15 | Then add the new documentation markdown file into the appropriate folder inside `docs/` and add it to the `mkdocs.yml` file in the project so that it can be navigated to. Put up a pull request for review.
--------------------------------------------------------------------------------
/docs/getting-started/license.md:
--------------------------------------------------------------------------------
1 | --8<-- "LICENSE"
--------------------------------------------------------------------------------
/docs/media/BackGestureAndroid.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/BackGestureAndroid.mp4
--------------------------------------------------------------------------------
/docs/media/BackGestureIos.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/BackGestureIos.mp4
--------------------------------------------------------------------------------
/docs/media/BackGestureMaterial.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/BackGestureMaterial.mp4
--------------------------------------------------------------------------------
/docs/media/ComponentHierarchy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/ComponentHierarchy.png
--------------------------------------------------------------------------------
/docs/media/ComponentStructure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/ComponentStructure.png
--------------------------------------------------------------------------------
/docs/media/ComposeAnimationFade.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/ComposeAnimationFade.gif
--------------------------------------------------------------------------------
/docs/media/ComposeAnimationFadeScale.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/ComposeAnimationFadeScale.gif
--------------------------------------------------------------------------------
/docs/media/ComposeAnimationSeparate.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/ComposeAnimationSeparate.gif
--------------------------------------------------------------------------------
/docs/media/ComposeAnimationSlide.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/ComposeAnimationSlide.gif
--------------------------------------------------------------------------------
/docs/media/DecomposeMeme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/DecomposeMeme.png
--------------------------------------------------------------------------------
/docs/media/LifecycleStates.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/LifecycleStates.png
--------------------------------------------------------------------------------
/docs/media/PluggableUiHierarchy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/PluggableUiHierarchy.png
--------------------------------------------------------------------------------
/docs/media/SampleCardsAndroid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleCardsAndroid.gif
--------------------------------------------------------------------------------
/docs/media/SampleComponentHierarchy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleComponentHierarchy.png
--------------------------------------------------------------------------------
/docs/media/SampleCountersAndroid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleCountersAndroid.gif
--------------------------------------------------------------------------------
/docs/media/SampleCountersDesktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleCountersDesktop.png
--------------------------------------------------------------------------------
/docs/media/SampleCountersIos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleCountersIos.png
--------------------------------------------------------------------------------
/docs/media/SampleCountersWeb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleCountersWeb.png
--------------------------------------------------------------------------------
/docs/media/SampleCustomNavigationDesktop.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleCustomNavigationDesktop.gif
--------------------------------------------------------------------------------
/docs/media/SampleCustomNavigationIos.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleCustomNavigationIos.mp4
--------------------------------------------------------------------------------
/docs/media/SampleGreetingsDemo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleGreetingsDemo.gif
--------------------------------------------------------------------------------
/docs/media/SampleMenuAndroid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleMenuAndroid.gif
--------------------------------------------------------------------------------
/docs/media/SampleMenuAndroid.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleMenuAndroid.mp4
--------------------------------------------------------------------------------
/docs/media/SampleMenuIos.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleMenuIos.gif
--------------------------------------------------------------------------------
/docs/media/SampleMultiPaneDesktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleMultiPaneDesktop.png
--------------------------------------------------------------------------------
/docs/media/SampleMultiPaneDetailsAndroid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleMultiPaneDetailsAndroid.png
--------------------------------------------------------------------------------
/docs/media/SampleMultiPaneIos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleMultiPaneIos.png
--------------------------------------------------------------------------------
/docs/media/SampleMultiPaneListAndroid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleMultiPaneListAndroid.png
--------------------------------------------------------------------------------
/docs/media/SampleMultiPaneWeb.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleMultiPaneWeb.gif
--------------------------------------------------------------------------------
/docs/media/SampleTodoStructure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/SampleTodoStructure.png
--------------------------------------------------------------------------------
/docs/media/logo/circle-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/logo/circle-1.png
--------------------------------------------------------------------------------
/docs/media/logo/circle-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/logo/circle-2.png
--------------------------------------------------------------------------------
/docs/media/logo/logo-titled-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/logo/logo-titled-dark.png
--------------------------------------------------------------------------------
/docs/media/logo/logo-titled.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/logo/logo-titled.psd
--------------------------------------------------------------------------------
/docs/media/logo/logo.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/logo/logo.ai
--------------------------------------------------------------------------------
/docs/media/logo/logo.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/logo/logo.psd
--------------------------------------------------------------------------------
/docs/media/logo/titled-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/logo/titled-1.png
--------------------------------------------------------------------------------
/docs/media/logo/titled-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/logo/titled-2.png
--------------------------------------------------------------------------------
/docs/media/logo/transparent-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/logo/transparent-1.png
--------------------------------------------------------------------------------
/docs/media/logo/transparent-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/docs/media/logo/transparent-2.png
--------------------------------------------------------------------------------
/docs/tips-tricks/overview.md:
--------------------------------------------------------------------------------
1 | # Tips and Tricks
2 |
3 | This section contains various hints, tips and tricks that can be useful when using Decompose.
4 |
5 | - [Calling Composable functions with ViewModels](composable-viewmodel.md)
6 | - [Hosting a component in navigation-compose](navigation-compose-component.md)
7 |
--------------------------------------------------------------------------------
/extensions-android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/extensions-android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.arkivanov.gradle.setupAndroidLibrary
2 | import com.arkivanov.gradle.setupBinaryCompatibilityValidator
3 | import com.arkivanov.gradle.setupPublication
4 |
5 | plugins {
6 | id("com.android.library")
7 | id("kotlin-android")
8 | id("com.arkivanov.gradle.setup")
9 | id("com.dropbox.dependency-guard")
10 | }
11 |
12 | setupAndroidLibrary()
13 | setupPublication()
14 | setupBinaryCompatibilityValidator()
15 |
16 | android {
17 | namespace = "com.arkivanov.decompose.extensions.android"
18 | }
19 |
20 | dependencyGuard {
21 | configuration("releaseRuntimeClasspath")
22 | }
23 |
24 | dependencies {
25 | implementation(project(":decompose"))
26 | }
27 |
--------------------------------------------------------------------------------
/extensions-android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/extensions-android/src/main/java/com/arkivanov/decompose/extensions/android/DefaultViewContext.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.android
2 |
3 | import android.view.ViewGroup
4 | import com.arkivanov.decompose.ExperimentalDecomposeApi
5 | import com.arkivanov.essenty.lifecycle.Lifecycle
6 |
7 | @ExperimentalDecomposeApi
8 | class DefaultViewContext(
9 | override val parent: ViewGroup,
10 | override val lifecycle: Lifecycle
11 | ) : ViewContext
12 |
--------------------------------------------------------------------------------
/extensions-android/src/main/java/com/arkivanov/decompose/extensions/android/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.android
2 |
3 | import android.view.View
4 | import android.view.ViewGroup
5 |
6 | internal inline fun ViewGroup.forEachChild(block: (View) -> Unit) {
7 | for (i in 0 until childCount) {
8 | block(getChildAt(i))
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/extensions-android/src/main/java/com/arkivanov/decompose/extensions/android/ViewContext.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.android
2 |
3 | import android.view.ViewGroup
4 | import com.arkivanov.decompose.ExperimentalDecomposeApi
5 | import com.arkivanov.essenty.lifecycle.LifecycleOwner
6 |
7 | @ExperimentalDecomposeApi
8 | interface ViewContext : LifecycleOwner {
9 |
10 | val parent: ViewGroup
11 | }
12 |
--------------------------------------------------------------------------------
/extensions-android/src/main/java/com/arkivanov/decompose/extensions/android/ViewContextExt.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.android
2 |
3 | import android.content.Context
4 | import android.content.res.Resources
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import com.arkivanov.decompose.ExperimentalDecomposeApi
9 |
10 | @ExperimentalDecomposeApi
11 | val ViewContext.context: Context
12 | get() = parent.context
13 |
14 | @ExperimentalDecomposeApi
15 | val ViewContext.resources: Resources
16 | get() = parent.resources
17 |
18 | @ExperimentalDecomposeApi
19 | val ViewContext.layoutInflater: LayoutInflater
20 | get() = LayoutInflater.from(context)
21 |
22 | @ExperimentalDecomposeApi
23 | fun ViewContext.child(container: ViewGroup, inflater: ViewContext.() -> View) {
24 | val childContext =
25 | DefaultViewContext(
26 | parent = container,
27 | lifecycle = lifecycle
28 | )
29 |
30 | container.addView(childContext.inflater())
31 | }
32 |
--------------------------------------------------------------------------------
/extensions-android/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/extensions-compose-experimental/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.experimental
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 |
6 | @Composable
7 | internal fun rememberLazy(key: Any, provider: () -> T): Lazy =
8 | remember(key) { lazy(provider) }
9 |
--------------------------------------------------------------------------------
/extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/panels/ChildPanelsLayout.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.experimental.panels
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.arkivanov.decompose.ExperimentalDecomposeApi
5 | import com.arkivanov.decompose.router.panels.ChildPanelsMode
6 |
7 | /**
8 | * A Child Panels layout used for laying out panels in single, dual and triple modes.
9 | */
10 | @ExperimentalDecomposeApi
11 | interface ChildPanelsLayout {
12 |
13 | /**
14 | * Lays out the provided `Composable` panels according to the current [mode].
15 | *
16 | * @param mode the current layout mode, see [ChildPanelsMode].
17 | * @param main the Main panel `Composable` function.
18 | * @param details the Details panel `Composable` function.
19 | * @param extra the Extra panel `Composable` function.
20 | */
21 | @Composable
22 | fun Layout(
23 | mode: ChildPanelsMode,
24 | main: @Composable () -> Unit,
25 | details: @Composable () -> Unit,
26 | extra: @Composable () -> Unit,
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/stack/SimpleAnimatedVisibilityScope.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.experimental.stack
2 |
3 | import androidx.compose.animation.AnimatedVisibilityScope
4 | import androidx.compose.animation.EnterExitState
5 | import androidx.compose.animation.core.Transition
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.remember
8 |
9 | @Composable
10 | internal fun WithAnimatedVisibilityScope(
11 | transition: Transition,
12 | block: @Composable AnimatedVisibilityScope.() -> Unit,
13 | ) {
14 | val scope = remember(transition) { SimpleAnimatedVisibilityScope(transition) }
15 | scope.block()
16 | }
17 |
18 | private class SimpleAnimatedVisibilityScope(
19 | override val transition: Transition,
20 | ) : AnimatedVisibilityScope
21 |
--------------------------------------------------------------------------------
/extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/stack/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.experimental.stack
2 |
3 | import com.arkivanov.decompose.router.stack.ChildStack
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.coroutineScope
6 | import kotlinx.coroutines.joinAll
7 | import kotlinx.coroutines.launch
8 |
9 | internal fun ChildStack.dropLast(): ChildStack =
10 | ChildStack(active = backStack.last(), backStack = backStack.dropLast(1))
11 |
12 | internal val ChildStack<*, *>.size: Int get() = items.size
13 |
14 | internal suspend inline fun awaitAll(vararg jobs: suspend CoroutineScope.() -> Unit) {
15 | coroutineScope {
16 | jobs.map { launch(block = it) }.joinAll()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/stack/animation/Fade.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.experimental.stack.animation
2 |
3 | import androidx.compose.animation.core.FiniteAnimationSpec
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.composed
7 | import androidx.compose.ui.draw.alpha
8 | import com.arkivanov.decompose.ExperimentalDecomposeApi
9 | import kotlin.math.abs
10 |
11 | /**
12 | * A simple fading animation. Appearing children's `alpha` is animated from [minAlpha] to 1.0.
13 | * Disappearing children's `alpha` is animated from 1.0 to [minAlpha].
14 | */
15 | @ExperimentalDecomposeApi
16 | fun fade(
17 | animationSpec: FiniteAnimationSpec = tween(),
18 | minAlpha: Float = 0F,
19 | ): StackAnimator =
20 | stackAnimator(animationSpec = animationSpec) { factor, _ ->
21 | Modifier.alpha(getFadeAlpha(factor = factor, minAlpha = minAlpha))
22 | }
23 |
24 | internal fun getFadeAlpha(factor: Float, minAlpha: Float): Float =
25 | (1F - abs(factor) * (1F - minAlpha)).coerceIn(minimumValue = 0F, maximumValue = 1F)
26 |
--------------------------------------------------------------------------------
/extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/stack/animation/Scale.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.experimental.stack.animation
2 |
3 | import androidx.compose.animation.core.FiniteAnimationSpec
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.draw.scale
7 | import com.arkivanov.decompose.ExperimentalDecomposeApi
8 |
9 | /**
10 | * A simple scaling animation. Front (above) children are scaling from [frontFactor] to 1.0.
11 | * Back (below) children are scaling from 1.0 to [backFactor].
12 | */
13 | @ExperimentalDecomposeApi
14 | fun scale(
15 | animationSpec: FiniteAnimationSpec = tween(),
16 | frontFactor: Float = 1.15F,
17 | backFactor: Float = 0.95F,
18 | ): StackAnimator =
19 | stackAnimator(animationSpec = animationSpec) { factor, _ ->
20 | Modifier.scale(
21 | if (factor >= 0F) {
22 | factor * (frontFactor - 1F) + 1F
23 | } else {
24 | factor * (1F - backFactor) + 1F
25 | }
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/stack/animation/StackAnimationProvider.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.experimental.stack.animation
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import com.arkivanov.decompose.ExperimentalDecomposeApi
5 |
6 | @ExperimentalDecomposeApi
7 | interface StackAnimationProvider {
8 | fun provide(): StackAnimation?
9 | }
10 |
11 | @ExperimentalDecomposeApi
12 | val LocalStackAnimationProvider =
13 | compositionLocalOf {
14 | object : StackAnimationProvider {
15 | override fun provide(): StackAnimation? = null
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/extensions-compose/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/extensions-compose/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/SubscribeAsState.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.runtime.SnapshotMutationPolicy
6 | import androidx.compose.runtime.State
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.structuralEqualityPolicy
10 | import com.arkivanov.decompose.value.Value
11 |
12 | /**
13 | * Subscribes to the [Value] and returns Compose [State].
14 | *
15 | * @param policy a [SnapshotMutationPolicy] that will be used by Compose when comparing values.
16 | * Default is [structuralEqualityPolicy].
17 | */
18 | @Composable
19 | fun Value.subscribeAsState(policy: SnapshotMutationPolicy = structuralEqualityPolicy()): State {
20 | val state = remember(this, policy) { mutableStateOf(value, policy) }
21 |
22 | DisposableEffect(this) {
23 | val disposable = subscribe { state.value = it }
24 | onDispose { disposable.cancel() }
25 | }
26 |
27 | return state
28 | }
29 |
--------------------------------------------------------------------------------
/extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/pages/PagesScrollAnimation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.pages
2 |
3 | import androidx.compose.animation.core.AnimationSpec
4 |
5 | sealed interface PagesScrollAnimation {
6 |
7 | data object Disabled : PagesScrollAnimation
8 | data object Default : PagesScrollAnimation
9 | class Custom(val spec: AnimationSpec) : PagesScrollAnimation
10 | }
11 |
--------------------------------------------------------------------------------
/extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/stack/animation/EmptyStackAnimation.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.stack.animation
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.compositionLocalOf
6 | import androidx.compose.ui.Modifier
7 | import com.arkivanov.decompose.Child
8 | import com.arkivanov.decompose.router.stack.ChildStack
9 |
10 | internal fun emptyStackAnimation(): StackAnimation =
11 | EmptyStackAnimation()
12 |
13 | /*
14 | * Can't be anonymous. See:
15 | * https://github.com/JetBrains/compose-jb/issues/2688
16 | * https://github.com/JetBrains/compose-jb/issues/2612
17 | */
18 | private class EmptyStackAnimation : StackAnimation {
19 |
20 | @Composable
21 | override fun invoke(
22 | stack: ChildStack,
23 | modifier: Modifier,
24 | content: @Composable (child: Child.Created) -> Unit,
25 | ) {
26 | Box(modifier = modifier) {
27 | content(stack.active)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/stack/animation/EmptyStackAnimator.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.stack.animation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.ui.Modifier
6 |
7 | internal object EmptyStackAnimator : StackAnimator {
8 |
9 | @Composable
10 | override fun invoke(
11 | direction: Direction,
12 | isInitial: Boolean,
13 | onFinished: () -> Unit,
14 | content: @Composable (Modifier) -> Unit,
15 | ) {
16 | content(Modifier)
17 |
18 | DisposableEffect(direction, isInitial) {
19 | onFinished()
20 | onDispose {}
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/stack/animation/Fade.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.stack.animation
2 |
3 | import androidx.compose.animation.core.FiniteAnimationSpec
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.draw.alpha
7 | import kotlin.math.abs
8 |
9 | /**
10 | * A simple fading animation. Appearing children's `alpha` is animated from [minAlpha] to 1.0.
11 | * Disappearing children's `alpha` is animated from 1.0 to [minAlpha].
12 | */
13 | fun fade(
14 | animationSpec: FiniteAnimationSpec = tween(),
15 | minAlpha: Float = 0F,
16 | ): StackAnimator =
17 | stackAnimator(animationSpec = animationSpec) { factor, _, content ->
18 | content(Modifier.alpha(getFadeAlpha(factor = factor, minAlpha = minAlpha)))
19 | }
20 |
21 | internal fun getFadeAlpha(factor: Float, minAlpha: Float): Float =
22 | (1F - abs(factor) * (1F - minAlpha)).coerceIn(minimumValue = 0F, maximumValue = 1F)
23 |
--------------------------------------------------------------------------------
/extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/stack/animation/Scale.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.stack.animation
2 |
3 | import androidx.compose.animation.core.FiniteAnimationSpec
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.draw.scale
7 |
8 | /**
9 | * A simple scaling animation. Front (above) children are scaling from [frontFactor] to 1.0.
10 | * Back (below) children are scaling from 1.0 to [backFactor].
11 | */
12 | fun scale(
13 | animationSpec: FiniteAnimationSpec = tween(),
14 | frontFactor: Float = 1.15F,
15 | backFactor: Float = 0.95F,
16 | ): StackAnimator =
17 | stackAnimator(animationSpec = animationSpec) { factor, _, content ->
18 | content(
19 | Modifier.scale(
20 | if (factor >= 0F) {
21 | factor * (frontFactor - 1F) + 1F
22 | } else {
23 | factor * (1F - backFactor) + 1F
24 | }
25 | )
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/stack/animation/StackAnimationProvider.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.stack.animation
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 |
5 | interface StackAnimationProvider {
6 | fun provide(): StackAnimation?
7 | }
8 |
9 | val LocalStackAnimationProvider = compositionLocalOf {
10 | object : StackAnimationProvider {
11 | override fun provide(): StackAnimation? = null
12 | }
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/stack/animation/predictiveback/BackGestureHandler.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import com.arkivanov.essenty.backhandler.BackCallback
6 | import com.arkivanov.essenty.backhandler.BackEvent
7 | import com.arkivanov.essenty.backhandler.BackHandler
8 |
9 | @Composable
10 | internal fun BackGestureHandler(
11 | backHandler: BackHandler,
12 | onBackStarted: (BackEvent) -> Unit = {},
13 | onBackProgressed: (BackEvent) -> Unit = {},
14 | onBackCancelled: () -> Unit = {},
15 | onBack: () -> Unit,
16 | ) {
17 | DisposableEffect(backHandler) {
18 | val callback =
19 | BackCallback(
20 | onBackStarted = onBackStarted,
21 | onBackProgressed = onBackProgressed,
22 | onBackCancelled = onBackCancelled,
23 | onBack = onBack,
24 | )
25 |
26 | backHandler.register(callback)
27 | onDispose { backHandler.unregister(callback) }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/utils/InputConsumingOverlay.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.utils
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.input.pointer.pointerInput
7 |
8 | @Composable
9 | internal fun InputConsumingOverlay(modifier: Modifier) {
10 | Box(
11 | modifier = modifier.pointerInput(Unit) {
12 | awaitPointerEventScope {
13 | while (true) {
14 | val event = awaitPointerEvent()
15 | event.changes.forEach { it.consume() }
16 | }
17 | }
18 | }
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/extensions-compose/src/jvmMain/kotlin/com/arkivanov/decompose/extensions/compose/mainthread/SwingMainThreadChecker.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.mainthread
2 |
3 | import com.arkivanov.decompose.mainthread.MainThreadChecker
4 | import javax.swing.SwingUtilities
5 |
6 | internal class SwingMainThreadChecker : MainThreadChecker {
7 |
8 | override fun isMainThread(): Boolean =
9 | SwingUtilities.isEventDispatchThread()
10 | }
11 |
--------------------------------------------------------------------------------
/extensions-compose/src/jvmMain/resources/META-INF/services/com.arkivanov.decompose.mainthread.MainThreadChecker:
--------------------------------------------------------------------------------
1 | com.arkivanov.decompose.extensions.compose.mainthread.SwingMainThreadChecker
2 |
--------------------------------------------------------------------------------
/extensions-compose/src/jvmTest/kotlin/com/arkivanov/decompose/extensions/compose/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose
2 |
3 | internal fun List.takeSorted(comparator: Comparator): List =
4 | takeWhileIndexed { index, item ->
5 | (index == 0) || (comparator.compare(item, get(index - 1)) >= 0)
6 | }
7 |
8 | internal fun Iterable.takeWhileIndexed(predicate: (Int, T) -> Boolean): List =
9 | withIndex()
10 | .takeWhile { (index, item) -> predicate(index, item) }
11 | .map { it.value }
12 |
--------------------------------------------------------------------------------
/extensions-compose/src/nonAndroidMain/kotlin/com/arkivanov/decompose/extensions/compose/stack/animation/predictiveback/LayoutCorners.nonAndroid.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 |
6 | @Composable
7 | internal actual fun Modifier.withLayoutCorners(block: @Composable Modifier.(LayoutCorners) -> Modifier): Modifier =
8 | block(LayoutCorners())
9 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | android.useAndroidX=true
3 | android.enableJetifier=false
4 | kotlin.code.style=official
5 | kotlin.native.disableCompilerDaemon=true
6 | org.gradle.parallel=true
7 | org.gradle.caching=true
8 | systemProp.org.gradle.internal.publish.checksums.insecure=true
9 | kotlin.mpp.androidSourceSetLayoutVersion=2
10 | kotlin.mpp.applyDefaultHierarchyTemplate=false
11 | org.jetbrains.compose.experimental.macos.enabled=true
12 | org.jetbrains.compose.experimental.jscanvas.enabled=true
13 | org.jetbrains.compose.experimental.wasm.enabled=true
14 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=89d4e70e4e84e2d2dfbb63e4daa53e21b25017cc70c37e4eea31ee51fb15098a
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/sample/app-android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/app-android/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -dontshrink
24 | -dontobfuscate
25 | -dontoptimize
26 |
--------------------------------------------------------------------------------
/sample/app-android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/layout/main_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-android/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-android/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-android/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-android/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-android/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-android/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/sample/app-android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Decompose Counter
3 |
4 |
--------------------------------------------------------------------------------
/sample/app-desktop/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | saved_state.dat
3 |
--------------------------------------------------------------------------------
/sample/app-desktop/src/jvmMain/kotlin/com/arkivanov/sample/app/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.app
2 |
3 | import javax.swing.SwingUtilities
4 |
5 | internal fun runOnUiThread(block: () -> T): T {
6 | if (SwingUtilities.isEventDispatchThread()) {
7 | return block()
8 | }
9 |
10 | var error: Throwable? = null
11 | var result: T? = null
12 |
13 | SwingUtilities.invokeAndWait {
14 | try {
15 | result = block()
16 | } catch (e: Throwable) {
17 | error = e
18 | }
19 | }
20 |
21 | error?.also { throw it }
22 |
23 | @Suppress("UNCHECKED_CAST")
24 | return result as T
25 | }
26 |
--------------------------------------------------------------------------------
/sample/app-ios-compose/.gitignore:
--------------------------------------------------------------------------------
1 | DerivedData/
2 | *.pbxuser
3 | !default.pbxuser
4 | *.mode1v3
5 | !default.mode1v3
6 | *.mode2v3
7 | !default.mode2v3
8 | *.perspectivev3
9 | !default.perspectivev3
10 | xcuserdata/
11 | *.moved-aside
12 | *.xccheckout
13 | *.xcscmblueprint
14 | *.hmap
15 | *.ipa
16 | *.dSYM.zip
17 | *.dSYM
18 | Pods
19 |
--------------------------------------------------------------------------------
/sample/app-ios-compose/app-ios-compose.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sample/app-ios-compose/app-ios-compose.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sample/app-ios-compose/app-ios-compose.xcodeproj/project.xcworkspace/xcuserdata/arkivanov.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-ios-compose/app-ios-compose.xcodeproj/project.xcworkspace/xcuserdata/arkivanov.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/sample/app-ios-compose/app-ios-compose.xcodeproj/xcuserdata/arkivanov.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | app-ios-compose.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/sample/app-ios-compose/app-ios-compose/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/sample/app-ios-compose/app-ios-compose/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/sample/app-ios-compose/app-ios-compose/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sample/app-ios-compose/app-ios-compose/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/sample/app-ios-compose/app-ios-compose/RootView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | struct RootView: UIViewControllerRepresentable {
5 | let root: RootComponent
6 | let backDispatcher: BackDispatcher
7 |
8 | func makeUIViewController(context: Context) -> UIViewController {
9 | let controller = RootViewControllerKt.rootViewController(root: root, backDispatcher: backDispatcher)
10 | controller.overrideUserInterfaceStyle = .light
11 | return controller
12 | }
13 |
14 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/sample/app-ios-compose/app-ios-compose/app_ios_compose.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/sample/app-ios/.gitignore:
--------------------------------------------------------------------------------
1 | DerivedData/
2 | *.pbxuser
3 | !default.pbxuser
4 | *.mode1v3
5 | !default.mode1v3
6 | *.mode2v3
7 | !default.mode2v3
8 | *.perspectivev3
9 | !default.perspectivev3
10 | xcuserdata/
11 | *.moved-aside
12 | *.xccheckout
13 | *.xcscmblueprint
14 | *.hmap
15 | *.ipa
16 | *.dSYM.zip
17 | *.dSYM
18 | Pods
19 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/cat1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "cat1.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/cat1.imageset/cat1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-ios/app-ios/Assets.xcassets/cat1.imageset/cat1.jpg
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/cat2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "cat2.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/cat2.imageset/cat2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-ios/app-ios/Assets.xcassets/cat2.imageset/cat2.jpg
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/cat3.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "cat3.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/cat3.imageset/cat3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-ios/app-ios/Assets.xcassets/cat3.imageset/cat3.jpg
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/cat4.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "cat4.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/cat4.imageset/cat4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-ios/app-ios/Assets.xcassets/cat4.imageset/cat4.jpg
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/cat5.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "cat5.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Assets.xcassets/cat5.imageset/cat5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/app-ios/app-ios/Assets.xcassets/cat5.imageset/cat5.jpg
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/CountersView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CountersView.swift
3 | // app-ios
4 | //
5 | // Created by Arkadii Ivanov on 13/05/2022.
6 | //
7 |
8 | import SwiftUI
9 | import Shared
10 |
11 | struct CountersView: View {
12 | private let counters: CountersComponent
13 |
14 | @StateValue
15 | private var stack: ChildStack
16 |
17 | private var activeChild: ChildCreated { stack.active }
18 |
19 | init(_ counters: CountersComponent) {
20 | self.counters = counters
21 | _stack = StateValue(counters.stack)
22 | }
23 |
24 | var body: some View {
25 | CounterView(activeChild.instance)
26 | .id(activeChild.configuration.hash)
27 | }
28 | }
29 |
30 | struct CountersView_Previews: PreviewProvider {
31 | static var previews: some View {
32 | CountersView(PreviewCountersComponent())
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/DecomposeHelpers/MutableValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MutableValue.swift
3 | // app-ios
4 | //
5 | // Created by Arkadii Ivanov on 13/05/2022.
6 | //
7 |
8 | import Shared
9 |
10 | func mutableValue(_ initialValue: T) -> MutableValue {
11 | return MutableValueBuilderKt.MutableValue(initialValue: initialValue) as! MutableValue
12 | }
13 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/DecomposeHelpers/ObservableValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObservableValue.swift
3 | // app-ios
4 | //
5 | // Created by Arkadii Ivanov on 13/05/2022.
6 | //
7 |
8 | import SwiftUI
9 | import Shared
10 |
11 | public class ObservableValue : ObservableObject {
12 | @Published
13 | var value: T
14 |
15 | private var cancellation: Cancellation?
16 |
17 | init(_ value: Value) {
18 | self.value = value.value
19 | self.cancellation = value.subscribe { [weak self] value in self?.value = value }
20 | }
21 |
22 | deinit {
23 | cancellation?.cancel()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/DecomposeHelpers/SimpleChildStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SimpleRouterState.swift
3 | // app-ios
4 | //
5 | // Created by Arkadii Ivanov on 13/05/2022.
6 | //
7 |
8 | import Shared
9 |
10 | func simpleChildStack(_ child: T) -> Value> {
11 | return mutableValue(
12 | ChildStack(
13 | configuration: "config" as AnyObject,
14 | instance: child
15 | )
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/DecomposeHelpers/StateValue.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | @propertyWrapper struct StateValue: DynamicProperty {
5 | @ObservedObject
6 | private var obj: ObservableValue
7 |
8 | var wrappedValue: T { obj.value }
9 |
10 | init(_ value: Value) {
11 | obj = ObservableValue(value)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/MenuView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | struct MenuView: View {
5 | private let component: MenuComponent
6 |
7 | init(_ component: MenuComponent) {
8 | self.component = component
9 | }
10 |
11 | var body: some View {
12 | VStack {
13 | Button(action: component.onCustomNavigationItemSelected) {
14 | Text("Custom Navigation")
15 | }
16 | }
17 | }
18 | }
19 |
20 | struct MenuView_Previews: PreviewProvider {
21 | static var previews: some View {
22 | MenuView(PreviewMenuComponent())
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/sample/app-ios/app-ios/RootView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RootView.swift
3 | // app-ios
4 | //
5 | // Created by Arkadii Ivanov on 13/05/2022.
6 | //
7 |
8 | import SwiftUI
9 | import Shared
10 |
11 | struct RootView: View {
12 | private let root: RootComponent
13 |
14 | init(_ root: RootComponent) {
15 | self.root = root
16 | }
17 |
18 | var body: some View {
19 | StackView(
20 | stackValue: StateValue(root.stack),
21 | getTitle: { _ in "Heh" },
22 | onBack: root.onBackClicked
23 | ) { child in
24 | switch child {
25 | case let child as TabsChild: TabsView(child.component)
26 | case let child as CustomNavigationChild: CustomNavigationView(child.component)
27 | default: EmptyView()
28 | }
29 | }
30 | }
31 | }
32 |
33 | private typealias TabsChild = RootComponentChild.TabsChild
34 | private typealias CustomNavigationChild = RootComponentChild.CustomNavigationChild
35 |
36 | struct RootView_Previews: PreviewProvider {
37 | static var previews: some View {
38 | RootView(PreviewRootComponent())
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/sample/app-js-compose/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/app-js-compose/src/jsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Title
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/sample/app-js-compose/webpack.config.d/devServerConfig.js:
--------------------------------------------------------------------------------
1 | config.devServer = {
2 | ...config.devServer, // Merge with other devServer settings
3 | "historyApiFallback": true
4 | };
5 |
--------------------------------------------------------------------------------
/sample/app-js/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/app-js/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.arkivanov.gradle.setupJsApp
2 |
3 | plugins {
4 | id("org.jetbrains.kotlin.js")
5 | id("com.arkivanov.gradle.setup")
6 | }
7 |
8 | setupJsApp()
9 |
10 | dependencies {
11 | implementation(project(":decompose"))
12 | implementation(project(":sample:shared:shared"))
13 | implementation(project.dependencies.enforcedPlatform(deps.jetbrains.kotlinWrappers.kotlinWrappersBom.get()))
14 | implementation("org.jetbrains.kotlin-wrappers:kotlin-react")
15 | implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom")
16 | implementation("org.jetbrains.kotlin-wrappers:kotlin-styled")
17 | implementation("org.jetbrains.kotlin-wrappers:kotlin-css-js")
18 | }
19 |
--------------------------------------------------------------------------------
/sample/app-js/src/main/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Decompose Counter sample
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/sample/app-js/webpack.config.d/devServerConfig.js:
--------------------------------------------------------------------------------
1 | config.devServer = {
2 | ...config.devServer, // Merge with other devServer settings
3 | "historyApiFallback": true
4 | };
5 |
--------------------------------------------------------------------------------
/sample/shared/compose/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/shared/compose/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/androidMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/FeatureContentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.dynamicfeatures
2 |
3 | @Suppress("UNCHECKED_CAST")
4 | internal inline fun featureContent(): DynamicFeatureContent =
5 | Class.forName("${T::class.java.name}Content").newInstance() as DynamicFeatureContent
6 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/androidMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1ContentFactoryAndroid.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1
2 |
3 | import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeatureContent
4 | import com.arkivanov.sample.shared.dynamicfeatures.featureContent
5 |
6 | internal actual fun feature1Content(): DynamicFeatureContent = featureContent()
7 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/androidMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2ContentFactoryAndroid.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2
2 |
3 | import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeatureContent
4 | import com.arkivanov.sample.shared.dynamicfeatures.featureContent
5 |
6 | internal actual fun feature2Content(): DynamicFeatureContent = featureContent()
7 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/commonMain/composeResources/drawable/cat1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/shared/compose/src/commonMain/composeResources/drawable/cat1.jpg
--------------------------------------------------------------------------------
/sample/shared/compose/src/commonMain/composeResources/drawable/cat2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/shared/compose/src/commonMain/composeResources/drawable/cat2.jpg
--------------------------------------------------------------------------------
/sample/shared/compose/src/commonMain/composeResources/drawable/cat3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/shared/compose/src/commonMain/composeResources/drawable/cat3.jpg
--------------------------------------------------------------------------------
/sample/shared/compose/src/commonMain/composeResources/drawable/cat4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/shared/compose/src/commonMain/composeResources/drawable/cat4.jpg
--------------------------------------------------------------------------------
/sample/shared/compose/src/commonMain/composeResources/drawable/cat5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arkivanov/Decompose/18dbdccb3b4336e88fc794dd24800ef93cb170df/sample/shared/compose/src/commonMain/composeResources/drawable/cat5.jpg
--------------------------------------------------------------------------------
/sample/shared/compose/src/commonMain/kotlin/androidx/compose/desktop/ui/tooling/preview/Preview.kt:
--------------------------------------------------------------------------------
1 | package androidx.compose.desktop.ui.tooling.preview
2 |
3 | @OptIn(ExperimentalMultiplatform::class)
4 | @OptionalExpectation
5 | expect annotation class Preview()
6 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/dialog/DialogContent.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.dialog
2 |
3 | import androidx.compose.foundation.layout.widthIn
4 | import androidx.compose.material.AlertDialog
5 | import androidx.compose.material.Text
6 | import androidx.compose.material.TextButton
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.dp
10 |
11 | @Composable
12 | internal fun DialogContent(dialogComponent: DialogComponent) {
13 | AlertDialog(
14 | onDismissRequest = {
15 | dialogComponent.onDismissClicked()
16 | },
17 | title = {
18 | Text(text = dialogComponent.title)
19 | },
20 | text = {
21 | Text(text = dialogComponent.message)
22 | },
23 | confirmButton = {
24 | TextButton(
25 | onClick = {
26 | dialogComponent.onDismissClicked()
27 | }
28 | ) {
29 | Text("Dismiss")
30 | }
31 | },
32 | modifier = Modifier.widthIn(min = 200.dp),
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1ContentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1
2 |
3 | import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeatureContent
4 |
5 | internal expect fun feature1Content(): DynamicFeatureContent
6 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2ContentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2
2 |
3 | import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeatureContent
4 |
5 | internal expect fun feature2Content(): DynamicFeatureContent
6 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.utils
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.platform.LocalDensity
5 | import androidx.compose.ui.unit.Dp
6 |
7 | @Composable
8 | internal fun Dp.toPx(): Float =
9 | with(LocalDensity.current) { toPx() }
10 |
11 | @Composable
12 | internal expect fun WebDocumentTitle(title: String)
13 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/iosMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1ContentFactoryIos.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeatureContent
7 |
8 | internal actual fun feature1Content(): DynamicFeatureContent =
9 | object : DynamicFeatureContent {
10 | @Composable
11 | override fun invoke(component: Feature1, modifier: Modifier) {
12 | Text(text = "Not implemented", modifier = modifier)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/iosMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2ContentFactoryIos.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeatureContent
7 |
8 | internal actual fun feature2Content(): DynamicFeatureContent =
9 | object : DynamicFeatureContent {
10 | @Composable
11 | override fun invoke(component: Feature2, modifier: Modifier) {
12 | Text(text = "Not implemented", modifier = modifier)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/jsMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1ContentFactoryIos.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeatureContent
7 |
8 | internal actual fun feature1Content(): DynamicFeatureContent =
9 | object : DynamicFeatureContent {
10 | @Composable
11 | override fun invoke(component: Feature1, modifier: Modifier) {
12 | Text(text = "Not implemented", modifier = modifier)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/shared/compose/src/jsMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2ContentFactoryIos.kt:
--------------------------------------------------------------------------------
1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeatureContent
7 |
8 | internal actual fun feature2Content(): DynamicFeatureContent