Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: arkivanov/Decompose
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.5.2
Choose a base ref
...
head repository: arkivanov/Decompose
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0.6.0-native-compose
Choose a head ref

Commits on Mar 25, 2022

  1. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    Zylphrex Tony Xiao
    Copy the full SHA
    de942c4 View commit details
  2. Merge pull request #53 from arkivanov/anim-selector

    Different animations for different children
    arkivanov authored Mar 25, 2022
    Copy the full SHA
    f60178e View commit details

Commits on Mar 26, 2022

  1. Copy the full SHA
    bdac3fd View commit details
  2. Merge pull request #58 from arkivanov/disable-interactions-while-anim…

    …ating
    
    Disable interaction with children while animating
    arkivanov authored Mar 26, 2022
    Copy the full SHA
    2e58b57 View commit details

Commits on Mar 31, 2022

  1. Copy the full SHA
    f9aa5d9 View commit details
  2. Merge pull request #59 from arkivanov/describe-root-ComponentContext-…

    …in-docs
    
    Describe root ComponentContext in docs
    arkivanov authored Mar 31, 2022
    Copy the full SHA
    69d8793 View commit details
  3. Copy the full SHA
    6fef9f1 View commit details
  4. Update README.md

    arkivanov authored Mar 31, 2022
    Copy the full SHA
    78b5584 View commit details
  5. Update README.md

    arkivanov authored Mar 31, 2022
    Copy the full SHA
    6ccf071 View commit details
  6. Merge pull request #60 from arkivanov/update-checkout-action-to-v3

    Updated checkout action to v3
    arkivanov authored Mar 31, 2022
    Copy the full SHA
    aa5fbd2 View commit details

Commits on Apr 1, 2022

  1. Copy the full SHA
    ad6b1bb View commit details
  2. Merge pull request #62 from arkivanov/describe-Decompose-with-Fragmen…

    …ts-in-docs
    
    Describe using Decompose with Fragments in docs
    arkivanov authored Apr 1, 2022
    Copy the full SHA
    4d5aebc View commit details
  3. Removed deprecated code with ERROR level, incresased deprecation leve…

    …ls from WARNING to ERROR
    arkivanov committed Apr 1, 2022
    Copy the full SHA
    d94340a View commit details
  4. Merge pull request #64 from arkivanov/tidy-deprecated

    Removed deprecated code with ERROR level, incresased deprecation levels from WARNING to ERROR
    arkivanov authored Apr 1, 2022
    Copy the full SHA
    cd56772 View commit details

Commits on Apr 8, 2022

  1. Copy the full SHA
    309fb39 View commit details
  2. Merge pull request #66 from arkivanov/add-Router-navigate-onComplete

    Added onComplete callback to Router.navigate
    arkivanov authored Apr 8, 2022
    Copy the full SHA
    bc73a1c View commit details
  3. Bumped version to 0.6.0

    arkivanov committed Apr 8, 2022
    Copy the full SHA
    969cb05 View commit details
  4. Copy the full SHA
    2bfdbb1 View commit details
  5. Merge pull request #67 from arkivanov/describe-new-compose-animations…

    …-in-docs
    
    Described new Compose animations in docs
    arkivanov authored Apr 8, 2022
    Copy the full SHA
    134b33c View commit details

Commits on Apr 12, 2022

  1. Copy the full SHA
    775a42e View commit details
  2. Merge pull request #72 from arkivanov/describe-separate-animations-in…

    …-docs
    
    Described separate Compose animations in the docs
    arkivanov authored Apr 12, 2022
    Copy the full SHA
    986a607 View commit details

Commits on Apr 21, 2022

  1. Copy the full SHA
    6ee5ea2 View commit details
  2. Fix style conflicts in docs

    darvld authored Apr 21, 2022
    Copy the full SHA
    bec761a View commit details
  3. Merge pull request #79 from darvld/improved-docs

    Document correct lifecycle setup for compose-jb
    arkivanov authored Apr 21, 2022
    Copy the full SHA
    65dcef7 View commit details

Commits on Apr 24, 2022

  1. Copy the full SHA
    0e69981 View commit details
  2. Merge pull request #80 from arkivanov/replace-muirwik-with-kotlin-wra…

    …ppers
    
    Replaced Muirwik components with Kotlin Wrappers
    arkivanov authored Apr 24, 2022
    Copy the full SHA
    216f86b View commit details
  3. Copy the full SHA
    83f1418 View commit details
  4. Merge pull request #81 from arkivanov/fix-folder-structure-in-master-…

    …detail-sample
    
    Fixed folder structure in master detail sample
    arkivanov authored Apr 24, 2022
    Copy the full SHA
    abe4aee View commit details
  5. Copy the full SHA
    6c9b4ac View commit details
  6. Merge pull request #82 from arkivanov/fix-counter-sample-build-on-apple

    Fixed Counter sample build on Apple
    arkivanov authored Apr 24, 2022
    Copy the full SHA
    59e3f26 View commit details
  7. Copy the full SHA
    5de7cb8 View commit details
  8. Merge pull request #83 from arkivanov/remove-jetpack-compose-extensions

    [compose-darwin] Removed extensions-compose-jetpack module
    arkivanov authored Apr 24, 2022
    Copy the full SHA
    7a714f0 View commit details
  9. Copy the full SHA
    b7490b8 View commit details
  10. Merge pull request #75 from arkivanov/enable-darwin-targets-for-compose

    [compose-darwin] Enable Darwin targets for extensions-compose-jetbrains module
    arkivanov authored Apr 24, 2022
    Copy the full SHA
    ddcd2ac View commit details
  11. Copy the full SHA
    61c73b7 View commit details
  12. Copy the full SHA
    38ee9b3 View commit details
