├── .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 = 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/utils/Utils.js.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import kotlinx.browser.document 6 | 7 | @Composable 8 | internal actual fun WebDocumentTitle(title: String) { 9 | DisposableEffect(Unit) { 10 | document.title = title 11 | onDispose {} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sample/shared/compose/src/jvmMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1ContentFactoryJvm.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1 2 | 3 | import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeatureContent 4 | 5 | internal actual fun feature1Content(): DynamicFeatureContent = Feature1Content() 6 | -------------------------------------------------------------------------------- /sample/shared/compose/src/jvmMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2ContentFactoryJvm.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2 2 | 3 | import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeatureContent 4 | 5 | internal actual fun feature2Content(): DynamicFeatureContent = Feature2Content() 6 | -------------------------------------------------------------------------------- /sample/shared/compose/src/jvmTest/kotlin/com/arkivanov/sample/shared/counters/counter/TestCounterComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.counters.counter 2 | 3 | import com.arkivanov.decompose.router.slot.ChildSlot 4 | import com.arkivanov.decompose.value.MutableValue 5 | import com.arkivanov.sample.shared.counters.counter.CounterComponent.Model 6 | import com.arkivanov.sample.shared.dialog.DialogComponent 7 | 8 | class TestCounterComponent(model: Model = Model()) : CounterComponent { 9 | 10 | override val model: MutableValue = MutableValue(model) 11 | override val dialogSlot: MutableValue> = MutableValue(ChildSlot()) 12 | 13 | var isNextClicked: Boolean = false 14 | var isPrevClicked: Boolean = false 15 | 16 | override fun onInfoClicked() {} 17 | 18 | override fun onNextClicked() { 19 | isNextClicked = true 20 | } 21 | 22 | override fun onPrevClicked() { 23 | isPrevClicked = true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/shared/compose/src/nonWebMain/kotlin/com/arkivanov/sample/shared/utils/Utils.nonWeb.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | internal actual fun WebDocumentTitle(title: String) { 7 | // no-op 8 | } 9 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/api/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/shared/dynamic-features/api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.iosCompat 2 | import com.arkivanov.gradle.setupMultiplatform 3 | 4 | plugins { 5 | id("kotlin-multiplatform") 6 | id("com.android.library") 7 | id("com.arkivanov.gradle.setup") 8 | } 9 | 10 | setupMultiplatform { 11 | androidTarget() 12 | jvm() 13 | js { browser() } 14 | iosCompat() 15 | } 16 | 17 | android { 18 | namespace = "com.arkivanov.sample.shared.dynamicfeatures.api" 19 | } 20 | 21 | kotlin { 22 | sourceSets { 23 | commonMain { 24 | dependencies { 25 | implementation(project(":decompose")) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/api/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/api/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1 2 | 3 | import com.arkivanov.decompose.value.Value 4 | 5 | interface Feature1 { 6 | 7 | val models: Value 8 | 9 | fun onFeature2Clicked() 10 | 11 | data class Model( 12 | val title: String, 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/api/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2 2 | 3 | import com.arkivanov.decompose.value.Value 4 | 5 | interface Feature2 { 6 | 7 | val models: Value 8 | 9 | fun onCloseClicked() 10 | 11 | data class Model( 12 | val title: String, 13 | val text: String, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/compose-api/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/shared/dynamic-features/compose-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.iosCompat 2 | import com.arkivanov.gradle.setupMultiplatform 3 | import com.arkivanov.gradle.setupSourceSets 4 | 5 | plugins { 6 | id("kotlin-multiplatform") 7 | id("com.android.library") 8 | id("org.jetbrains.compose") 9 | id("org.jetbrains.kotlin.plugin.compose") 10 | id("com.arkivanov.gradle.setup") 11 | } 12 | 13 | setupMultiplatform { 14 | androidTarget() 15 | jvm() 16 | iosCompat() 17 | js { browser() } 18 | } 19 | 20 | android { 21 | namespace = "com.arkivanov.sample.shared.dynamicfeatures.composeapi" 22 | } 23 | 24 | kotlin { 25 | setupSourceSets { 26 | common.main.dependencies { 27 | implementation(project(":sample:shared:dynamic-features:api")) 28 | implementation(compose.runtime) 29 | implementation(compose.ui) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/compose-api/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/compose-api/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/DynamicFeatureContent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | 6 | interface DynamicFeatureContent { 7 | 8 | @Composable 9 | operator fun invoke(component: T, modifier: Modifier) 10 | } 11 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/feature1Impl/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/shared/dynamic-features/feature1Impl/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/feature1Impl/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Feature 1 5 | 6 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/feature1Impl/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1Component.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.value.MutableValue 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.sample.shared.dynamicfeatures.feature1.Feature1.Model 7 | 8 | class Feature1Component( 9 | componentContext: ComponentContext, 10 | private val onFeature2: () -> Unit, 11 | ) : Feature1, ComponentContext by componentContext { 12 | 13 | override val models: Value = 14 | MutableValue( 15 | Model( 16 | title = "Hello from Feature1!", 17 | ) 18 | ) 19 | 20 | override fun onFeature2Clicked() { 21 | onFeature2() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/feature2Impl/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/shared/dynamic-features/feature2Impl/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/feature2Impl/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Feature 2 5 | 6 | -------------------------------------------------------------------------------- /sample/shared/dynamic-features/feature2Impl/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2Component.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.value.MutableValue 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.sample.shared.dynamicfeatures.feature2.Feature2.Model 7 | 8 | class Feature2Component( 9 | componentContext: ComponentContext, 10 | magicNumber: Int, 11 | private val onFinished: () -> Unit, 12 | ) : Feature2, ComponentContext by componentContext { 13 | 14 | override val models: Value = 15 | MutableValue( 16 | Model( 17 | title = "Hello from Feature2!", 18 | text = "Magic number: $magicNumber", 19 | ) 20 | ) 21 | 22 | override fun onCloseClicked() { 23 | onFinished() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/shared/shared/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/kotlin/com/arkivanov/sample/shared/counters/CountersView.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.counters 2 | 3 | import android.view.View 4 | import com.arkivanov.decompose.ExperimentalDecomposeApi 5 | import com.arkivanov.decompose.extensions.android.ViewContext 6 | import com.arkivanov.decompose.extensions.android.layoutInflater 7 | import com.arkivanov.decompose.extensions.android.stack.StackRouterView 8 | import com.arkivanov.sample.shared.R 9 | import com.arkivanov.sample.shared.counters.counter.CounterView 10 | import com.arkivanov.sample.shared.viewSwitcher 11 | 12 | @ExperimentalDecomposeApi 13 | @Suppress("FunctionName") // Factory function 14 | internal fun ViewContext.CountersView(component: CountersComponent): View { 15 | val layout = layoutInflater.inflate(R.layout.counters, parent, false) 16 | val router: StackRouterView = layout.findViewById(R.id.router) 17 | 18 | router.children(component.stack, lifecycle, viewSwitcher(viewFactory = ::CounterView)) 19 | 20 | return layout 21 | } 22 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/FeatureFactory.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures 2 | 3 | internal inline fun feature(vararg args: Any?): T = 4 | Class.forName("${T::class.java.name}Component") 5 | .declaredConstructors 6 | .first() 7 | .newInstance(*args) as T 8 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1FactoryAndroid.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.sample.shared.dynamicfeatures.feature 5 | 6 | internal actual fun Feature1( 7 | componentContext: ComponentContext, 8 | onFeature2: () -> Unit, 9 | ): Feature1 = 10 | feature(componentContext, onFeature2) 11 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2FactoryAndroid.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.sample.shared.dynamicfeatures.feature 5 | 6 | internal actual fun Feature2( 7 | componentContext: ComponentContext, 8 | magicNumber: Int, 9 | onFinished: () -> Unit, 10 | ): Feature2 = 11 | feature(componentContext, magicNumber, onFinished) 12 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/kotlin/com/arkivanov/sample/shared/root/NotImplementedView.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.root 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.View 5 | import com.arkivanov.decompose.ExperimentalDecomposeApi 6 | import com.arkivanov.decompose.extensions.android.ViewContext 7 | import com.arkivanov.decompose.extensions.android.layoutInflater 8 | import com.arkivanov.sample.shared.R 9 | import com.google.android.material.appbar.MaterialToolbar 10 | 11 | @SuppressLint("SetTextI18n") 12 | @ExperimentalDecomposeApi 13 | @Suppress("FunctionName") // Factory function 14 | internal fun ViewContext.NotImplementedView(title: String): View { 15 | val layout = layoutInflater.inflate(R.layout.not_implemented, parent, false) 16 | layout.findViewById(R.id.toolbar).title = title 17 | 18 | return layout 19 | } 20 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/res/drawable/ic_tab_cards.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/res/drawable/ic_tab_counters.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/res/drawable/ic_tab_menu.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/res/drawable/ic_tab_multipane.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/res/layout/root.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/res/layout/tabs.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /sample/shared/shared/src/androidMain/res/menu/root_tabs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | 17 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/ImageResourceId.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | enum class ImageResourceId { 7 | CAT_1, 8 | CAT_2, 9 | CAT_3, 10 | CAT_4, 11 | CAT_5, 12 | } 13 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/Json.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | internal val json = 6 | Json { 7 | allowStructuredMapKeys = true 8 | } 9 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/PreviewComponentContext.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.DefaultComponentContext 5 | import com.arkivanov.essenty.lifecycle.LifecycleRegistry 6 | 7 | internal object PreviewComponentContext : ComponentContext by DefaultComponentContext( 8 | lifecycle = LifecycleRegistry(), 9 | ) 10 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared 2 | 3 | import com.arkivanov.decompose.value.Value 4 | import com.badoo.reaktive.base.setCancellable 5 | import com.badoo.reaktive.observable.Observable 6 | import com.badoo.reaktive.observable.observable 7 | import com.badoo.reaktive.subject.behavior.BehaviorObservable 8 | 9 | internal fun Value.asObservable(): BehaviorObservable = 10 | object : BehaviorObservable, Observable by asObservableInternal() { 11 | override val value: T get() = this@asObservable.value 12 | } 13 | 14 | private fun Value.asObservableInternal(): Observable = 15 | observable { emitter -> 16 | val cancellation = subscribe(emitter::onNext) 17 | emitter.setCancellable(cancellation::cancel) 18 | } 19 | 20 | internal fun String.snakeCase(): String = 21 | buildString { 22 | for (c in this@snakeCase) { 23 | if (c.isUpperCase() && isNotEmpty()) { 24 | append('_') 25 | } 26 | 27 | append(c.lowercaseChar()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/CardsComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.cards 2 | 3 | import com.arkivanov.decompose.router.stack.ChildStack 4 | import com.arkivanov.decompose.value.Value 5 | import com.arkivanov.sample.shared.cards.card.CardComponent 6 | 7 | interface CardsComponent { 8 | 9 | val stack: Value> 10 | 11 | fun onCardSwiped(index: Int) 12 | fun onAddClicked() 13 | fun onRemoveClicked() 14 | } 15 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/card/CardComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.cards.card 2 | 3 | import com.arkivanov.decompose.value.Value 4 | 5 | interface CardComponent { 6 | 7 | val model: Value 8 | 9 | data class Model( 10 | val color: Long, 11 | val title: String, 12 | val status: String = "", 13 | val text: String = "", 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/card/PreviewCardComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.cards.card 2 | 3 | import com.arkivanov.decompose.value.MutableValue 4 | import com.arkivanov.decompose.value.Value 5 | import com.arkivanov.sample.shared.cards.card.CardComponent.Model 6 | 7 | class PreviewCardComponent( 8 | color: Long = 0xFFFF0000, 9 | ) : CardComponent { 10 | 11 | override val model: Value = 12 | MutableValue( 13 | Model( 14 | color = color, 15 | title = "1", 16 | status = "Status: Resumed", 17 | text = "Count: 10", 18 | ) 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.counters 2 | 3 | import com.arkivanov.decompose.router.stack.ChildStack 4 | import com.arkivanov.decompose.value.Value 5 | import com.arkivanov.essenty.backhandler.BackHandlerOwner 6 | import com.arkivanov.sample.shared.counters.counter.CounterComponent 7 | 8 | interface CountersComponent : BackHandlerOwner { 9 | 10 | val stack: Value> 11 | 12 | fun onBackClicked() 13 | fun onBackClicked(toIndex: Int) 14 | } 15 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/PreviewCountersComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.counters 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.router.stack.ChildStack 5 | import com.arkivanov.decompose.value.MutableValue 6 | import com.arkivanov.decompose.value.Value 7 | import com.arkivanov.sample.shared.PreviewComponentContext 8 | import com.arkivanov.sample.shared.counters.counter.CounterComponent 9 | import com.arkivanov.sample.shared.counters.counter.PreviewCounterComponent 10 | 11 | class PreviewCountersComponent : CountersComponent, ComponentContext by PreviewComponentContext { 12 | 13 | override val stack: Value> = 14 | MutableValue( 15 | ChildStack( 16 | configuration = Unit, 17 | instance = PreviewCounterComponent(), 18 | ) 19 | ) 20 | 21 | override fun onBackClicked() {} 22 | override fun onBackClicked(toIndex: Int) {} 23 | } 24 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/counter/CounterComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.counters.counter 2 | 3 | import com.arkivanov.decompose.router.slot.ChildSlot 4 | import com.arkivanov.decompose.value.Value 5 | import com.arkivanov.sample.shared.dialog.DialogComponent 6 | 7 | interface CounterComponent { 8 | 9 | val model: Value 10 | val dialogSlot: Value> 11 | 12 | fun onInfoClicked() 13 | 14 | fun onNextClicked() 15 | 16 | fun onPrevClicked() 17 | 18 | data class Model( 19 | val title: String = "", 20 | val text: String = "", 21 | val isBackEnabled: Boolean = false, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/counter/PreviewCounterComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.counters.counter 2 | 3 | import com.arkivanov.decompose.router.slot.ChildSlot 4 | import com.arkivanov.decompose.value.MutableValue 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.sample.shared.counters.counter.CounterComponent.Model 7 | import com.arkivanov.sample.shared.dialog.DialogComponent 8 | 9 | class PreviewCounterComponent : CounterComponent { 10 | 11 | override val model: Value = 12 | MutableValue( 13 | Model( 14 | title = "Counter 0", 15 | text = "123", 16 | isBackEnabled = false, 17 | ) 18 | ) 19 | 20 | override val dialogSlot: Value> = 21 | MutableValue(ChildSlot()) 22 | 23 | override fun onNextClicked() {} 24 | override fun onPrevClicked() {} 25 | override fun onInfoClicked() {} 26 | } 27 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/CustomNavigationComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.customnavigation 2 | 3 | import com.arkivanov.decompose.Child 4 | import com.arkivanov.decompose.value.Value 5 | 6 | interface CustomNavigationComponent { 7 | 8 | val children: Value> 9 | 10 | fun onSwitchToPagerClicked() 11 | fun onSwitchToCarouselClicked() 12 | fun onForwardClicked() 13 | fun onBackwardClicked() 14 | fun onShuffleClicked() 15 | fun onCloseClicked() 16 | 17 | class Children( 18 | val items: List>, 19 | val index: Int, 20 | val mode: Mode, 21 | ) 22 | 23 | enum class Mode { 24 | CAROUSEL, PAGER 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/KittenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.customnavigation 2 | 3 | import com.arkivanov.decompose.value.Value 4 | import com.arkivanov.sample.shared.ImageResourceId 5 | 6 | interface KittenComponent { 7 | 8 | val model: Value 9 | 10 | data class Model( 11 | val imageResourceId: ImageResourceId, 12 | val text: String, 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/customnavigation/PreviewKittenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.customnavigation 2 | 3 | import com.arkivanov.decompose.value.MutableValue 4 | import com.arkivanov.decompose.value.Value 5 | import com.arkivanov.sample.shared.ImageResourceId 6 | import com.arkivanov.sample.shared.customnavigation.KittenComponent.Model 7 | 8 | class PreviewKittenComponent : KittenComponent { 9 | 10 | override val model: Value = MutableValue(Model(imageResourceId = ImageResourceId.CAT_1, text = "Kitten")) 11 | } 12 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dialog/DefaultDialogComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dialog 2 | 3 | class DefaultDialogComponent( 4 | override val title: String, 5 | override val message: String, 6 | private val onDismissed: () -> Unit, 7 | ) : DialogComponent { 8 | override fun onDismissClicked() { 9 | onDismissed() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dialog/DialogComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dialog 2 | 3 | interface DialogComponent { 4 | 5 | val title: String 6 | val message: String 7 | 8 | fun onDismissClicked() 9 | } 10 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/DynamicFeaturesComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures 2 | 3 | import com.arkivanov.decompose.router.stack.ChildStack 4 | import com.arkivanov.decompose.value.Value 5 | import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DynamicFeatureComponent 6 | import com.arkivanov.sample.shared.dynamicfeatures.feature1.Feature1 7 | import com.arkivanov.sample.shared.dynamicfeatures.feature2.Feature2 8 | 9 | interface DynamicFeaturesComponent { 10 | 11 | val stack: Value> 12 | 13 | fun onCloseClicked() 14 | 15 | sealed class Child { 16 | class Feature1Child(val feature1: DynamicFeatureComponent) : Child() 17 | class Feature2Child(val feature2: DynamicFeatureComponent) : Child() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DynamicFeatureComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature 2 | 3 | import com.arkivanov.decompose.router.stack.ChildStack 4 | import com.arkivanov.decompose.value.Value 5 | 6 | interface DynamicFeatureComponent { 7 | 8 | val childStack: Value>> 9 | 10 | sealed class Child { 11 | class LoadingChild(val name: String) : Child() 12 | class FeatureChild(val feature: T) : Child() 13 | class ErrorChild(val name: String) : Child() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/FeatureInstaller.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature 2 | 3 | import com.badoo.reaktive.single.Single 4 | 5 | interface FeatureInstaller { 6 | 7 | fun install(name: String): Single 8 | 9 | sealed interface Result { 10 | data object Installed : Result 11 | data object Cancelled : Result 12 | data object Error : Result 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1Factory.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | 5 | internal expect fun Feature1( 6 | componentContext: ComponentContext, 7 | onFeature2: () -> Unit, 8 | ): Feature1 9 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2Factory.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | 5 | internal expect fun Feature2( 6 | componentContext: ComponentContext, 7 | magicNumber: Int, 8 | onFinished: () -> Unit, 9 | ): Feature2 10 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/menu/DefaultMenuComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.menu 2 | 3 | internal class DefaultMenuComponent( 4 | override val onDynamicFeaturesItemSelected: () -> Unit, 5 | override val onCustomNavigationItemSelected: () -> Unit, 6 | override val onPagesItemSelected: () -> Unit, 7 | override val onSharedTransitionsItemSelected: () -> Unit, 8 | ) : MenuComponent 9 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/menu/MenuComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.menu 2 | 3 | interface MenuComponent { 4 | 5 | val onDynamicFeaturesItemSelected: () -> Unit 6 | val onCustomNavigationItemSelected: () -> Unit 7 | val onPagesItemSelected: () -> Unit 8 | val onSharedTransitionsItemSelected: () -> Unit 9 | } 10 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/menu/PreviewMenuComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.menu 2 | 3 | class PreviewMenuComponent : MenuComponent { 4 | override val onDynamicFeaturesItemSelected: () -> Unit = {} 5 | override val onCustomNavigationItemSelected: () -> Unit = {} 6 | override val onPagesItemSelected: () -> Unit = {} 7 | override val onSharedTransitionsItemSelected: () -> Unit = {} 8 | } 9 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/MultiPaneComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.multipane 2 | 3 | import com.arkivanov.decompose.ExperimentalDecomposeApi 4 | import com.arkivanov.decompose.router.panels.ChildPanels 5 | import com.arkivanov.decompose.router.panels.ChildPanelsMode 6 | import com.arkivanov.decompose.router.webhistory.WebNavigationOwner 7 | import com.arkivanov.decompose.value.Value 8 | import com.arkivanov.essenty.backhandler.BackHandlerOwner 9 | import com.arkivanov.sample.shared.multipane.author.ArticleAuthorComponent 10 | import com.arkivanov.sample.shared.multipane.details.ArticleDetailsComponent 11 | import com.arkivanov.sample.shared.multipane.list.ArticleListComponent 12 | 13 | @OptIn(ExperimentalDecomposeApi::class) 14 | interface MultiPaneComponent : BackHandlerOwner, WebNavigationOwner { 15 | 16 | val panels: Value> 17 | 18 | fun setMode(mode: ChildPanelsMode) 19 | fun onBack() 20 | } 21 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/author/ArticleAuthorComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.multipane.author 2 | 3 | import com.arkivanov.decompose.value.Value 4 | 5 | interface ArticleAuthorComponent { 6 | 7 | val models: Value 8 | 9 | fun onCloseClicked() 10 | 11 | data class Model( 12 | val isToolbarVisible: Boolean, 13 | val isCloseButtonVisible: Boolean, 14 | val author: Author, 15 | ) 16 | 17 | data class Author( 18 | val name: String, 19 | val bio: String, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/database/ArticleDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.multipane.database 2 | 3 | internal interface ArticleDatabase { 4 | 5 | fun getArticles(): List 6 | 7 | fun getArticle(id: Long): ArticleEntity 8 | } 9 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/database/ArticleEntity.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.multipane.database 2 | 3 | internal data class ArticleEntity( 4 | val id: Long, 5 | val author: AuthorEntity, 6 | val title: String, 7 | val text: String, 8 | ) 9 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/database/AuthorEntity.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.multipane.database 2 | 3 | internal data class AuthorEntity( 4 | val id: Long, 5 | val name: String, 6 | val bio: String, 7 | ) 8 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/details/ArticleDetailsComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.multipane.details 2 | 3 | import com.arkivanov.decompose.value.Value 4 | 5 | interface ArticleDetailsComponent { 6 | 7 | val models: Value 8 | 9 | fun onAuthorClicked() 10 | fun onCloseClicked() 11 | 12 | data class Model( 13 | val isToolbarVisible: Boolean, 14 | val article: Article 15 | ) 16 | 17 | data class Article( 18 | val title: String, 19 | val authorId: Long, 20 | val authorName: String, 21 | val text: String 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/details/PreviewArticleDetailsComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.multipane.details 2 | 3 | import com.arkivanov.decompose.value.MutableValue 4 | import com.arkivanov.decompose.value.Value 5 | import com.arkivanov.sample.shared.multipane.details.ArticleDetailsComponent.Article 6 | import com.arkivanov.sample.shared.multipane.details.ArticleDetailsComponent.Model 7 | 8 | class PreviewArticleDetailsComponent( 9 | isToolbarVisible: Boolean = false, 10 | ) : ArticleDetailsComponent { 11 | 12 | override val models: Value = 13 | MutableValue( 14 | Model( 15 | isToolbarVisible = isToolbarVisible, 16 | article = Article( 17 | title = "Article 1", 18 | authorId = 1, 19 | authorName = "John Smith", 20 | text = "Article 1 ".repeat(50), 21 | ), 22 | ) 23 | ) 24 | 25 | override fun onAuthorClicked() {} 26 | override fun onCloseClicked() {} 27 | } 28 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/list/ArticleListComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.multipane.list 2 | 3 | import com.arkivanov.decompose.value.Value 4 | 5 | interface ArticleListComponent { 6 | 7 | val models: Value 8 | 9 | fun onArticleClicked(article: Article) 10 | 11 | data class Model( 12 | val articles: List
, 13 | val isToolbarVisible: Boolean, 14 | val selectedArticleId: Long? 15 | ) 16 | 17 | data class Article( 18 | val id: Long, 19 | val title: String 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/list/PreviewArticleListComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.multipane.list 2 | 3 | import com.arkivanov.decompose.value.MutableValue 4 | import com.arkivanov.decompose.value.Value 5 | import com.arkivanov.sample.shared.multipane.list.ArticleListComponent.Article 6 | import com.arkivanov.sample.shared.multipane.list.ArticleListComponent.Model 7 | 8 | class PreviewArticleListComponent( 9 | isToolbarVisible: Boolean = false, 10 | ) : ArticleListComponent { 11 | 12 | override val models: Value = 13 | MutableValue( 14 | Model( 15 | articles = List(10) { index -> 16 | Article(id = index.toLong(), title = "Article $index") 17 | }, 18 | isToolbarVisible = isToolbarVisible, 19 | selectedArticleId = null, 20 | ) 21 | ) 22 | 23 | override fun onArticleClicked(article: Article) {} 24 | } 25 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.pages 2 | 3 | import com.arkivanov.decompose.router.pages.ChildPages 4 | import com.arkivanov.decompose.router.webhistory.WebNavigationOwner 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.sample.shared.customnavigation.KittenComponent 7 | 8 | interface PagesComponent : WebNavigationOwner { 9 | 10 | val pages: Value> 11 | 12 | fun selectPage(index: Int) 13 | fun selectNext() 14 | fun selectPrev() 15 | fun onCloseClicked() 16 | } 17 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/Image.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.sharedtransitions 2 | 3 | import com.arkivanov.sample.shared.ImageResourceId 4 | 5 | data class Image( 6 | val id: Int, 7 | val resourceId: ImageResourceId, 8 | ) 9 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/SharedTransitionsComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.sharedtransitions 2 | 3 | import com.arkivanov.decompose.router.stack.ChildStack 4 | import com.arkivanov.decompose.router.webhistory.WebNavigationOwner 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.essenty.backhandler.BackHandlerOwner 7 | import com.arkivanov.sample.shared.sharedtransitions.gallery.GalleryComponent 8 | import com.arkivanov.sample.shared.sharedtransitions.photo.PhotoComponent 9 | 10 | interface SharedTransitionsComponent : BackHandlerOwner, WebNavigationOwner { 11 | 12 | val stack: Value> 13 | 14 | fun onBack() 15 | 16 | sealed class Child { 17 | class GalleryChild(val component: GalleryComponent) : Child() 18 | class PhotoChild(val component: PhotoComponent) : Child() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/gallery/GalleryComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.sharedtransitions.gallery 2 | 3 | import com.arkivanov.decompose.ExperimentalDecomposeApi 4 | import com.arkivanov.decompose.router.items.LazyChildItems 5 | import com.arkivanov.sample.shared.sharedtransitions.Image 6 | import com.arkivanov.sample.shared.sharedtransitions.thumbnail.ThumbnailComponent 7 | 8 | interface GalleryComponent { 9 | 10 | @OptIn(ExperimentalDecomposeApi::class) 11 | val items: LazyChildItems 12 | 13 | fun onCloseClicked() 14 | } 15 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/gallery/PreviewGalleryComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.sharedtransitions.gallery 2 | 3 | import com.arkivanov.decompose.router.items.LazyChildItems 4 | import com.arkivanov.sample.shared.ImageResourceId 5 | import com.arkivanov.sample.shared.SimpleLazyChildItems 6 | import com.arkivanov.sample.shared.sharedtransitions.Image 7 | import com.arkivanov.sample.shared.sharedtransitions.thumbnail.PreviewThumbnailComponent 8 | import com.arkivanov.sample.shared.sharedtransitions.thumbnail.ThumbnailComponent 9 | 10 | class PreviewGalleryComponent : GalleryComponent { 11 | 12 | override val items: LazyChildItems = 13 | SimpleLazyChildItems( 14 | List(10) { index -> 15 | PreviewThumbnailComponent( 16 | image = Image( 17 | id = index, 18 | resourceId = ImageResourceId.entries[index % ImageResourceId.entries.size], 19 | ), 20 | ) 21 | }.associateBy { it.image }, 22 | ) 23 | 24 | override fun onCloseClicked() {} 25 | } 26 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/DefaultPhotoComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.sharedtransitions.photo 2 | 3 | import com.arkivanov.sample.shared.sharedtransitions.Image 4 | 5 | class DefaultPhotoComponent( 6 | override val image: Image, 7 | private val onFinished: () -> Unit, 8 | ) : PhotoComponent { 9 | 10 | override fun onCloseClicked() { 11 | onFinished() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/PhotoComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.sharedtransitions.photo 2 | 3 | import com.arkivanov.sample.shared.sharedtransitions.Image 4 | 5 | interface PhotoComponent { 6 | 7 | val image: Image 8 | 9 | fun onCloseClicked() 10 | } 11 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/photo/PreviewPhotoComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.sharedtransitions.photo 2 | 3 | import com.arkivanov.sample.shared.ImageResourceId 4 | import com.arkivanov.sample.shared.sharedtransitions.Image 5 | 6 | class PreviewPhotoComponent : PhotoComponent { 7 | 8 | override val image: Image = Image(id = 1, resourceId = ImageResourceId.CAT_1) 9 | 10 | override fun onCloseClicked() {} 11 | } 12 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/thumbnail/DefaultThumbnailComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.sharedtransitions.thumbnail 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.sample.shared.sharedtransitions.Image 5 | 6 | class DefaultThumbnailComponent( 7 | componentContext: ComponentContext, 8 | override val image: Image, 9 | private val onSelected: () -> Unit, 10 | ) : ThumbnailComponent, ComponentContext by componentContext { 11 | 12 | override fun onClicked() { 13 | onSelected() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/thumbnail/PreviewThumbnailComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.sharedtransitions.thumbnail 2 | 3 | import com.arkivanov.sample.shared.ImageResourceId 4 | import com.arkivanov.sample.shared.sharedtransitions.Image 5 | 6 | class PreviewThumbnailComponent( 7 | override val image: Image = Image(id = 1, resourceId = ImageResourceId.CAT_1), 8 | ) : ThumbnailComponent { 9 | 10 | override fun onClicked() {} 11 | } 12 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/sharedtransitions/thumbnail/ThumbnailComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.sharedtransitions.thumbnail 2 | 3 | import com.arkivanov.sample.shared.sharedtransitions.Image 4 | 5 | interface ThumbnailComponent { 6 | 7 | val image: Image 8 | 9 | fun onClicked() 10 | } 11 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/PreviewTabsComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.tabs 2 | 3 | import com.arkivanov.decompose.ExperimentalDecomposeApi 4 | import com.arkivanov.decompose.router.stack.ChildStack 5 | import com.arkivanov.decompose.router.webhistory.WebNavigationOwner 6 | import com.arkivanov.decompose.value.MutableValue 7 | import com.arkivanov.decompose.value.Value 8 | import com.arkivanov.sample.shared.counters.PreviewCountersComponent 9 | import com.arkivanov.sample.shared.tabs.TabsComponent.Child.CountersChild 10 | 11 | @OptIn(ExperimentalDecomposeApi::class) 12 | class PreviewTabsComponent : TabsComponent, WebNavigationOwner.NoOp { 13 | 14 | override val stack: Value> = 15 | MutableValue( 16 | ChildStack( 17 | configuration = Unit, 18 | instance = CountersChild(PreviewCountersComponent()), 19 | ) 20 | ) 21 | 22 | override fun onMenuTabClicked() {} 23 | override fun onCountersTabClicked() {} 24 | override fun onCardsTabClicked() {} 25 | override fun onMultiPaneTabClicked() {} 26 | } 27 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/TabsComponent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.tabs 2 | 3 | import com.arkivanov.decompose.router.stack.ChildStack 4 | import com.arkivanov.decompose.router.webhistory.WebNavigationOwner 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.sample.shared.cards.CardsComponent 7 | import com.arkivanov.sample.shared.counters.CountersComponent 8 | import com.arkivanov.sample.shared.menu.MenuComponent 9 | import com.arkivanov.sample.shared.multipane.MultiPaneComponent 10 | 11 | interface TabsComponent : WebNavigationOwner { 12 | 13 | val stack: Value> 14 | 15 | fun onMenuTabClicked() 16 | fun onCountersTabClicked() 17 | fun onCardsTabClicked() 18 | fun onMultiPaneTabClicked() 19 | 20 | sealed class Child { 21 | class MenuChild(val component: MenuComponent) : Child() 22 | class CountersChild(val component: CountersComponent) : Child() 23 | class CardsChild(val component: CardsComponent) : Child() 24 | class MultiPaneChild(val component: MultiPaneComponent) : Child() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonTest/kotlin/com/arkivanov/sample/shared/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.DefaultComponentContext 5 | import com.arkivanov.decompose.router.stack.ChildStack 6 | import com.arkivanov.decompose.router.stack.active 7 | import com.arkivanov.decompose.value.Value 8 | import com.arkivanov.essenty.lifecycle.LifecycleRegistry 9 | import com.arkivanov.essenty.lifecycle.resume 10 | import kotlin.test.assertIs 11 | 12 | internal fun createComponent(factory: (ComponentContext) -> T): T { 13 | val lifecycle = LifecycleRegistry() 14 | val component = factory(DefaultComponentContext(lifecycle = lifecycle)) 15 | lifecycle.resume() 16 | 17 | return component 18 | } 19 | 20 | internal inline fun Value>.assertActiveInstance() { 21 | value.assertActiveInstance() 22 | } 23 | 24 | internal inline fun ChildStack<*, *>.assertActiveInstance() { 25 | assertIs(active.instance) 26 | } 27 | 28 | internal inline fun Value>.activeInstance(): T = 29 | active.instance as T 30 | -------------------------------------------------------------------------------- /sample/shared/shared/src/commonTest/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/TestFeatureInstaller.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature 2 | 3 | import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.FeatureInstaller.Result 4 | import com.badoo.reaktive.single.Single 5 | import com.badoo.reaktive.single.singleOf 6 | 7 | class TestFeatureInstaller : FeatureInstaller { 8 | 9 | override fun install(name: String): Single = 10 | singleOf(Result.Installed) 11 | } 12 | -------------------------------------------------------------------------------- /sample/shared/shared/src/iosMain/kotlin/com/arkivanov/sample/shared/StateKeeperUtils.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared 2 | 3 | import com.arkivanov.essenty.statekeeper.SerializableContainer 4 | import kotlinx.cinterop.BetaInteropApi 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | import platform.Foundation.NSCoder 7 | import platform.Foundation.NSString 8 | import platform.Foundation.decodeTopLevelObjectOfClass 9 | import platform.Foundation.encodeObject 10 | 11 | @Suppress("unused") // Used in Swift 12 | fun save(coder: NSCoder, state: SerializableContainer) { 13 | coder.encodeObject(`object` = json.encodeToString(SerializableContainer.serializer(), state), forKey = "state") 14 | } 15 | 16 | @Suppress("unused") // Used in Swift 17 | @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) 18 | fun restore(coder: NSCoder): SerializableContainer? = 19 | (coder.decodeTopLevelObjectOfClass(aClass = NSString, forKey = "state", error = null) as String?)?.let { 20 | try { 21 | json.decodeFromString(SerializableContainer.serializer(), it) 22 | } catch (e: Exception) { 23 | null 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/RProps.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared 2 | 3 | import react.Props 4 | 5 | external interface RProps : Props { 6 | var component: T 7 | } 8 | -------------------------------------------------------------------------------- /sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/UniqueId.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared 2 | 3 | import js.core.Object 4 | import js.core.jso 5 | 6 | var uniqueId: Long = 0L 7 | 8 | internal fun Any.uniqueId(): Long { 9 | var id: dynamic = asDynamic().__unique_id 10 | if (id == undefined) { 11 | id = ++uniqueId 12 | Object.defineProperty(this, "__unique_id", jso { value = id }) 13 | } 14 | 15 | return id 16 | } 17 | 18 | internal fun Any.uniqueKey(): String = uniqueId().toString() 19 | -------------------------------------------------------------------------------- /sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared 2 | 3 | import com.arkivanov.decompose.value.Value 4 | import react.ChildrenBuilder 5 | import react.FC 6 | import react.StateInstance 7 | import react.useEffectOnce 8 | import react.useState 9 | 10 | internal fun Value.useAsState(): StateInstance { 11 | val state = useState { value } 12 | val (_, set) = state 13 | 14 | useEffectOnce { 15 | val cancellation = subscribe { set(it) } 16 | cleanup { cancellation.cancel() } 17 | } 18 | 19 | return state 20 | } 21 | 22 | internal fun ChildrenBuilder.componentContent(component: T, content: FC>) { 23 | content { 24 | this.component = component 25 | key = component.uniqueKey() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/counters/CountersContent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.counters 2 | 3 | import com.arkivanov.sample.shared.AppBar 4 | import com.arkivanov.sample.shared.RProps 5 | import com.arkivanov.sample.shared.Scaffold 6 | import com.arkivanov.sample.shared.componentContent 7 | import com.arkivanov.sample.shared.counters.counter.CounterContent 8 | import com.arkivanov.sample.shared.useAsState 9 | import react.FC 10 | 11 | val CountersContent: FC> = FC { props -> 12 | val stack by props.component.stack.useAsState() 13 | 14 | Scaffold { 15 | appBar = AppBar(title = "Counters") 16 | 17 | componentContent(component = stack.active.instance, content = CounterContent) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DynamicFeatureContent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature 2 | 3 | import com.arkivanov.sample.shared.RProps 4 | import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DynamicFeatureComponent.Child.ErrorChild 5 | import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DynamicFeatureComponent.Child.FeatureChild 6 | import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.DynamicFeatureComponent.Child.LoadingChild 7 | import com.arkivanov.sample.shared.useAsState 8 | import mui.material.Typography 9 | import react.ChildrenBuilder 10 | import react.FC 11 | 12 | @Suppress("FunctionName") // Factory function 13 | fun DynamicFeatureContent( 14 | content: ChildrenBuilder.(T) -> Unit, 15 | ): FC>> = FC { props -> 16 | val stack by props.component.childStack.useAsState() 17 | 18 | when (val child = stack.active.instance) { 19 | is LoadingChild -> Typography { +"Loading ${child.name}" } 20 | is FeatureChild -> content(child.feature) 21 | is ErrorChild -> Typography { +"Error loading ${child.name}" } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1Content.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1 2 | 3 | import com.arkivanov.sample.shared.RProps 4 | import com.arkivanov.sample.shared.useAsState 5 | import mui.material.Button 6 | import mui.material.ButtonColor 7 | import mui.material.ButtonVariant 8 | import mui.material.Stack 9 | import mui.material.Typography 10 | import mui.system.responsive 11 | import react.FC 12 | 13 | val Feature1Content: FC> = FC { props -> 14 | val model by props.component.models.useAsState() 15 | 16 | Stack { 17 | spacing = responsive(2) 18 | 19 | Typography { +model.title } 20 | 21 | Button { 22 | variant = ButtonVariant.contained 23 | color = ButtonColor.primary 24 | onClick = { props.component.onFeature2Clicked() } 25 | 26 | +"Go to Feature2" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2Content.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2 2 | 3 | import com.arkivanov.sample.shared.RProps 4 | import com.arkivanov.sample.shared.useAsState 5 | import mui.material.Button 6 | import mui.material.ButtonColor 7 | import mui.material.ButtonVariant 8 | import mui.material.Stack 9 | import mui.material.Typography 10 | import mui.system.responsive 11 | import react.FC 12 | 13 | val Feature2Content: FC> = FC { props -> 14 | val model by props.component.models.useAsState() 15 | 16 | Stack { 17 | spacing = responsive(2) 18 | 19 | Typography { +model.title } 20 | 21 | Typography { +model.text } 22 | 23 | Button { 24 | variant = ButtonVariant.contained 25 | color = ButtonColor.primary 26 | onClick = { props.component.onCloseClicked() } 27 | 28 | +"Close" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/root/NotImplementedContent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.root 2 | 3 | import mui.material.Box 4 | import mui.material.Typography 5 | import mui.system.sx 6 | import react.FC 7 | import web.cssom.AlignItems 8 | import web.cssom.Display 9 | import web.cssom.FlexDirection 10 | import web.cssom.px 11 | 12 | var NotImplementedContent: FC = FC { 13 | Box { 14 | sx { 15 | padding = 16.px 16 | display = Display.flex 17 | flexDirection = FlexDirection.column 18 | alignItems = AlignItems.center 19 | } 20 | 21 | Typography { +"Not implemented yet :-(" } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/root/RootContent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.root 2 | 3 | import com.arkivanov.sample.shared.RProps 4 | import com.arkivanov.sample.shared.componentContent 5 | import com.arkivanov.sample.shared.root.RootComponent.Child.CustomNavigationChild 6 | import com.arkivanov.sample.shared.root.RootComponent.Child.DynamicFeaturesChild 7 | import com.arkivanov.sample.shared.root.RootComponent.Child.PagesChild 8 | import com.arkivanov.sample.shared.root.RootComponent.Child.SharedTransitionsChild 9 | import com.arkivanov.sample.shared.root.RootComponent.Child.TabsChild 10 | import com.arkivanov.sample.shared.tabs.TabsContent 11 | import com.arkivanov.sample.shared.useAsState 12 | import react.FC 13 | 14 | var RootContent: FC> = FC { props -> 15 | val stack by props.component.stack.useAsState() 16 | 17 | when (val child = stack.active.instance) { 18 | is TabsChild -> componentContent(component = child.component, content = TabsContent) 19 | 20 | is CustomNavigationChild, 21 | is DynamicFeaturesChild, 22 | is PagesChild, 23 | is SharedTransitionsChild -> error("Unsupported child: $child") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/shared/shared/src/jvmMain/kotlin/com/arkivanov/sample/shared/StateKeeperUtils.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared 2 | 3 | import com.arkivanov.essenty.statekeeper.SerializableContainer 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.json.decodeFromStream 6 | import kotlinx.serialization.json.encodeToStream 7 | import java.io.File 8 | 9 | @OptIn(ExperimentalSerializationApi::class) 10 | fun SerializableContainer.writeToFile(file: File) { 11 | file.outputStream().use { output -> 12 | json.encodeToStream(SerializableContainer.serializer(), this, output) 13 | } 14 | } 15 | 16 | @OptIn(ExperimentalSerializationApi::class) 17 | fun File.readSerializableContainer(): SerializableContainer? = 18 | takeIf(File::exists)?.inputStream()?.use { input -> 19 | try { 20 | json.decodeFromStream(SerializableContainer.serializer(), input) 21 | } catch (e: Exception) { 22 | null 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/shared/shared/src/nonAndroidMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DefaultFeatureInstaller.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature 2 | 3 | import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.FeatureInstaller.Result 4 | import com.badoo.reaktive.single.Single 5 | import com.badoo.reaktive.single.singleOf 6 | 7 | object DefaultFeatureInstaller : FeatureInstaller { 8 | 9 | override fun install(name: String): Single = singleOf(Result.Installed) 10 | } 11 | -------------------------------------------------------------------------------- /sample/shared/shared/src/nonAndroidMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature1/Feature1FactoryNonAndroid.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature1 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | 5 | internal actual fun Feature1( 6 | componentContext: ComponentContext, 7 | onFeature2: () -> Unit, 8 | ): Feature1 = 9 | Feature1Component( 10 | componentContext = componentContext, 11 | onFeature2 = onFeature2, 12 | ) 13 | -------------------------------------------------------------------------------- /sample/shared/shared/src/nonAndroidMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/feature2/Feature2FactoryNonAndroid.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.sample.shared.dynamicfeatures.feature2 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | 5 | internal actual fun Feature2( 6 | componentContext: ComponentContext, 7 | magicNumber: Int, 8 | onFinished: () -> Unit, 9 | ): Feature2 = 10 | Feature2Component( 11 | componentContext = componentContext, 12 | magicNumber = magicNumber, 13 | onFinished = onFinished, 14 | ) 15 | -------------------------------------------------------------------------------- /tools/check-publication/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /tools/check-publication/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tools/check-publication/src/commonMain/kotlin/com/arkivanov/decompose/tools/checkpublication/Dummy.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.decompose.tools.checkpublication 2 | 3 | fun dummy() { 4 | // no-op 5 | } 6 | --------------------------------------------------------------------------------