Showing with 2,275 additions and 2,761 deletions.
  1. +1 −8 .github/workflows/build-linux.yml
  2. +1 −1 .github/workflows/build-macos.yml
  3. +5 −5 .github/workflows/publish.yml
  4. +4 −4 README.md
  5. +20 −5 decompose/api/android/decompose.api
  6. +4 −5 decompose/api/jvm/decompose.api
  7. +1 −11 decompose/src/commonMain/kotlin/com/arkivanov/decompose/ComponentContext.kt
  8. +0 −7 decompose/src/commonMain/kotlin/com/arkivanov/decompose/ComponentContextExt.kt
  9. +0 −6 decompose/src/commonMain/kotlin/com/arkivanov/decompose/DefaultComponentContext.kt
  10. +15 −5 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/Router.kt
  11. +19 −5 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterExt.kt
  12. +14 −5 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterFactoryExt.kt
  13. +11 −4 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterImpl.kt
  14. +17 −0 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterStackExt.kt
  15. +6 −1 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/StackNavigatorImpl.kt
  16. +0 −2 decompose/src/commonTest/kotlin/com/arkivanov/decompose/ChildContextTest.kt
  17. +54 −0 decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterPopTest.kt
  18. +107 −49 decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterTest.kt
  19. +95 −74 decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/StackNavigatorTest.kt
  20. +5 −2 decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/TestRouter.kt
  21. +1 −0 decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/webhistory/DefaultWebHistoryController.kt
  22. +1 −0 ...se/src/jsTest/kotlin/com/arkivanov/decompose/router/webhistory/DefaultWebHistoryControllerTest.kt
  23. +7 −18 deps.versions.toml
  24. +94 −1 docs/component/overview.md
  25. +121 −41 docs/extensions/compose.md
  26. BIN docs/media/{ComposeAnimationCrossfade.gif → ComposeAnimationFade.gif}
  27. BIN docs/media/{ComposeAnimationCrossfadeScale.gif → ComposeAnimationFadeScale.gif}
  28. BIN docs/media/ComposeAnimationSeparate.gif
  29. +18 −4 docs/router/navigation.md
  30. +38 −34 extensions-compose-jetbrains/api/android/extensions-compose-jetbrains.api
  31. +38 −33 extensions-compose-jetbrains/api/jvm/extensions-compose-jetbrains.api
  32. +8 −1 extensions-compose-jetbrains/build.gradle.kts
  33. +0 −70 ...c/androidMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/RootComponentBuilder.kt
  34. +3 −0 ...-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/Children.kt
  35. +0 −13 ...ins/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/ValueComposable.kt
  36. +44 −117 ...ain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation.kt
  37. +0 −20 ...n/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection.kt
  38. +89 −0 ...Main/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator.kt
  39. +0 −21 ...ain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement.kt
  40. +0 −27 ...mmonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Crossfade.kt
  41. +0 −34 ...ain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/CrossfadeScale.kt
  42. +113 −0 ...lin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/DefaultChildAnimation.kt
  43. +45 −0 ...tlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/DefaultChildAnimator.kt
  44. +35 −0 ...mmonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction.kt
  45. +3 −1 ...otlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/EmptyChildAnimation.kt
  46. +18 −0 ...rc/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Fade.kt
  47. +29 −0 ...c/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Scale.kt
  48. +5 −17 ...c/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Slide.kt
  49. +0 −34 ...mmonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Typealias.kt
  50. +0 −43 ...s/src/jvmMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/RootComponentBuilder.kt
  51. +9 −6 ...jetbrains/src/jvmTest/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/ChildrenTest.kt
  52. +0 −73 extensions-compose-jetpack/api/extensions-compose-jetpack.api
  53. +0 −41 extensions-compose-jetpack/build.gradle.kts
  54. +0 −151 ...e-jetpack/src/androidTest/java/com/arkivanov/decompose/extensions/compose/jetpack/ChildrenTest.kt
  55. +0 −2 extensions-compose-jetpack/src/main/AndroidManifest.xml
  56. +0 −83 ...ions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/Children.kt
  57. +0 −70 ...-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/RootComponentBuilder.kt
  58. +0 −26 ...pose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/SubscribeAsState.kt
  59. +0 −13 ...mpose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/ValueComposable.kt
  60. +0 −136 ...rc/main/java/com/arkivanov/decompose/extensions/compose/jetpack/animation/child/ChildAnimation.kt
  61. +0 −20 ...ava/com/arkivanov/decompose/extensions/compose/jetpack/animation/child/ChildAnimationDirection.kt
  62. +0 −21 ...rc/main/java/com/arkivanov/decompose/extensions/compose/jetpack/animation/child/ChildPlacement.kt
  63. +0 −27 ...ack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/animation/child/Crossfade.kt
  64. +0 −33 ...rc/main/java/com/arkivanov/decompose/extensions/compose/jetpack/animation/child/CrossfadeScale.kt
  65. +0 −10 ...in/java/com/arkivanov/decompose/extensions/compose/jetpack/animation/child/EmptyChildAnimation.kt
  66. +0 −36 ...jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/animation/child/Slide.kt
  67. +0 −34 ...ack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/animation/child/Typealias.kt
  68. +0 −24 ...n/java/com/arkivanov/decompose/extensions/compose/jetpack/lifecycle/LifecycleComposableBuilder.kt
  69. +427 −285 kotlin-js-store/yarn.lock
  70. +6 −7 sample/counter/app-android/build.gradle.kts
  71. +2 −2 sample/counter/app-android/src/main/java/com/arkivanov/counter/app/MainActivity.kt
  72. +2 −3 sample/counter/app-js/build.gradle.kts
  73. +0 −47 sample/counter/app-js/src/main/kotlin/com/arkivanov/sample/counter/app/App.kt
  74. +22 −0 sample/counter/app-js/src/main/kotlin/com/arkivanov/sample/counter/app/CounterR.kt
  75. +105 −0 sample/counter/app-js/src/main/kotlin/com/arkivanov/sample/counter/app/InnerR.kt
  76. +44 −10 sample/counter/app-js/src/main/kotlin/com/arkivanov/sample/counter/app/Main.kt
  77. +7 −0 sample/counter/app-js/src/main/kotlin/com/arkivanov/sample/counter/app/RProps.kt
  78. +75 −0 sample/counter/app-js/src/main/kotlin/com/arkivanov/sample/counter/app/RootR.kt
  79. +6 −4 ...asterdetail.shared → counter/app-js/src/main/kotlin/com/arkivanov/sample/counter/app}/UniqueId.kt
  80. +20 −0 sample/counter/app-js/src/main/kotlin/com/arkivanov/sample/counter/app/Utils.kt
  81. +8 −2 sample/counter/ios-app/ios-app/ContentView.swift
  82. +2 −60 sample/counter/shared/build.gradle.kts
  83. +0 −7 sample/counter/shared/src/jsMain/kotlin/com/arkivanov/sample/counter/shared/Props.kt
  84. +0 −42 sample/counter/shared/src/jsMain/kotlin/com/arkivanov/sample/counter/shared/RenderableComponent.kt
  85. +0 −16 sample/counter/shared/src/jsMain/kotlin/com/arkivanov/sample/counter/shared/UniqueId.kt
  86. +0 −11 sample/counter/shared/src/jsMain/kotlin/com/arkivanov/sample/counter/shared/Utils.kt
  87. +0 −30 sample/counter/shared/src/jsMain/kotlin/com/arkivanov/sample/counter/shared/counter/CounterR.kt
  88. +0 −96 sample/counter/shared/src/jsMain/kotlin/com/arkivanov/sample/counter/shared/inner/InnerR.kt
  89. +0 −83 sample/counter/shared/src/jsMain/kotlin/com/arkivanov/sample/counter/shared/root/RootR.kt
  90. 0 {extensions-compose-jetpack → sample/counter/ui-android}/.gitignore
  91. +15 −0 sample/counter/ui-android/build.gradle.kts
  92. +2 −0 sample/counter/ui-android/src/main/AndroidManifest.xml
  93. +1 −2 ...→ ui-android/src/main/kotlin/com/arkivanov/sample/counter/uiandroid}/CounterInnerContainerView.kt
  94. +1 −2 ... → ui-android/src/main/kotlin/com/arkivanov/sample/counter/uiandroid}/CounterRootContainerView.kt
  95. +1 −2 ...ed/ui/android → ui-android/src/main/kotlin/com/arkivanov/sample/counter/uiandroid}/CounterView.kt
  96. 0 sample/counter/{shared → ui-android}/src/main/res/drawable/border.xml
  97. 0 sample/counter/{shared → ui-android}/src/main/res/layout/counter.xml
  98. 0 sample/counter/{shared → ui-android}/src/main/res/layout/counter_inner.xml
  99. 0 sample/counter/{shared → ui-android}/src/main/res/layout/counter_root.xml
  100. 0 sample/{master-detail/compose-ui → counter/ui-compose}/.gitignore
  101. +28 −0 sample/counter/ui-compose/build.gradle.kts
  102. +7 −51 ...pose → ui-compose/src/commonMain/kotlin/com/arkivanov/sample/counter/uicompose}/CounterInnerUi.kt
  103. +5 −33 ...mpose → ui-compose/src/commonMain/kotlin/com/arkivanov/sample/counter/uicompose}/CounterRootUi.kt
  104. +2 −16 ...i/compose → ui-compose/src/commonMain/kotlin/com/arkivanov/sample/counter/uicompose}/CounterUi.kt
  105. +2 −0 sample/counter/ui-compose/src/main/AndroidManifest.xml
  106. +8 −2 sample/counter/watchos-app/Counter WatchKit Extension/ContentView.swift
  107. +7 −2 ...shared/root/src/commonMain/kotlin/com/arkivanov/sample/dynamicfeatures/shared/root/RootContent.kt
  108. +7 −2 ...n/kotlin/com/arkivanov/sample/dynamicfeatures/shared/root/dynamicfeature/DynamicFeatureContent.kt
  109. +1 −1 sample/master-detail/app-android/build.gradle.kts
  110. +1 −1 sample/master-detail/app-android/src/main/java/com/arkivanov/masterdetail/app/MainActivity.kt
  111. +1 −1 sample/master-detail/app-desktop/build.gradle.kts
  112. +1 −1 sample/master-detail/app-desktop/src/jvmMain/kotlin/com/arkivanov/masterdetail/app/Main.kt
  113. +2 −2 sample/master-detail/app-js/build.gradle.kts
  114. +0 −35 sample/master-detail/app-js/src/main/kotlin/com.arkivanov.sample.masterdetail.app/App.kt
  115. +0 −16 sample/master-detail/app-js/src/main/kotlin/com.arkivanov.sample.masterdetail.app/Main.kt
  116. +65 −0 sample/master-detail/app-js/src/main/kotlin/com/arkivanov/sample/masterdetail/app/ArticleDetails.kt
  117. +25 −0 sample/master-detail/app-js/src/main/kotlin/com/arkivanov/sample/masterdetail/app/ArticleListR.kt
  118. +45 −0 sample/master-detail/app-js/src/main/kotlin/com/arkivanov/sample/masterdetail/app/Main.kt
  119. +7 −0 sample/master-detail/app-js/src/main/kotlin/com/arkivanov/sample/masterdetail/app/RProps.kt
  120. +144 −0 sample/master-detail/app-js/src/main/kotlin/com/arkivanov/sample/masterdetail/app/RootR.kt
  121. +20 −0 sample/master-detail/app-js/src/main/kotlin/com/arkivanov/sample/masterdetail/app/Utils.kt
  122. +2 −2 sample/master-detail/app-js/src/main/resources/index.html
  123. +0 −2 sample/master-detail/compose-ui/src/main/AndroidManifest.xml
  124. +1 −10 sample/master-detail/shared/build.gradle.kts
  125. +1 −0 ...etail/shared/src/commonMain/kotlin/com/arkivanov/sample/masterdetail/shared/root/DetailsRouter.kt
  126. +0 −49 ...er-detail/shared/src/jsMain/kotlin/com.arkivanov.sample.masterdetail.shared/MasterDetailStyles.kt
  127. +0 −7 sample/master-detail/shared/src/jsMain/kotlin/com.arkivanov.sample.masterdetail.shared/Props.kt
  128. +0 −42 ...r-detail/shared/src/jsMain/kotlin/com.arkivanov.sample.masterdetail.shared/RenderableComponent.kt
  129. +0 −11 sample/master-detail/shared/src/jsMain/kotlin/com.arkivanov.sample.masterdetail.shared/Utils.kt
  130. +0 −64 ...ster-detail/shared/src/jsMain/kotlin/com.arkivanov.sample.masterdetail.shared/details/DetailsR.kt
  131. +0 −47 ...ter-detail/shared/src/jsMain/kotlin/com.arkivanov.sample.masterdetail.shared/list/ArticleListR.kt
  132. +0 −103 sample/master-detail/shared/src/jsMain/kotlin/com.arkivanov.sample.masterdetail.shared/root/RootR.kt
  133. +1 −0 sample/master-detail/ui-compose/.gitignore
  134. 0 sample/master-detail/{compose-ui → ui-compose}/build.gradle.kts
  135. +1 −1 ...se/src/commonMain/kotlin/com/arkivanov/sample/masterdetail/uicompose}/details/ArticleDetailsUi.kt
  136. +1 −1 ...-compose/src/commonMain/kotlin/com/arkivanov/sample/masterdetail/uicompose}/list/ArticleListUi.kt
  137. +12 −6 ...ui → ui-compose/src/commonMain/kotlin/com/arkivanov/sample/masterdetail/uicompose}/root/RootUi.kt
  138. +1 −1 ...rc/jvmMain/kotlin/com/arkivanov/sample/masterdetail/uicompose}/details/ArticleDetailsUiPreview.kt
  139. +1 −1 ...pose/src/jvmMain/kotlin/com/arkivanov/sample/masterdetail/uicompose}/list/ArticleListUiPreview.kt
  140. +3 −3 ... ui-compose/src/jvmMain/kotlin/com/arkivanov/sample/masterdetail/uicompose}/root/RootUiPreview.kt
  141. +2 −0 sample/master-detail/ui-compose/src/main/AndroidManifest.xml
  142. +3 −2 settings.gradle.kts
  143. +24 −1 tools/check-publication/build.gradle.kts
9 changes: 1 addition & 8 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Java
uses: actions/setup-java@v1
with:
@@ -29,10 +29,3 @@ jobs:
${{ runner.os }}-gradle-
- name: Build
run: ./gradlew build -Dsplit_targets
- name: Android instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 21
arch: x86
disable-animations: true
script: ./gradlew :extensions-compose-jetpack:connectedDebugAndroidTest --stacktrace
2 changes: 1 addition & 1 deletion .github/workflows/build-macos.yml
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Java
uses: actions/setup-java@v1
with:
10 changes: 5 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ jobs:
needs: create-staging-repository
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Java
uses: actions/setup-java@v1
with:
@@ -43,7 +43,7 @@ jobs:
needs: create-staging-repository
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Java
uses: actions/setup-java@v1
with:
@@ -61,7 +61,7 @@ jobs:
needs: create-staging-repository
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Java
uses: actions/setup-java@v1
with:
@@ -91,7 +91,7 @@ jobs:
needs: close-staging-repository
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Java
uses: actions/setup-java@v1
with:
@@ -108,7 +108,7 @@ jobs:
needs: close-staging-repository
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Java
uses: actions/setup-java@v1
with:
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -16,19 +16,18 @@ Please see the [project website](https://arkivanov.github.io/Decompose/) for doc

Decompose is a Kotlin Multiplatform library for breaking down your code into lifecycle-aware business logic components (aka BLoC), with routing functionality and pluggable UI (Jetpack Compose, Android Views, SwiftUI, JS React, etc.).

## ⚡⚡⚡ Why is this repository a fork?
## ⚡⚡⚡ Where are all the stars, issues, discussions, pull requests, etc?

Having spent 5 years working on a variety of projects for Badoo/Bumble, I’m now off to another adventure. As part of that transition I was asked to transfer this repository to [Badoo GitHub account](https://github.com/badoo).

Now I **continue my work** on this project **as a fork**.
Now I **continue my work** on this project **as a copy**.

There should be no breaking changes related to this transfer. Most of the external links should not be broken. The repository link is also the same: [arkivanov/Decompose](https://github.com/arkivanov/Decompose). Please file an issue in this repository, if you think something is broken or does not work properly.

Here is what is mostly affected by the transfer:

- All the stars were transferred
- Search in the code will not work **as long as this repository has less stars than the parent** (your help is needed!)
- All the Issues and Discussions were transferred as well. I will do all my best to fill the gap here.
- All the issues and discussions were transferred as well. I will do all my best to fill the gap here.
- All pull requests with all the comment history are also gone.

I will continue doing all my best for this project and for the community! Business as usual!
@@ -39,6 +38,7 @@ Additional resources:

## Why Decompose?

* Decompose breaks the code down into small and independent components and organizes them into trees. Each parent component is only aware of its immediate children.
* Decompose draws clear boundaries between UI and non-UI code, which gives the following benefits:
* Better separation of concerns
* Pluggable platform-specific UI (Compose, SwiftUI, React, etc.)
25 changes: 20 additions & 5 deletions decompose/api/android/decompose.api
Original file line number Diff line number Diff line change
@@ -34,11 +34,9 @@ public final class com/arkivanov/decompose/Child$Destroyed : com/arkivanov/decom
}

public abstract interface class com/arkivanov/decompose/ComponentContext : com/arkivanov/essenty/backpressed/BackPressedHandlerOwner, com/arkivanov/essenty/instancekeeper/InstanceKeeperOwner, com/arkivanov/essenty/lifecycle/LifecycleOwner, com/arkivanov/essenty/statekeeper/StateKeeperOwner {
public abstract fun getBackPressedDispatcher ()Lcom/arkivanov/essenty/backpressed/BackPressedDispatcher;
}

public final class com/arkivanov/decompose/ComponentContextExtKt {
public static final fun child (Lcom/arkivanov/decompose/ComponentContext;Ljava/lang/String;)Lcom/arkivanov/decompose/ComponentContext;
public static final fun childContext (Lcom/arkivanov/decompose/ComponentContext;Ljava/lang/String;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Lcom/arkivanov/decompose/ComponentContext;
public static synthetic fun childContext$default (Lcom/arkivanov/decompose/ComponentContext;Ljava/lang/String;Lcom/arkivanov/essenty/lifecycle/Lifecycle;ILjava/lang/Object;)Lcom/arkivanov/decompose/ComponentContext;
}
@@ -47,7 +45,6 @@ public final class com/arkivanov/decompose/DefaultComponentContext : com/arkivan
public fun <init> (Lcom/arkivanov/essenty/lifecycle/Lifecycle;)V
public fun <init> (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Lcom/arkivanov/essenty/backpressed/BackPressedHandler;)V
public synthetic fun <init> (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Lcom/arkivanov/essenty/backpressed/BackPressedHandler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getBackPressedDispatcher ()Lcom/arkivanov/essenty/backpressed/BackPressedDispatcher;
public fun getBackPressedHandler ()Lcom/arkivanov/essenty/backpressed/BackPressedHandler;
public fun getInstanceKeeper ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;
public fun getLifecycle ()Lcom/arkivanov/essenty/lifecycle/Lifecycle;
@@ -75,13 +72,15 @@ public final class com/arkivanov/decompose/lifecycle/MergedLifecycle : com/arkiv

public abstract interface class com/arkivanov/decompose/router/Router {
public abstract fun getState ()Lcom/arkivanov/decompose/value/Value;
public abstract fun navigate (Lkotlin/jvm/functions/Function1;)V
public abstract fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
}

public final class com/arkivanov/decompose/router/RouterExtKt {
public static final fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun getActiveChild (Lcom/arkivanov/decompose/router/Router;)Lcom/arkivanov/decompose/Child$Created;
public static final fun pop (Lcom/arkivanov/decompose/router/Router;)V
public static final fun navigate (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static final fun pop (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun pop$default (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun popWhile (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static final fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
@@ -109,6 +108,22 @@ public final class com/arkivanov/decompose/router/RouterState {
public fun toString ()Ljava/lang/String;
}

public final class com/arkivanov/decompose/router/StackSaverImpl$SavedEntry$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arkivanov/decompose/router/StackSaverImpl$SavedEntry;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/arkivanov/decompose/router/StackSaverImpl$SavedEntry;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/arkivanov/decompose/router/StackSaverImpl$SavedState$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arkivanov/decompose/router/StackSaverImpl$SavedState;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/arkivanov/decompose/router/StackSaverImpl$SavedState;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public abstract interface class com/arkivanov/decompose/router/webhistory/WebHistoryController {
public abstract fun attach (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
}
9 changes: 4 additions & 5 deletions decompose/api/jvm/decompose.api
Original file line number Diff line number Diff line change
@@ -27,11 +27,9 @@ public final class com/arkivanov/decompose/Child$Destroyed : com/arkivanov/decom
}

public abstract interface class com/arkivanov/decompose/ComponentContext : com/arkivanov/essenty/backpressed/BackPressedHandlerOwner, com/arkivanov/essenty/instancekeeper/InstanceKeeperOwner, com/arkivanov/essenty/lifecycle/LifecycleOwner, com/arkivanov/essenty/statekeeper/StateKeeperOwner {
public abstract fun getBackPressedDispatcher ()Lcom/arkivanov/essenty/backpressed/BackPressedDispatcher;
}

public final class com/arkivanov/decompose/ComponentContextExtKt {
public static final fun child (Lcom/arkivanov/decompose/ComponentContext;Ljava/lang/String;)Lcom/arkivanov/decompose/ComponentContext;
public static final fun childContext (Lcom/arkivanov/decompose/ComponentContext;Ljava/lang/String;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Lcom/arkivanov/decompose/ComponentContext;
public static synthetic fun childContext$default (Lcom/arkivanov/decompose/ComponentContext;Ljava/lang/String;Lcom/arkivanov/essenty/lifecycle/Lifecycle;ILjava/lang/Object;)Lcom/arkivanov/decompose/ComponentContext;
}
@@ -40,7 +38,6 @@ public final class com/arkivanov/decompose/DefaultComponentContext : com/arkivan
public fun <init> (Lcom/arkivanov/essenty/lifecycle/Lifecycle;)V
public fun <init> (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Lcom/arkivanov/essenty/backpressed/BackPressedHandler;)V
public synthetic fun <init> (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Lcom/arkivanov/essenty/backpressed/BackPressedHandler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getBackPressedDispatcher ()Lcom/arkivanov/essenty/backpressed/BackPressedDispatcher;
public fun getBackPressedHandler ()Lcom/arkivanov/essenty/backpressed/BackPressedHandler;
public fun getInstanceKeeper ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;
public fun getLifecycle ()Lcom/arkivanov/essenty/lifecycle/Lifecycle;
@@ -62,13 +59,15 @@ public final class com/arkivanov/decompose/lifecycle/MergedLifecycle : com/arkiv

public abstract interface class com/arkivanov/decompose/router/Router {
public abstract fun getState ()Lcom/arkivanov/decompose/value/Value;
public abstract fun navigate (Lkotlin/jvm/functions/Function1;)V
public abstract fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
}

public final class com/arkivanov/decompose/router/RouterExtKt {
public static final fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun getActiveChild (Lcom/arkivanov/decompose/router/Router;)Lcom/arkivanov/decompose/Child$Created;
public static final fun pop (Lcom/arkivanov/decompose/router/Router;)V
public static final fun navigate (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static final fun pop (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun pop$default (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun popWhile (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
public static final fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.arkivanov.decompose

import com.arkivanov.essenty.backpressed.BackPressedDispatcher
import com.arkivanov.essenty.backpressed.BackPressedHandlerOwner
import com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner
import com.arkivanov.essenty.lifecycle.LifecycleOwner
@@ -10,13 +9,4 @@ interface ComponentContext :
LifecycleOwner,
StateKeeperOwner,
InstanceKeeperOwner,
BackPressedHandlerOwner {

@Deprecated(
message = "ComponentContext now extends BackPressedHandlerOwner instead of BackPressedDispatcherOwner. " +
"Please use backPressedHandler property.",
replaceWith = ReplaceWith("backPressedHandler"),
level = DeprecationLevel.ERROR,
)
val backPressedDispatcher: BackPressedDispatcher
}
BackPressedHandlerOwner
Original file line number Diff line number Diff line change
@@ -22,10 +22,3 @@ fun ComponentContext.childContext(key: String, lifecycle: Lifecycle? = null): Co
instanceKeeper = instanceKeeper.child(key, lifecycle),
backPressedHandler = backPressedHandler.child(lifecycle),
)

@Deprecated(
message = "Use childContext(key)",
replaceWith = ReplaceWith("childContext(key)"),
level = DeprecationLevel.ERROR,
)
fun ComponentContext.child(key: String): ComponentContext = childContext(key = key)
Original file line number Diff line number Diff line change
@@ -20,12 +20,6 @@ class DefaultComponentContext(
override val instanceKeeper: InstanceKeeper = instanceKeeper ?: InstanceKeeperDispatcher().attachTo(lifecycle)
override val backPressedHandler: BackPressedHandler = backPressedHandler ?: BackPressedDispatcher()

override val backPressedDispatcher: BackPressedDispatcher by lazy {
BackPressedDispatcher().also { dispatcher ->
this.backPressedHandler.register(dispatcher::onBackPressed)
}
}

constructor(lifecycle: Lifecycle) : this(
lifecycle = lifecycle,
stateKeeper = null,
Original file line number Diff line number Diff line change
@@ -14,10 +14,20 @@ interface Router<C : Any, out T : Any> {
* The stack is represented as [List], where the last element is the top of the stack,
* and the first element is the bottom of the stack. The returned stack must not be empty.
*
* The [Router] compares the current stack with new one returned by the [transformer] function.
* New components are created for all new configurations in the stack, and all components
* that are no longer in the stack are destroyed. The amount and order of components in the
* resulting stack matches the amount and order of configurations returned by the [transformer].
* During the navigation process, the `Router` compares the new stack of configurations with
* the previous one. The `Router` ensures that all removed components are destroyed, and that
* there is only one component resumed at a time - the top one. All components in the back stack
* are always either stopped or destroyed.
*
* The `Router` usually performs the navigation synchronously, which means that by the time
* the `navigate` method returns, the navigation is finished and all component lifecycles are
* moved into required states. However the navigation is performed asynchronously in case of
* recursive invocations - e.g. `pop` is called from `onResume` lifecycle callback of a
* component being pushed. All recursive invocations are queued and performed one by one once
* the current navigation is finished.
*
* @param transformer transforms the current configuration stack to a new one.
* @param onComplete called when the navigation is finished (either synchronously or asynchronously).
*/
fun navigate(transformer: (stack: List<C>) -> List<C>)
fun navigate(transformer: (stack: List<C>) -> List<C>, onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit)
}
Original file line number Diff line number Diff line change
@@ -3,17 +3,31 @@ package com.arkivanov.decompose.router
import com.arkivanov.decompose.Child

/**
* Pushes the provided [configuration] at the top of the stack
* A convenience method for [Router.navigate].
*/
fun <C : Any> Router<C, *>.navigate(transformer: (stack: List<C>) -> List<C>) {
navigate(transformer = transformer, onComplete = { _, _ -> })
}

/**
* Pushes the provided [configuration] at the top of the stack..
*/
fun <C : Any> Router<C, *>.push(configuration: C) {
navigate { it + configuration }
}

/**
* Pops the latest configuration at the top of the stack
* Pops the latest configuration at the top of the stack.
*
* @param onComplete called when the navigation is finished (either synchronously or asynchronously).
* The `isSuccess` argument is `true` if the stack size was greater than 1 and a component was popped,
* `false` otherwise.
*/
fun <C : Any> Router<C, *>.pop() {
navigate { it.dropLast(1) }
fun <C : Any> Router<C, *>.pop(onComplete: (isSuccess: Boolean) -> Unit = {}) {
navigate(
transformer = { stack -> stack.takeIf { it.size > 1 }?.dropLast(1) ?: stack },
onComplete = { newStack, oldStack -> onComplete(newStack.size < oldStack.size) }
)
}

/**
@@ -24,7 +38,7 @@ inline fun <C : Any> Router<C, *>.popWhile(crossinline predicate: (C) -> Boolean
}

/**
* Replaces the current configuration at the top of the stack with the provided [configuration]
* Replaces the current configuration at the top of the stack with the provided [configuration].
*/
fun <C : Any> Router<C, *>.replaceCurrent(configuration: C) {
navigate { it.dropLast(1) + configuration }
Original file line number Diff line number Diff line change
@@ -83,7 +83,10 @@ inline fun <reified C : Parcelable, T : Any> ComponentContext.router(
* A convenience extension function for [ComponentContext.router].
*/
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated(message = "Please use ComponentContext.router extension function with initialStack argument")
@Deprecated(
message = "Please use ComponentContext.router extension function with initialStack argument",
level = DeprecationLevel.ERROR,
)
fun <C : Parcelable, T : Any> ComponentContext.router(
initialConfiguration: () -> C,
initialBackStack: () -> List<C> = ::emptyList,
@@ -103,8 +106,11 @@ fun <C : Parcelable, T : Any> ComponentContext.router(
/**
* A convenience extension function for [ComponentContext.router].
*/
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated(message = "Please use ComponentContext.router extension function with initialStack argument")
@Suppress("DeprecatedCallableAddReplaceWith", "DEPRECATION_ERROR")
@Deprecated(
message = "Please use ComponentContext.router extension function with initialStack argument",
level = DeprecationLevel.ERROR,
)
inline fun <reified C : Parcelable, T : Any> ComponentContext.router(
initialConfiguration: C,
initialBackStack: List<C> = emptyList(),
@@ -124,8 +130,11 @@ inline fun <reified C : Parcelable, T : Any> ComponentContext.router(
/**
* A convenience extension function for [ComponentContext.router].
*/
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated(message = "Please use ComponentContext.router extension function with initialStack argument")
@Suppress("DeprecatedCallableAddReplaceWith", "DEPRECATION_ERROR")
@Deprecated(
message = "Please use ComponentContext.router extension function with initialStack argument",
level = DeprecationLevel.ERROR,
)
inline fun <reified C : Parcelable, T : Any> ComponentContext.router(
noinline initialConfiguration: () -> C,
noinline initialBackStack: () -> List<C> = ::emptyList,
Original file line number Diff line number Diff line change
@@ -33,14 +33,16 @@ internal class RouterImpl<C : Any, T : Any>(
backPressedHandler.unregister(onBackPressedHandler)
}

override fun navigate(transformer: (stack: List<C>) -> List<C>) {
queue.offer(transformer)
override fun navigate(transformer: (stack: List<C>) -> List<C>, onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit) {
queue.offer(NavigationItem(transformer = transformer, onComplete = onComplete))
}

private fun navigateActual(transformer: (stack: List<C>) -> List<C>) {
val newStack = navigator.navigate(oldStack = stackHolder.stack, transformer = transformer)
private fun navigateActual(item: NavigationItem<C>) {
val oldStack = stackHolder.stack
val newStack = navigator.navigate(oldStack = oldStack, transformer = item.transformer)
stackHolder.stack = newStack
state.value = newStack.toState()
item.onComplete(newStack.configurationStack, oldStack.configurationStack)
}

private fun onBackPressed(): Boolean =
@@ -66,4 +68,9 @@ internal class RouterImpl<C : Any, T : Any>(
is RouterEntry.Created -> Child.Created(configuration = configuration, instance = instance)
is RouterEntry.Destroyed -> Child.Destroyed(configuration = configuration)
}

private class NavigationItem<C : Any>(
val transformer: (stack: List<C>) -> List<C>,
val onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.arkivanov.decompose.router

internal val <C : Any> RouterStack<C, *>.configurationBackStack: List<C>
get() =
object : AbstractList<C>() {
override val size: Int get() = backStack.size

override fun get(index: Int): C = backStack[index].configuration
}

internal val <C : Any> RouterStack<C, *>.configurationStack: List<C>
get() =
object : AbstractList<C>() {
override val size: Int get() = backStack.size + 1

override fun get(index: Int): C = (backStack.getOrNull(index) ?: active).configuration
}
Original file line number Diff line number Diff line change
@@ -11,7 +11,12 @@ internal class StackNavigatorImpl<C : Any, T : Any>(
) : StackNavigator<C, T> {

override fun navigate(oldStack: RouterStack<C, T>, transformer: (stack: List<C>) -> List<C>): RouterStack<C, T> {
val newConfigurationStack = transformer((oldStack.backStack + oldStack.active).map(RouterEntry<C, *>::configuration))
val oldConfigurationStack = oldStack.configurationStack
val newConfigurationStack = transformer(oldConfigurationStack)

if (newConfigurationStack === oldConfigurationStack) {
return oldStack
}

check(newConfigurationStack.isNotEmpty()) { "Configuration stack can not be empty" }
check(newConfigurationStack.isUnique()) { "Configurations in the stack must be unique" }
Original file line number Diff line number Diff line change
@@ -251,7 +251,5 @@ class ChildContextTest {
override val lifecycle: LifecycleRegistry = LifecycleRegistry()
override val stateKeeper: TestStateKeeperDispatcher = TestStateKeeperDispatcher(savedState)
override val backPressedHandler: BackPressedDispatcher = BackPressedDispatcher()

override val backPressedDispatcher: BackPressedDispatcher get() = TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.arkivanov.decompose.router

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

@Suppress("TestFunctionName")
class RouterPopTest {

@Test
fun GIVEN_stack_size_2_WHEN_pop_THEN_popped() {
val router = TestRouter(listOf(Config.A, Config.B))

router.pop()

assertEquals(listOf(Config.A), router.stack)
}

@Test
fun GIVEN_stack_size_2_WHEN_pop_THEN_onComplete_success() {
val router = TestRouter(listOf(Config.A, Config.B))
var isSuccess = false

router.pop { isSuccess = it }

assertTrue(isSuccess)
}

@Test
fun GIVEN_stack_size_1_WHEN_pop_THEN_not_popped() {
val router = TestRouter(listOf(Config.A))

router.pop()

assertEquals(listOf(Config.A), router.stack)
}


@Test
fun GIVEN_stack_size_1_WHEN_pop_THEN_onComplete_not_success() {
val router = TestRouter(listOf(Config.A))
var isSuccess = true

router.pop { isSuccess = it }

assertFalse(isSuccess)
}

private sealed class Config {
object A : Config()
object B : Config()
}
}
Original file line number Diff line number Diff line change
@@ -8,86 +8,147 @@ import com.arkivanov.essenty.parcelable.Parcelable
import com.arkivanov.essenty.parcelable.Parcelize
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse

@Suppress("TestFunctionName")
class RouterTest {

private val navigator = TestStackNavigator()

@Test
fun WHEN_navigate_THEN_new_stack_applied() {
val config1 = Config()
val config2 = Config()
val config3 = Config()
val config4 = Config()

val router = router(initialStack = listOf(config1, config2, config3))

navigator.add(
configs = listOf(config1, config3, config4),
stack = { routerStack(listOf(config1, config3, config4)) }
)

router.navigate { listOf(config1, config3, config4) }

assertEquals(listOf(config1, config3, config4), router.configurations)
}

@Test
fun WHEN_navigate_THEN_onComplete_called_synchronously() {
val config1 = Config()
val config2 = Config()
val config3 = Config()
val config4 = Config()
val router = router(initialStack = listOf(config1, config2, config3))

var resultNewStack: List<Config>? = null
var resultOldStack: List<Config>? = null
router.navigate(transformer = { listOf(config1, config3, config4) }) { newStack, oldStack ->
resultNewStack = newStack
resultOldStack = oldStack
}

assertEquals(listOf(config1, config3, config4), resultNewStack)
assertEquals(listOf(config1, config2, config3), resultOldStack)
}

@Test
fun WHEN_navigate_recursively_THEN_last_stack_applied() {
val config1 = Config()
val config2 = Config()
val config3 = Config()
val navigator = TestStackNavigator()
val router = router(initialStack = listOf(config1), navigator = navigator)

navigator.onNavigate =
{ newConfigs ->
if (newConfigs == listOf(config2)) {
router.navigate { listOf(config3) }
}
}

val router = router(initialStack = listOf(config1))
router.navigate { listOf(config2) }

navigator.add(
configs = listOf(config2),
stack = {
router.navigate { listOf(config3) }
routerStack(listOf(config2))
assertEquals(listOf(config3), router.configurations)
}


@Test
fun WHEN_navigate_recursively_THEN_onComplete_not_called_synchronously() {
val config1 = Config()
val config2 = Config()
val config3 = Config()
val navigator = TestStackNavigator()
val router = router(initialStack = listOf(config1), navigator = navigator)

var isCalledSynchronously = false
navigator.onNavigate =
{ newConfigs ->
if (newConfigs == listOf(config2)) {
var isCalled = false
router.navigate(transformer = { listOf(config3) }) { _, _ ->
isCalled = true
}
isCalledSynchronously = isCalled
}
}
)

navigator.add(
configs = listOf(config3),
stack = { routerStack(listOf(config3)) }
)
router.navigate { listOf(config2) }

assertFalse(isCalledSynchronously)
}


@Test
fun WHEN_navigate_recursively_THEN_onComplete_called() {
val config1 = Config()
val config2 = Config()
val config3 = Config()
val navigator = TestStackNavigator()
val router = router(initialStack = listOf(config1), navigator = navigator)

var resultNewStack: List<Config>? = null
var resultOldStack: List<Config>? = null
navigator.onNavigate =
{ newConfigs ->
if (newConfigs == listOf(config2)) {
router.navigate(transformer = { listOf(config3) }) { newStack, oldStack ->
resultNewStack = newStack
resultOldStack = oldStack
}
}
}

router.navigate { listOf(config2) }

assertEquals(listOf(config3), router.configurations)
assertEquals(listOf(config3), resultNewStack)
assertEquals(listOf(config2), resultOldStack)
}

private fun router(initialStack: List<Config>): Router<Config, Config> =
private fun router(initialStack: List<Config>, navigator: TestStackNavigator = TestStackNavigator()): Router<Config, Config> =
RouterImpl(
lifecycle = LifecycleRegistry(),
backPressedHandler = BackPressedDispatcher(),
popOnBackPressed = false,
stackHolder = TestStackHolder(routerStack(initialStack)),
navigator = navigator
navigator = navigator,
)

private fun routerStack(stack: List<Config>): RouterStack<Config, Config> =
RouterStack(
active = RouterEntry.Created(
configuration = stack.last(),
instance = stack.last(),
lifecycleRegistry = LifecycleRegistry(),
stateKeeperDispatcher = TestStateKeeperDispatcher(),
instanceKeeperDispatcher = InstanceKeeperDispatcher(),
backPressedDispatcher = BackPressedDispatcher()
),
backStack = stack.dropLast(1).map {
RouterEntry.Destroyed(configuration = it)
}
)

private val Router<Config, *>.configurations: List<Config>
get() = state.value.configurations

private val RouterState<Config, *>.configurations: List<Config>
get() = backStack.map { it.configuration } + activeChild.configuration
private companion object {
private fun routerStack(stack: List<Config>): RouterStack<Config, Config> =
RouterStack(
active = RouterEntry.Created(
configuration = stack.last(),
instance = stack.last(),
lifecycleRegistry = LifecycleRegistry(),
stateKeeperDispatcher = TestStateKeeperDispatcher(),
instanceKeeperDispatcher = InstanceKeeperDispatcher(),
backPressedDispatcher = BackPressedDispatcher()
),
backStack = stack.dropLast(1).map {
RouterEntry.Destroyed(configuration = it)
}
)

private val Router<Config, *>.configurations: List<Config>
get() = state.value.configurations

private val RouterState<Config, *>.configurations: List<Config>
get() = backStack.map { it.configuration } + activeChild.configuration
}

@Parcelize
private class Config : Parcelable
@@ -97,20 +158,17 @@ class RouterTest {
) : StackHolder<Config, Config>

private class TestStackNavigator : StackNavigator<Config, Config> {
private val map = HashMap<List<Config>, () -> RouterStack<Config, Config>>()
var onNavigate: (newConfigs: List<Config>) -> Unit = {}

override fun navigate(
oldStack: RouterStack<Config, Config>,
transformer: (stack: List<Config>) -> List<Config>
): RouterStack<Config, Config> {
val oldConfigs = (oldStack.backStack + oldStack.active).map { it.configuration }
val oldConfigs = oldStack.configurationStack
val newConfigs = transformer(oldConfigs)
onNavigate(newConfigs)

return map.getValue(newConfigs).invoke()
}

fun add(configs: List<Config>, stack: () -> RouterStack<Config, Config>) {
map[configs] = stack
return routerStack(newConfigs)
}
}
}
Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@ class StackNavigatorTest {
transformer = { it + Config() }
)

assertEquals(listOf(oldConfig), newStack.getConfigurationBackStack())
assertEquals(listOf(oldConfig), newStack.configurationBackStack)
}

@Test
@@ -69,37 +69,35 @@ class StackNavigatorTest {
transformer = { it + Config() }
)

assertEquals(listOf(oldConfig1, oldConfig2, oldConfig3), newStack.getConfigurationBackStack())
assertEquals(listOf(oldConfig1, oldConfig2, oldConfig3), newStack.configurationBackStack)
}

@Test
fun GIVEN_empty_back_stack_WHEN_push_THEN_old_component_not_recreated() {
val newStack =
navigator.navigate(
oldStack = RouterStack(
active = activeEntry(configuration = Config()),
backStack = emptyList()
),
transformer = { it + Config() }
val oldStack =
RouterStack(
active = activeEntry(configuration = Config()),
backStack = emptyList()
)

assertEquals(1, (newStack.backStack[0] as RouterEntry.Created).instance.instanceNumber)
val newStack = navigator.navigate(oldStack = oldStack, transformer = { it + Config() })

assertSame(newStack.backStack[0].asCreated().instance, oldStack.active.instance)
}

@Test
fun GIVEN_not_empty_back_stack_WHEN_push_THEN_old_components_not_recreated() {
val newStack =
navigator.navigate(
oldStack = RouterStack(
active = activeEntry(configuration = Config()),
backStack = listOf(createdEntry(configuration = Config()), createdEntry(configuration = Config()))
),
transformer = { it + Config() }
val oldStack =
RouterStack(
active = activeEntry(configuration = Config()),
backStack = listOf(createdEntry(configuration = Config()), createdEntry(configuration = Config()))
)

assertEquals(1, (newStack.backStack[0] as RouterEntry.Created).instance.instanceNumber)
assertEquals(1, (newStack.backStack[1] as RouterEntry.Created).instance.instanceNumber)
assertEquals(1, (newStack.backStack[2] as RouterEntry.Created).instance.instanceNumber)
val newStack = navigator.navigate(oldStack = oldStack, transformer = { it + Config() })

assertSame(oldStack.backStack[0].asCreated().instance, newStack.backStack[0].asCreated().instance)
assertSame(oldStack.backStack[1].asCreated().instance, newStack.backStack[1].asCreated().instance)
assertSame(oldStack.active.instance, newStack.backStack[2].asCreated().instance)
}

@Test
@@ -240,17 +238,16 @@ class StackNavigatorTest {

@Test
fun GIVEN_back_stack_WHEN_pop_THEN_old_components_not_recreated() {
val newStack =
navigator.navigate(
oldStack = RouterStack(
active = activeEntry(configuration = Config()),
backStack = listOf(createdEntry(configuration = Config()), createdEntry(configuration = Config()))
),
transformer = { it.dropLast(1) }
val oldStack =
RouterStack(
active = activeEntry(configuration = Config()),
backStack = listOf(createdEntry(configuration = Config()), createdEntry(configuration = Config()))
)

assertEquals(1, (newStack.backStack[0] as RouterEntry.Created).instance.instanceNumber)
assertEquals(1, newStack.active.instance.instanceNumber)
val newStack = navigator.navigate(oldStack = oldStack, transformer = { it.dropLast(1) })

assertSame(oldStack.backStack[0].asCreated().instance, newStack.backStack[0].asCreated().instance)
assertSame(oldStack.backStack[1].asCreated().instance, newStack.active.instance)
}

@Test
@@ -341,7 +338,7 @@ class StackNavigatorTest {
transformer = { newConfigurationStack }
)

assertEquals(newConfigurationStack, newStack.getConfigurationStack())
assertEquals(newConfigurationStack, newStack.configurationStack)
}

@Test
@@ -392,30 +389,33 @@ class StackNavigatorTest {
transformer = { newConfigurationStack }
)

assertEquals(newConfigurationStack, newStack.getConfigurationStack())
assertEquals(newConfigurationStack, newStack.configurationStack)
}

@Test
fun WHEN_navigate_to_partially_different_stack_with_new_active_component_THEN_same_old_components_not_recreated() {
val sameConfig1 = Config()
val sameConfig2 = Config()

val oldStack =
RouterStack(
active = activeEntry(configuration = Config()),
backStack = listOf(
createdEntry(configuration = Config()),
createdEntry(configuration = sameConfig1),
createdEntry(configuration = Config()),
createdEntry(configuration = sameConfig2)
)
)

val newStack =
navigator.navigate(
oldStack = RouterStack(
active = activeEntry(configuration = Config()),
backStack = listOf(
createdEntry(configuration = Config()),
createdEntry(configuration = sameConfig1),
createdEntry(configuration = Config()),
createdEntry(configuration = sameConfig2)
)
),
oldStack = oldStack,
transformer = { listOf(Config(), sameConfig1, sameConfig2, Config(), Config()) }
)

assertEquals(1, (newStack.backStack[1] as RouterEntry.Created).instance.instanceNumber)
assertEquals(1, (newStack.backStack[2] as RouterEntry.Created).instance.instanceNumber)
assertSame(oldStack.backStack[1].asCreated().instance, newStack.backStack[1].asCreated().instance)
assertSame(oldStack.backStack[3].asCreated().instance, newStack.backStack[2].asCreated().instance)
}

@Test
@@ -439,7 +439,7 @@ class StackNavigatorTest {
transformer = { newConfigurationStack }
)

assertEquals(newConfigurationStack, newStack.getConfigurationStack())
assertEquals(newConfigurationStack, newStack.configurationStack)
}

@Test
@@ -448,30 +448,62 @@ class StackNavigatorTest {
val sameConfig2 = Config()
val sameConfig3 = Config()

val oldStack =
RouterStack(
active = activeEntry(configuration = sameConfig3),
backStack = listOf(
createdEntry(configuration = Config()),
createdEntry(configuration = sameConfig1),
createdEntry(configuration = Config()),
createdEntry(configuration = sameConfig2)
)
)

val newStack =
navigator.navigate(
oldStack = RouterStack(
active = activeEntry(configuration = sameConfig3),
backStack = listOf(
createdEntry(configuration = Config()),
createdEntry(configuration = sameConfig1),
createdEntry(configuration = Config()),
createdEntry(configuration = sameConfig2)
)
),
oldStack = oldStack,
transformer = { listOf(Config(), sameConfig1, sameConfig2, Config(), sameConfig3) }
)

assertEquals(1, (newStack.backStack[1] as RouterEntry.Created).instance.instanceNumber)
assertEquals(1, (newStack.backStack[2] as RouterEntry.Created).instance.instanceNumber)
assertEquals(1, newStack.active.instance.instanceNumber)
assertSame(oldStack.backStack[1].asCreated().instance, newStack.backStack[1].asCreated().instance)
assertSame(oldStack.backStack[3].asCreated().instance, newStack.backStack[2].asCreated().instance)
assertSame(oldStack.active.instance, newStack.active.instance)
}

@Test
fun WHEN_navigate_to_equal_stack_THEN_stack_not_changed() {
val sameConfig1 = Config()
val sameConfig2 = Config()

val oldStack =
RouterStack(
active = activeEntry(configuration = sameConfig1),
backStack = listOf(createdEntry(configuration = sameConfig2))
)

val newStack = navigator.navigate(oldStack = oldStack, transformer = { listOf(sameConfig2, sameConfig1) })

assertEquals(oldStack, newStack)
}

private fun RouterStack<Config, *>.getConfigurationBackStack(): List<Config> =
backStack.map(RouterEntry<Config, *>::configuration)
@Test
fun WHEN_navigate_to_same_stack_THEN_stack_not_changed() {
val sameConfig1 = Config()
val sameConfig2 = Config()

private fun RouterStack<Config, *>.getConfigurationStack(): List<Config> =
getConfigurationBackStack() + active.configuration
val oldStack =
RouterStack(
active = activeEntry(configuration = sameConfig1),
backStack = listOf(createdEntry(configuration = sameConfig2))
)

val newStack = navigator.navigate(oldStack = oldStack, transformer = { it })

assertEquals(oldStack, newStack)
}

private fun <C : Any, T : Any> RouterEntry<C, T>.asCreated(): RouterEntry.Created<C, T> =
this as RouterEntry.Created<C, T>

private companion object {
private fun activeEntry(
@@ -519,29 +551,18 @@ class StackNavigatorTest {

private class Config

private data class Component(
val instanceNumber: Int = 1
)
private class Component

private class TestRouterEntryFactory : RouterEntryFactory<Config, Component> {
private val components = ArrayList<Pair<Config, Component>>()

override fun invoke(
configuration: Config,
savedState: ParcelableContainer?,
instanceKeeperDispatcher: InstanceKeeperDispatcher?
): RouterEntry.Created<Config, Component> {
var component: Component? = components.find { it.first === configuration }?.second
component = component?.copy(instanceNumber = component.instanceNumber + 1) ?: Component()

components.removeAll { it.first === configuration }
components += configuration to component

return createdEntry(
): RouterEntry.Created<Config, Component> =
createdEntry(
configuration = configuration,
component = component,
component = Component(),
stateKeeperDispatcher = TestStateKeeperDispatcher(savedState)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -27,7 +27,10 @@ class TestRouter<C : Any>(stack: List<C>) : Router<C, Any> {
}
)

override fun navigate(transformer: (stack: List<C>) -> List<C>) {
stack = transformer(stack)
override fun navigate(transformer: (stack: List<C>) -> List<C>, onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit) {
val oldStack = stack
val newStack = transformer(stack)
stack = newStack
onComplete(newStack, oldStack)
}
}
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import com.arkivanov.decompose.router.Router
import com.arkivanov.decompose.router.RouterState
import com.arkivanov.decompose.router.configurations
import com.arkivanov.decompose.router.findFirstDifferentIndex
import com.arkivanov.decompose.router.navigate
import com.arkivanov.decompose.router.startsWith
import com.arkivanov.decompose.router.subscribe
import org.w3c.dom.PopStateEvent
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package com.arkivanov.decompose.router.webhistory

import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.router.TestRouter
import com.arkivanov.decompose.router.navigate
import com.arkivanov.decompose.router.pop
import com.arkivanov.decompose.router.push
import com.arkivanov.essenty.parcelable.Parcelable
25 changes: 7 additions & 18 deletions deps.versions.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
[versions]

decompose = "0.5.2"
kotlin = "1.6.10"
decompose = "0.6.0-native-compose"
kotlin = "1.6.20"
essenty = "0.2.2"
reaktive = "1.2.0"
reaktive = "1.2.1"
junit = "4.13.2"
jetbrainsCompose = "1.1.0"
jetbrainsKotlinWrappers = "0.0.1-pre.213-kotlin-1.5.10"
jetpackCompose = "1.1.1"
jetpackComposeCompiler = "1.1.1"
jetbrainsCompose = "0.0.0-on-rebase-12-apr-2022-dev668"
jetbrainsKotlinWrappers = "0.0.1-pre.325-kotlin-1.6.10"
jetbrainsKotlinxCoroutines = "1.6.1"
androidGradle = "7.1.0"
androidMaterial = "1.3.0"
androidPlay = "1.10.0"
@@ -17,7 +16,6 @@ androidxAppcompat = "1.2.0"
androidxLifecycle = "2.3.1"
androidxActivity = "1.3.1"
androidxTestCore = "1.4.0"
muirwickMuirwickComponents = "0.8.2"

[libraries]

@@ -35,13 +33,7 @@ junit-junit = { group = "junit", name = "junit", version.ref = "junit" }
jetbrains-compose-composeGradlePlug = { group = "org.jetbrains.compose", name = "compose-gradle-plugin", version.ref = "jetbrainsCompose" }
jetbrains-compose-ui-uiTestJunit4 = { group = "org.jetbrains.compose.ui", name = "ui-test-junit4", version.ref = "jetbrainsCompose" }
jetbrains-kotlinWrappers-kotlinWrappersBom = { group = "org.jetbrains.kotlin-wrappers", name = "kotlin-wrappers-bom", version.ref = "jetbrainsKotlinWrappers" }

androidx-compose-compiler-compiler = { group = "androidx.compose.compiler", name = "compiler", version.ref = "jetpackCompose" }
androidx-compose-foundation-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "jetpackCompose" }
androidx-compose-material-material = { group = "androidx.compose.material", name = "material", version.ref = "jetpackCompose" }
androidx-compose-ui-uiTestJunit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "jetpackCompose" }
androidx-compose-ui-uiTestManifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "jetpackCompose" }
androidx-compose-ui-uiTooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "jetpackCompose" }
jetbrains-kotlinx-kotlinxCoroutinesSwing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "jetbrainsKotlinxCoroutines" }

android-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradle" }
android-material-material = { group = "com.google.android.material", name = "material", version.ref = "androidMaterial" }
@@ -53,6 +45,3 @@ androidx-lifecycle-lifecycleCommonJava8 = { group = "androidx.lifecycle", name =
androidx-activity-activityKtx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidxActivity" }
androidx-activity-activityCompose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" }
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" }

muirwik-muirwikComponents = { group = "com.ccfraser.muirwik", name = "muirwik-components", version.ref = "muirwickMuirwickComponents" }
# = { group = "", name = "", version.ref = "" }
95 changes: 94 additions & 1 deletion docs/component/overview.md
Original file line number Diff line number Diff line change
@@ -36,7 +36,100 @@ class Counter(
}
```

When instantiating a root component we have to create `ComponentContext` manually. There is [DefaultComponentContext](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/DefaultComponentContext.kt) which is the default implementation class of the `ComponentContext`. There are also handy helper functions provided by Jetpack/JetBrains Compose extension modules.
## Root ComponentContext

When instantiating a root component, the `ComponentContext` should be created manually. There is [DefaultComponentContext](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/DefaultComponentContext.kt) which is the default implementation class of the `ComponentContext`.

### Root ComponentContext in Android

Decompose provides a few handy [helper functions](https://github.com/arkivanov/Decompose/blob/master/decompose/src/androidMain/kotlin/com/arkivanov/decompose/DefaultComponentContextBuilder.kt) for creating the root `ComponentContext` in Android. The preferred way is to create the root `ComponentContext` in an `Activity` or a `Fragment`.

#### Root ComponentContext in Activity

For this case Decompose provides `defaultComponentContext()` extension function, which can be called in scope of an `Activity`.

#### Root ComponentContext in Fragment

The `defaultComponentContext()` extension function can not be used in a Fragment. This is because the `Fragment` class does not implement the `OnBackPressedDispatcherOwner` interface, and so by default can't handle back button events. It is advised to use the Android-specific `DefaultComponentContext(AndroidLifecycle, SavedStateRegistry?, ViewModelStore?, OnBackPressedDispatcher?)` factory function, and supply all the arguments manually. The first three arguments (`AndroidLifecycle`, `SavedStateRegistry` and `ViewModelStore`) can be obtained directly from `Fragment`. However the last argument `OnBackPressedDispatcher` - can not. If you don't need to handle back button events in your Decompose components, then you can just ignore this argument. Otherwise, a manual solution is required.

> ⚠️ Don't take any argument values from the hosting `Activity` (e.g. `requireActivity().onBackPressedDispatcher`), as it may produce memory leaks.
Here is an example with using Decompose in a `DialogFragment`.

```kotlin
class MyFragment : DialogFragment() {
// Create custom OnBackPressedDispatcher
private val onBackPressedDispatcher = OnBackPressedDispatcher(::dismiss)

private lateinit var root: Root

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

root =
Root(
DefaultComponentContext(
lifecycle = lifecycle,
savedStateRegistry = savedStateRegistry,
viewModelStore = viewModelStore,
onBackPressedDispatcher = onBackPressedDispatcher,
)
)
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
object : Dialog(requireContext(), theme) {
override fun onBackPressed() {
onBackPressedDispatcher.onBackPressed()
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
// Start Compose here
}
```

### Root ComponentContext in Jetpack/JetBrains Compose

It is advised to not create the root `ComponentContext` (and a root component) directly in a `Composable` function. Compositions may be performed in a background thread, which may brake things. The preferred way is to create the root component outside of Compose.

> ⚠️ If you can't avoid creating the root component in a `Composable` function, please make sure you use `remember`. This will prevent the root component and its `ComponentContext` from being recreated on each composition.
#### Android with Compose

Prefer creating the root `ComponentContext` (and a root component) before starting Compose, e.g. in an `Activity` or a `Fragment`.

```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Create the root component before starting Compose
val root = RootComponent(componentContext = defaultComponentContext())

// Start Compose
setContent {
// The rest of the code
}
}
}
```

#### Other platforms with Compose

Prefer creating the root `ComponentContext` (and a root component) before starting Compose, e.g. in directly in the `main` function.

```kotlin
fun main() {
// Create the root component before starting Compose
val root = RootComponent(componentContext = DefaultComponentContext(...))

// Start Compose
application {
// The rest of the code
}
}
```

## Child components

162 changes: 121 additions & 41 deletions docs/extensions/compose.md
Original file line number Diff line number Diff line change
@@ -54,11 +54,40 @@ interface SomeComponent {
}

@Composable
fun SomeUi(component: SomeComponent) {
fun SomeContent(component: SomeComponent) {
val models: State<Model> by component.models.subscribeAsState()
}
```

### Controlling the Lifecycle on Desktop

When using JetBrains Compose, you can have a `LifecycleRegistry` react to changes in the window state using the `LifecycleController()` composable. This will trigger appropriate lifecycle events when the window is minimized, restored or closed.

It is also possible to manually start the lifecycle using `LifecycleRegistry.resume()` when the instance is created.

```kotlin
fun main() {
val lifecycle = LifecycleRegistry()
val root = RootComponent(DefaultComponentContext(lifecycle))

// Alternative: manually start the lifecycle (no reaction to window state)
// lifecycle.resume()

application {
val windowState = rememberWindowState()

// Bind the registry to the life cycle of the window
LifecycleController(lifecycle, windowState)

Window(state = windowState, ...) {
// The rest of your content
}
}
}
```

> ⚠️ When using Compose in desktop platforms, make sure to always use one of the methods above, or your components might not receive lifecycle events correctly.
### Navigating between Composable components

The [Router](https://arkivanov.github.io/Decompose/router/overview/) provides
@@ -79,99 +108,124 @@ interface RootComponent {
val routerState: Value<RouterState<*, Child>>

sealed class Child {
data class Profile(val component: ProfileComponent) : Child()
data class Settings(val component: SettingsComponent) : Child()
data class Main(val component: MainComponent) : Child()
data class Details(val component: DetailsComponent) : Child()
}
}

@Composable
fun RootUi(rootComponent: RootComponent) {
fun RootContent(rootComponent: RootComponent) {
Children(rootComponent.routerState) {
when (val child = it.instance) {
is RootComponent.Child.Profile -> ProfileUi(child.component)
is RootComponent.Child.Settings -> SettingsUi(child.component)
is RootComponent.Child.Main -> MainContent(child.component)
is RootComponent.Child.Details -> DetailsContent(child.component)
}
}
}

// Children

interface ProfileComponent
interface MainComponent

interface SettingsComponent
interface DetailsComponent

@Composable
fun ProfileUi(profileComponent: ProfileComponent) {
fun MainContent(profileComponent: MainComponent) {
// Omitted code
}

@Composable
fun SettingsUi(settingsComponent: SettingsComponent) {
fun DetailsContent(settingsComponent: DetailsComponent) {
// Omitted code
}
```

### Animations
### Animations (experimental)

Decompose provides the [Child Animation API](https://github.com/arkivanov/Decompose/tree/master/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/animation/child) for Compose, as well as some predefined animation specs. To enable child animations you need to pass the `animation` argument to the `Children` function.
Decompose provides the [Child Animation API](https://github.com/arkivanov/Decompose/tree/master/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/animation/child) for Compose, as well as some predefined animation specs. To enable child animations you need to pass the `animation` argument to the `Children` function. There are predefined animators provided by Decompose.

#### Crossfade animation
#### Fade animation

```kotlin
@Composable
fun RootUi(rootComponent: RootComponent) {
fun RootContent(rootComponent: RootComponent) {
Children(
routerState = rootComponent.routerState,
animation = crossfade()
animation = childAnimation(fade())
) {
// Omitted code
}
}
```

<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/ComposeAnimationCrossfade.gif" width="512">
<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/ComposeAnimationFade.gif" width="512">

#### Crossfade-Scale animation
#### Slide animation

```kotlin
@Composable
fun RootUi(rootComponent: RootComponent) {
fun RootContent(rootComponent: RootComponent) {
Children(
routerState = rootComponent.routerState,
animation = crossfadeScale()
animation = childAnimation(slide())
) {
// Omitted code
}
}
```

<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/ComposeAnimationCrossfadeScale.gif" width="512">
<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/ComposeAnimationSlide.gif" width="512">

#### Slide animation
#### Combining animators

It is also possible to combine animators using the `plus` operator. Please note that the order matters - the right animator is applied after the left animator.

```kotlin
@Composable
fun RootUi(rootComponent: RootComponent) {
fun RootContent(rootComponent: RootComponent) {
Children(
routerState = rootComponent.routerState,
animation = slide()
animation = childAnimation(fade() + scale())
) {
// Omitted code
}
}
```

<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/ComposeAnimationSlide.gif" width="512">
<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/ComposeAnimationFadeScale.gif" width="512">

#### Separate animations for children

Previous examples demonstrate simple cases, when all children have the same animation. But it is also possible to specify separate animations for children.

```kotlin
@Composable
fun RootContent(rootComponent: RootComponent) {
Children(
routerState = rootComponent.routerState,
animation = childAnimation { child, direction ->
when (child.instance) {
is RootComponent.Child.Main -> fade() + scale()
is RootComponent.Child.Details -> fade() + slide()
}
}
) {
// Omitted code
}
}
```

<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/ComposeAnimationSeparate.gif" width="512">

#### Custom animations

It is possible to define custom animations in two ways.
It is also possible to define custom animations.

Implementing the `ChildAnimation` manually:
Implementing `ChildAnimation` manually. This is the most flexible low-level API. The animation block receives the current `RouterState` and animates children using the provided `content` slot.

```kotlin
@Composable
fun RootUi(rootComponent: RootComponent) {
fun RootContent(rootComponent: RootComponent) {
Children(
routerState = rootComponent.routerState,
animation = someAnimation()
@@ -181,27 +235,53 @@ fun RootUi(rootComponent: RootComponent) {
}

fun <C : Any, T : Any> someAnimation(): ChildAnimation<C, T> =
{ routerState: RouterState<C, T>, childContent: ChildContent<C, T> ->
ChildAnimation { routerState: RouterState<C, T>,
modifier: Modifier,
content: @Composable (Child.Created<C, T>) -> Unit ->
// Render each frame here
}
```

Implementing the `ChildAnimation` using the `childAnimation` builder function:
Using the `childAnimation` helper function and implementing `ChildAnimator`. The `childAnimation` function takes care of tracking the `RouterState` changes. `ChildAnimator` is only responsible for manipulating the `Modifier` in the given `direction`, and calling `onFinished` at the end.

```kotlin
@Composable
fun RootContent(rootComponent: RootComponent) {
Children(
routerState = rootComponent.routerState,
animation = childAnimation(someAnimator())
) {
// Omitted code
}
}

fun someAnimator(): ChildAnimator =
ChildAnimator { direction: Direction,
onFinished: () -> Unit,
content: @Composable (Modifier) -> Unit ->
// Manipulate the Modifier in the given direction and call onFinished at the end
}
```

Using `childAnimation` and `childAnimator` helper functions. This is the simplest, but less powerful way. The `childAnimator` function takes care of running the animation. Its block has a very limited responsibility - to render the current frame using the provided `factor` and `direction`.

```kotlin
fun <C : Any, T : Any> someAnimation(
animationSpec: FiniteAnimationSpec<Float> = defaultChildAnimationSpec,
): ChildAnimation<C, T> =
childAnimation(
animationSpec = animationSpec
@Composable
fun RootContent(rootComponent: RootComponent) {
Children(
routerState = rootComponent.routerState,
animation = childAnimation(someAnimator())
) {
child: Child.Created<C, T>,
factor: Float,
placement: ChildPlacement,
direction: ChildAnimationDirection,
content: @Composable () -> Unit ->
// Render the current frame here
// Omitted code
}
}

fun someAnimator(): ChildAnimator =
childAnimator { factor: Float,
direction: Direction,
content: (Modifier) -> Unit ->
// Render the current frame
}
```

Please refer to the predefined animations for implementation examples.
Please refer to the predefined animators (`fade`, `slide`, etc.) for implementation examples.
File renamed without changes
File renamed without changes
Binary file added docs/media/ComposeAnimationSeparate.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 18 additions & 4 deletions docs/router/navigation.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
# Navigation

## Router
## The Router

All navigation in Decompose is done through the [`Router`](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/Router.kt) interface. It has one function `navigate(transformer: (List<C>) -> List<C>)` which transforms the current stack of configurations into a new one by the provided `transformer` function. The stack is represented as `List`, where the last element is the top of the stack, and the first element is the bottom of the stack.
All navigation in Decompose is done through the [`Router`](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/Router.kt) interface. There is `navigate(transformer: (List<C>) -> List<C>, onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit)` method with two arguments:

> ⚠️ The returned stack must not be empty.
- `transformer` - converts the current stack of configurations into a new one. The stack is represented as `List`, where the last element is the top of the stack, and the first element is the bottom of the stack.
- `onComplete` - called when navigation is finished.

The navigation is always performed synchronously during the `navigate` method call. The only exception to this rule is when the `navigate` method is called recursively. All recursive invocations are queued and performed one by one once the current navigation is finished.
There is also `navigate(transformer: (stack: List<C>) -> List<C>)` extension function for convenience, without the `onComplete` callback.

> ⚠️ The configuration stack returned by the `transformer` function must not be empty.
### The navigation process

During the navigation process, the `Router` compares the new stack of configurations with the previous one. The `Router` ensures that all removed components are destroyed, and that there is only one component resumed at a time - the top one. All components in the back stack are always either stopped or destroyed.

The `Router` usually performs the navigation synchronously, which means that by the time the `navigate` method returns, the navigation is finished and all component lifecycles are moved into required states. However the navigation is performed asynchronously in case of recursive invocations - e.g. `pop` is called from `onResume` lifecycle callback of a component being pushed. All recursive invocations are queued and performed one by one once the current navigation is finished.

## Router extension functions

There are `Router` [extension functions](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterExt.kt) that provide conveniences for navigating, some of which were already used in the [router overview example](../overview/#routing-example).
@@ -46,6 +53,13 @@ Pops the latest configuration at the top of the stack.
router.pop()
```

```kotlin
router.pop { isSuccess ->
// Called when the navigation is finished.
// isSuccess - `true` if the stack size was greater than 1 and a component was popped, `false` otherwise.
}
```

![](../media/RouterPop.png)

### Pop While
Original file line number Diff line number Diff line change
@@ -6,68 +6,72 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/BuildCon
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/ChildrenKt {
public static final fun Children (Lcom/arkivanov/decompose/router/RouterState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
public static final fun Children (Lcom/arkivanov/decompose/value/Value;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/RootComponentBuilderKt {
public static final fun rememberRootComponent (Landroidx/savedstate/SavedStateRegistry;Landroidx/lifecycle/ViewModelStore;Landroidx/activity/OnBackPressedDispatcher;Landroidx/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Ljava/lang/Object;
public static final fun rememberRootComponent (Landroidx/savedstate/SavedStateRegistryOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object;
public static final fun Children (Lcom/arkivanov/decompose/router/RouterState;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
public static final fun Children (Lcom/arkivanov/decompose/value/Value;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/SubscribeAsStateKt {
public static final fun subscribeAsState (Lcom/arkivanov/decompose/value/Value;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/ValueComposableKt {
public static final fun asState (Lcom/arkivanov/decompose/value/Value;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
public abstract interface class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation {
public abstract fun invoke (Lcom/arkivanov/decompose/router/RouterState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)V
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection : java/lang/Enum {
public static final field ENTER Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection;
public static final field EXIT Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection;
public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection;
public static fun values ()[Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection;
public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationKt {
public static final fun ChildAnimation (Lkotlin/jvm/functions/Function5;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;
public static final fun childAnimation (Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;
public static final fun childAnimation (Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;
public static synthetic fun childAnimation$default (Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationKt {
public static final fun childAnimation (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function7;)Lkotlin/jvm/functions/Function5;
public static synthetic fun childAnimation$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function7;ILjava/lang/Object;)Lkotlin/jvm/functions/Function5;
public abstract interface class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator {
public abstract fun invoke (Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)V
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement : java/lang/Enum {
public static final field BACK Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement;
public static final field FRONT Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement;
public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement;
public static fun values ()[Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement;
public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimatorKt {
public static final fun ChildAnimator (Lkotlin/jvm/functions/Function5;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static final fun childAnimator (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static synthetic fun childAnimator$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static final fun plus (Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$EmptyChildAnimationKt {
public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$EmptyChildAnimationKt;
public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$FadeKt {
public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$FadeKt;
public static field lambda-1 Lkotlin/jvm/functions/Function5;
public fun <init> ()V
public final fun getLambda-1$extensions_compose_jetbrains_release ()Lkotlin/jvm/functions/Function5;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$SlideKt {
public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$SlideKt;
public static field lambda-1 Lkotlin/jvm/functions/Function7;
public static field lambda-1 Lkotlin/jvm/functions/Function5;
public fun <init> ()V
public final fun getLambda-1$extensions_compose_jetbrains_release ()Lkotlin/jvm/functions/Function7;
public final fun getLambda-1$extensions_compose_jetbrains_release ()Lkotlin/jvm/functions/Function5;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction : java/lang/Enum {
public static final field ENTER_BACK Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static final field ENTER_FRONT Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static final field EXIT_BACK Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static final field EXIT_FRONT Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static final field IDLE Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static fun values ()[Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/CrossfadeKt {
public static final fun crossfade (Landroidx/compose/animation/core/FiniteAnimationSpec;)Lkotlin/jvm/functions/Function5;
public static synthetic fun crossfade$default (Landroidx/compose/animation/core/FiniteAnimationSpec;ILjava/lang/Object;)Lkotlin/jvm/functions/Function5;
public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/FadeKt {
public static final fun fade (Landroidx/compose/animation/core/FiniteAnimationSpec;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static synthetic fun fade$default (Landroidx/compose/animation/core/FiniteAnimationSpec;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/CrossfadeScaleKt {
public static final fun crossfadeScale (Landroidx/compose/animation/core/FiniteAnimationSpec;FF)Lkotlin/jvm/functions/Function5;
public static synthetic fun crossfadeScale$default (Landroidx/compose/animation/core/FiniteAnimationSpec;FFILjava/lang/Object;)Lkotlin/jvm/functions/Function5;
public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ScaleKt {
public static final fun scale (Landroidx/compose/animation/core/FiniteAnimationSpec;FF)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static synthetic fun scale$default (Landroidx/compose/animation/core/FiniteAnimationSpec;FFILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/SlideKt {
public static final fun slide (Landroidx/compose/animation/core/FiniteAnimationSpec;)Lkotlin/jvm/functions/Function5;
public static synthetic fun slide$default (Landroidx/compose/animation/core/FiniteAnimationSpec;ILjava/lang/Object;)Lkotlin/jvm/functions/Function5;
public static final fun slide (Landroidx/compose/animation/core/FiniteAnimationSpec;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static synthetic fun slide$default (Landroidx/compose/animation/core/FiniteAnimationSpec;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
}

Original file line number Diff line number Diff line change
@@ -1,66 +1,71 @@
public final class com/arkivanov/decompose/extensions/compose/jetbrains/ChildrenKt {
public static final fun Children (Lcom/arkivanov/decompose/router/RouterState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
public static final fun Children (Lcom/arkivanov/decompose/value/Value;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/RootComponentBuilderKt {
public static final fun rememberRootComponent (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Lcom/arkivanov/essenty/backpressed/BackPressedDispatcher;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Ljava/lang/Object;
public static final fun Children (Lcom/arkivanov/decompose/router/RouterState;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
public static final fun Children (Lcom/arkivanov/decompose/value/Value;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/SubscribeAsStateKt {
public static final fun subscribeAsState (Lcom/arkivanov/decompose/value/Value;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/ValueComposableKt {
public static final fun asState (Lcom/arkivanov/decompose/value/Value;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
public abstract interface class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation {
public abstract fun invoke (Lcom/arkivanov/decompose/router/RouterState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)V
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection : java/lang/Enum {
public static final field ENTER Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection;
public static final field EXIT Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection;
public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection;
public static fun values ()[Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationDirection;
public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationKt {
public static final fun ChildAnimation (Lkotlin/jvm/functions/Function5;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;
public static final fun childAnimation (Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;
public static final fun childAnimation (Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;
public static synthetic fun childAnimation$default (Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimation;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimationKt {
public static final fun childAnimation (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function7;)Lkotlin/jvm/functions/Function5;
public static synthetic fun childAnimation$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function7;ILjava/lang/Object;)Lkotlin/jvm/functions/Function5;
public abstract interface class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator {
public abstract fun invoke (Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)V
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement : java/lang/Enum {
public static final field BACK Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement;
public static final field FRONT Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement;
public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement;
public static fun values ()[Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildPlacement;
public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimatorKt {
public static final fun ChildAnimator (Lkotlin/jvm/functions/Function5;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static final fun childAnimator (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static synthetic fun childAnimator$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static final fun plus (Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$EmptyChildAnimationKt {
public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$EmptyChildAnimationKt;
public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$FadeKt {
public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$FadeKt;
public static field lambda-1 Lkotlin/jvm/functions/Function5;
public fun <init> ()V
public final fun getLambda-1$extensions_compose_jetbrains ()Lkotlin/jvm/functions/Function5;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$SlideKt {
public static final field INSTANCE Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ComposableSingletons$SlideKt;
public static field lambda-1 Lkotlin/jvm/functions/Function7;
public static field lambda-1 Lkotlin/jvm/functions/Function5;
public fun <init> ()V
public final fun getLambda-1$extensions_compose_jetbrains ()Lkotlin/jvm/functions/Function7;
public final fun getLambda-1$extensions_compose_jetbrains ()Lkotlin/jvm/functions/Function5;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction : java/lang/Enum {
public static final field ENTER_BACK Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static final field ENTER_FRONT Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static final field EXIT_BACK Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static final field EXIT_FRONT Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static final field IDLE Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
public static fun values ()[Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/Direction;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/CrossfadeKt {
public static final fun crossfade (Landroidx/compose/animation/core/FiniteAnimationSpec;)Lkotlin/jvm/functions/Function5;
public static synthetic fun crossfade$default (Landroidx/compose/animation/core/FiniteAnimationSpec;ILjava/lang/Object;)Lkotlin/jvm/functions/Function5;
public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/FadeKt {
public static final fun fade (Landroidx/compose/animation/core/FiniteAnimationSpec;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static synthetic fun fade$default (Landroidx/compose/animation/core/FiniteAnimationSpec;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/CrossfadeScaleKt {
public static final fun crossfadeScale (Landroidx/compose/animation/core/FiniteAnimationSpec;FF)Lkotlin/jvm/functions/Function5;
public static synthetic fun crossfadeScale$default (Landroidx/compose/animation/core/FiniteAnimationSpec;FFILjava/lang/Object;)Lkotlin/jvm/functions/Function5;
public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ScaleKt {
public static final fun scale (Landroidx/compose/animation/core/FiniteAnimationSpec;FF)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static synthetic fun scale$default (Landroidx/compose/animation/core/FiniteAnimationSpec;FFILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/animation/child/SlideKt {
public static final fun slide (Landroidx/compose/animation/core/FiniteAnimationSpec;)Lkotlin/jvm/functions/Function5;
public static synthetic fun slide$default (Landroidx/compose/animation/core/FiniteAnimationSpec;ILjava/lang/Object;)Lkotlin/jvm/functions/Function5;
public static final fun slide (Landroidx/compose/animation/core/FiniteAnimationSpec;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
public static synthetic fun slide$default (Landroidx/compose/animation/core/FiniteAnimationSpec;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/animation/child/ChildAnimator;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/lifecycle/LifecycleControllerKt {
9 changes: 8 additions & 1 deletion extensions-compose-jetbrains/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -8,7 +8,13 @@ plugins {
}

setupMultiplatform {
targets(Target.Android, Target.Jvm)
targets(
Target.Android,
Target.Jvm,
Target.MacOs(),
Target.Ios(isAppleSiliconEnabled = false),
)

publications()
binaryCompatibilityValidator()
}
@@ -31,6 +37,7 @@ kotlin {
named("jvmTest") {
dependencies {
implementation(deps.jetbrains.compose.ui.uiTestJunit4)
implementation(deps.jetbrains.kotlinx.kotlinxCoroutinesSwing)
implementation(deps.junit.junit)
implementation(compose.desktop.currentOs)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -7,13 +7,15 @@ import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetbrains.animation.child.ChildAnimation
import com.arkivanov.decompose.extensions.compose.jetbrains.animation.child.emptyChildAnimation
import com.arkivanov.decompose.router.RouterState
import com.arkivanov.decompose.value.Value

typealias ChildContent<C, T> = @Composable (child: Child.Created<C, T>) -> Unit

@OptIn(ExperimentalDecomposeApi::class)
@Composable
fun <C : Any, T : Any> Children(
routerState: RouterState<C, T>,
@@ -32,6 +34,7 @@ fun <C : Any, T : Any> Children(
}
}

@OptIn(ExperimentalDecomposeApi::class)
@Composable
fun <C : Any, T : Any> Children(
routerState: Value<RouterState<C, T>>,

This file was deleted.

Original file line number Diff line number Diff line change
@@ -2,135 +2,62 @@

package com.arkivanov.decompose.extensions.compose.jetbrains.animation.child

import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.isFinished
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetbrains.ChildContent

internal val defaultChildAnimationSpec: FiniteAnimationSpec<Float> = tween()
import com.arkivanov.decompose.router.RouterState

/**
* A handy API for the [Children][com.arkivanov.decompose.extensions.compose.jetbrains.Children] animations
*
* @param animationSpec a [FiniteAnimationSpec] to configure the animation
* @param animator see [ChildAnimator] for details
* Tracks the [RouterState] changes and animates between child widget.
*/
@ExperimentalDecomposeApi
fun <C : Any, T : Any> childAnimation(
animationSpec: FiniteAnimationSpec<Float> = defaultChildAnimationSpec,
animator: ChildAnimator<C, T>
): ChildAnimation<C, T> =
{ routerState, modifier, content ->
ChildAnimationImpl(
targetPage = Page(routerState.activeChild, routerState.backStack.size),
modifier = modifier,
animationSpec = animationSpec,
animator = animator,
content = content
)
}

@Suppress("TransitionPropertiesLabel")
@Composable
private fun <C : Any, T : Any> ChildAnimationImpl(
targetPage: Page<C, T>,
modifier: Modifier,
animationSpec: FiniteAnimationSpec<Float>,
animator: ChildAnimator<C, T>,
content: ChildContent<C, T>,
) {
var pages: Pages<C, T> by remember { mutableStateOf(Pages(target = targetPage)) }
if (targetPage.configuration != pages.target.configuration) {
pages = Pages(target = targetPage, previous = pages.target)
}

val new = pages.target
val old = pages.previous

val animationState = remember(new.configuration) { AnimationState(if (old == null) 1F else 0F) }
interface ChildAnimation<C : Any, T : Any> {

LaunchedEffect(new.configuration) {
animationState.animateTo(
targetValue = 1F,
animationSpec = animationSpec,
sequentialAnimation = !animationState.isFinished
)

pages = Pages(target = pages.target)
}

val items = rememberAnimationItems(targetPage = new, previousPage = old)

Box(modifier = modifier) {
items.forEach { item ->
key(item.page.child.configuration) {
animator(
item.page.child,
when (item.direction) {
ChildAnimationDirection.ENTER -> animationState.value
ChildAnimationDirection.EXIT -> 1F - animationState.value
},
item.placement,
item.direction
) {
content(item.page.child)
}
}
}
}
@Composable
operator fun invoke(state: RouterState<C, T>, modifier: Modifier, content: @Composable (child: Child.Created<C, T>) -> Unit)
}

@Composable
private fun <C : Any, T : Any> rememberAnimationItems(
targetPage: Page<C, T>,
previousPage: Page<C, T>?
): List<AnimationItem<C, T>> =
remember(targetPage.configuration, previousPage?.configuration) {
when {
previousPage == null ->
listOf(AnimationItem(targetPage, ChildPlacement.BACK, ChildAnimationDirection.ENTER))

targetPage.index >= previousPage.index ->
listOf(
AnimationItem(previousPage, ChildPlacement.BACK, ChildAnimationDirection.EXIT),
AnimationItem(targetPage, ChildPlacement.FRONT, ChildAnimationDirection.ENTER),
)

else ->
listOf(
AnimationItem(targetPage, ChildPlacement.BACK, ChildAnimationDirection.ENTER),
AnimationItem(previousPage, ChildPlacement.FRONT, ChildAnimationDirection.EXIT),
)
/**
* Factory function for [ChildAnimation] while `fun interface` with a `@Composable` function
* is not supported - [b/221488059](https://issuetracker.google.com/issues/221488059).
*/
@Suppress("FunctionName") // Factory function
@ExperimentalDecomposeApi
inline fun <C : Any, T : Any> ChildAnimation(
crossinline content: @Composable (
state: RouterState<C, T>,
modifier: Modifier,
content: @Composable (child: Child.Created<C, T>) -> Unit
) -> Unit
): ChildAnimation<C, T> =
object : ChildAnimation<C, T> {
@Composable
override operator fun invoke(
state: RouterState<C, T>,
modifier: Modifier,
content: @Composable (child: Child.Created<C, T>) -> Unit
) {
content(state, modifier, content)
}
}

private class Page<out C : Any, out T : Any>(
val child: Child.Created<C, T>,
val index: Int,
) {
val configuration: C = child.configuration
}

private class Pages<out C : Any, out T : Any>(
val target: Page<C, T>,
val previous: Page<C, T>? = null,
)
/**
* Creates an implementation of [ChildAnimation] that allows different [ChildAnimator]s.
*
* @param selector provides a [ChildAnimator] for [Child] and [Direction].
*/
@ExperimentalDecomposeApi
fun <C : Any, T : Any> childAnimation(
selector: (child: Child.Created<C, T>, direction: Direction) -> ChildAnimator
): ChildAnimation<C, T> =
DefaultChildAnimation(selector)

private class AnimationItem<out C : Any, out T : Any>(
val page: Page<C, T>,
val placement: ChildPlacement,
val direction: ChildAnimationDirection,
)
/**
* Creates an implementation of [ChildAnimation] with the provided [ChildAnimator].
*
* @param animator a [ChildAnimator] to be used for animation, default is [fade].
*/
@ExperimentalDecomposeApi
fun <C : Any, T : Any> childAnimation(animator: ChildAnimator = fade()): ChildAnimation<C, T> =
childAnimation { _, _ -> animator }

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.animation.child

import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.ExperimentalDecomposeApi

/**
* Animates a child widget in the given [Direction].
*/
@ExperimentalDecomposeApi
interface ChildAnimator {

/**
* Animates the [content] in the given [Direction], and calls [onFinished] at the end.
*/
@Composable
operator fun invoke(direction: Direction, onFinished: () -> Unit, content: @Composable (Modifier) -> Unit)
}

/**
* Factory function for [ChildAnimator] while `fun interface` with a `@Composable` function
* is not supported - [b/221488059](https://issuetracker.google.com/issues/221488059).
*/
@ExperimentalDecomposeApi
inline fun ChildAnimator(
crossinline content: @Composable (Direction, onFinished: () -> Unit, content: @Composable (Modifier) -> Unit) -> Unit
): ChildAnimator =
object : ChildAnimator {
@Composable
override operator fun invoke(direction: Direction, onFinished: () -> Unit, content: @Composable (Modifier) -> Unit) {
content(direction, onFinished, content)
}
}

/**
* Creates an implementation of [ChildAnimator] with a convenient frame-by-frame rendering.
*
* @param animationSpec a [FiniteAnimationSpec] to configure the animation.
* @param frame renders the `content` using the provided `factor` and [Direction]. Called for every animation frame.
* The `factor` argument changes as follows:
* - Always 0F for [Direction.IDLE]
* - From 1F to 0F for [Direction.ENTER_FRONT]
* - From 0F to 1F for [Direction.EXIT_FRONT]
* - From -1F to 0F for [Direction.ENTER_BACK]
* - From 0F to -1F for [Direction.EXIT_BACK]
*/
@ExperimentalDecomposeApi
fun childAnimator(
animationSpec: FiniteAnimationSpec<Float> = tween(),
frame: @Composable (factor: Float, direction: Direction, content: @Composable (Modifier) -> Unit) -> Unit
): ChildAnimator =
DefaultChildAnimator(
animationSpec = animationSpec,
frame = frame
)

/**
* Combines (merges) the receiver [ChildAnimator] with the [other] [ChildAnimator].
*/
@ExperimentalDecomposeApi
operator fun ChildAnimator.plus(other: ChildAnimator): ChildAnimator =
ChildAnimator { direction, onFinished, content ->
val finished = remember(direction) { BooleanArray(2) }

this(
direction = direction,
onFinished = {
finished[0] = true
if (finished.all { it }) {
onFinished()
}
},
) { thisModifier ->
other(
direction = direction,
onFinished = {
finished[1] = true
if (finished.all { it }) {
onFinished()
}
},
) { otherModifier ->
content(thisModifier.then(otherModifier))
}
}
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
@file:OptIn(ExperimentalDecomposeApi::class)

package com.arkivanov.decompose.extensions.compose.jetbrains.animation.child

import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerInput
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.router.RouterState

@ExperimentalDecomposeApi
internal class DefaultChildAnimation<C : Any, T : Any>(
private val selector: (child: Child.Created<C, T>, direction: Direction) -> ChildAnimator
) : ChildAnimation<C, T> {

@Composable
override operator fun invoke(state: RouterState<C, T>, modifier: Modifier, content: @Composable (child: Child.Created<C, T>) -> Unit) {
var activePage by remember { mutableStateOf(state.activePage()) }
var items by remember { mutableStateOf(getAnimationItems(newPage = activePage, oldPage = null)) }

if (state.activeChild.configuration != activePage.child.configuration) {
val oldPage = activePage
activePage = state.activePage()
items = getAnimationItems(newPage = activePage, oldPage = oldPage)
}

Box(modifier = modifier) {
items.forEach { (configuration, item) ->
val (child, direction) = item

key(configuration) {
val animator = remember(direction) { selector(child, direction) }

animator(
direction = direction,
onFinished = {
when (direction) {
Direction.EXIT_FRONT,
Direction.EXIT_BACK -> items = items - configuration
Direction.ENTER_FRONT,
Direction.ENTER_BACK -> items = items + (configuration to item.copy(direction = Direction.IDLE))
Direction.IDLE -> Unit
}
}
) { modifier ->
Box(modifier = modifier) {
content(child)
}
}
}
}

// A workaround until https://issuetracker.google.com/issues/214231672.
// Normally only the exiting child be disabled.
if (items.size > 1) {
Overlay(modifier = Modifier.matchParentSize())
}
}
}

@Composable
private fun Overlay(modifier: Modifier) {
Box(
modifier = modifier.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
event.changes.forEach { it.consumeAllChanges() }
}
}
}
)
}

private fun RouterState<C, T>.activePage(): Page<C, T> =
Page(child = activeChild, index = backStack.size)

private fun getAnimationItems(newPage: Page<C, T>, oldPage: Page<C, T>?): Map<C, AnimationItem<C, T>> =
when {
oldPage == null ->
listOf(AnimationItem(newPage.child, Direction.IDLE))

newPage.index >= oldPage.index ->
listOf(
AnimationItem(oldPage.child, Direction.EXIT_BACK),
AnimationItem(newPage.child, Direction.ENTER_FRONT),
)

else ->
listOf(
AnimationItem(newPage.child, Direction.ENTER_BACK),
AnimationItem(oldPage.child, Direction.EXIT_FRONT),
)
}.associateBy { it.child.configuration }

private data class AnimationItem<out C : Any, out T : Any>(
val child: Child.Created<C, T>,
val direction: Direction,
)

private class Page<out C : Any, out T : Any>(
val child: Child.Created<C, T>,
val index: Int,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.animation.child

import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.isFinished
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.ExperimentalDecomposeApi

@ExperimentalDecomposeApi
internal class DefaultChildAnimator(
private val animationSpec: FiniteAnimationSpec<Float> = tween(),
private val frame: @Composable (factor: Float, direction: Direction, content: @Composable (Modifier) -> Unit) -> Unit
) : ChildAnimator {

@Composable
override operator fun invoke(direction: Direction, onFinished: () -> Unit, content: @Composable (Modifier) -> Unit) {
val animationState = remember(direction) { AnimationState(initialValue = if (direction == Direction.IDLE) 0F else 1F) }

LaunchedEffect(animationState) {
animationState.animateTo(
targetValue = 0F,
animationSpec = animationSpec,
sequentialAnimation = !animationState.isFinished,
)

onFinished()
}

val factor =
when (direction) {
Direction.IDLE,
Direction.ENTER_FRONT -> animationState.value
Direction.EXIT_FRONT -> 1F - animationState.value
Direction.ENTER_BACK -> -animationState.value
Direction.EXIT_BACK -> animationState.value - 1F
}

frame(factor, direction, content)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.animation.child

import com.arkivanov.decompose.ExperimentalDecomposeApi

/**
* Represents a direction in which child widgets are animated.
*/
@ExperimentalDecomposeApi
enum class Direction {

/**
* The child is not being animated.
*/
IDLE,

/**
* The child is entering from the front (push).
*/
ENTER_FRONT,

/**
* The child is exiting to the front (pop).
*/
EXIT_FRONT,

/**
* The child is entering from the back (move from the backstack).
*/
ENTER_BACK,

/**
* The child is exiting to the back (move to the backstack).
*/
EXIT_BACK,
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.animation.child

import androidx.compose.foundation.layout.Box
import com.arkivanov.decompose.ExperimentalDecomposeApi

@ExperimentalDecomposeApi
internal fun <C : Any, T : Any> emptyChildAnimation(): ChildAnimation<C, T> =
{ routerState, modifier, childContent ->
ChildAnimation { routerState, modifier, childContent ->
Box(modifier = modifier) {
childContent(routerState.activeChild)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.animation.child

import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import com.arkivanov.decompose.ExperimentalDecomposeApi
import kotlin.math.abs

/**
* A simple fading animation. Appearing children's `alpha` is animated from 0.0 to 1.0.
* Disappearing children's `alpha` is animated from 1.0 to 0.0.
*/
@ExperimentalDecomposeApi
fun fade(animationSpec: FiniteAnimationSpec<Float> = tween()): ChildAnimator =
childAnimator(animationSpec = animationSpec) { factor, _, content ->
content(Modifier.alpha(1F - abs(factor)))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.animation.child

import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import com.arkivanov.decompose.ExperimentalDecomposeApi

/**
* A simple scaling animation. Front (above) children are scaling from [frontFactor] to 1.0.
* Back (below) children are scaling from 1.0 to [backFactor].
*/
@ExperimentalDecomposeApi
fun scale(
animationSpec: FiniteAnimationSpec<Float> = tween(),
frontFactor: Float = 1.15F,
backFactor: Float = 0.95F,
): ChildAnimator =
childAnimator(animationSpec = animationSpec) { factor, _, content ->
content(
Modifier.scale(
if (factor >= 0F) {
factor * (frontFactor - 1F) + 1F
} else {
factor * (1F - backFactor) + 1F
}
)
)
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.animation.child

import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.foundation.layout.Box
import androidx.compose.animation.core.tween
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetbrains.animation.child.ChildAnimation

/**
* A simple sliding animation. Children enter from the right side and exit from the left.
* A simple sliding animation. Children enter from one side and exit to another side.
*/
@ExperimentalDecomposeApi
fun <C : Any, T : Any> slide(
animationSpec: FiniteAnimationSpec<Float> = defaultChildAnimationSpec,
): ChildAnimation<C, T> =
childAnimation(animationSpec = animationSpec) { _, factor, placement, _, content ->
Box(
modifier = Modifier.offsetXFactor(
factor = when (placement) {
ChildPlacement.BACK -> factor - 1F
ChildPlacement.FRONT -> 1F - factor
}
)
) {
content()
}
fun slide(animationSpec: FiniteAnimationSpec<Float> = tween()): ChildAnimator =
childAnimator(animationSpec = animationSpec) { factor, _, content ->
content(Modifier.offsetXFactor(factor = factor))
}

private fun Modifier.offsetXFactor(factor: Float): Modifier =

This file was deleted.

This file was deleted.

Loading