├── .codeclimate.yml ├── .gitignore ├── .jitpack.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── build.gradle ├── circle.yml ├── core ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── zhuinden │ │ └── simplestackcomposeintegration │ │ └── core │ │ ├── BackstackHolderViewModel.kt │ │ ├── ComposeBackstackStore.kt │ │ ├── ComposeIntegrationCore.kt │ │ ├── ComposeNavigator.kt │ │ ├── StoreHolderViewModel.kt │ │ └── util │ │ └── BackstackCompose.kt │ └── test │ └── java │ └── com │ └── zhuinden │ └── simplestackcomposeintegration │ └── TestSuite.java ├── example-dogs ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zhuinden │ │ └── simplestackcomposedogexample │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── zhuinden │ │ │ └── simplestackcomposedogexample │ │ │ ├── app │ │ │ ├── CustomApplication.kt │ │ │ └── MainActivity.kt │ │ │ ├── core │ │ │ ├── models │ │ │ │ └── ModelExtensions.kt │ │ │ ├── navigation │ │ │ │ └── ComposeKey.kt │ │ │ └── theme │ │ │ │ └── Theme.kt │ │ │ ├── data │ │ │ ├── datasource │ │ │ │ └── DogDataSource.kt │ │ │ └── models │ │ │ │ └── Dog.kt │ │ │ ├── features │ │ │ ├── dogdetail │ │ │ │ ├── DogDetailKey.kt │ │ │ │ └── DogDetailScreen.kt │ │ │ └── doglist │ │ │ │ ├── DogListKey.kt │ │ │ │ ├── DogListScreen.kt │ │ │ │ └── DogListViewModel.kt │ │ │ └── utils │ │ │ └── OptionalWrapper.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── zhuinden │ └── simplestackcomposedogexample │ └── ExampleUnitTest.kt ├── example-ftue ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zhuinden │ │ └── simplestackftuecomposesample │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── zhuinden │ │ │ └── simplestackftuecomposesample │ │ │ ├── app │ │ │ ├── AuthenticationManager.kt │ │ │ ├── ComposeKey.kt │ │ │ ├── CustomApplication.kt │ │ │ ├── MainActivity.kt │ │ │ └── ServiceProvider.kt │ │ │ ├── features │ │ │ ├── login │ │ │ │ ├── LoginKey.kt │ │ │ │ ├── LoginScreen.kt │ │ │ │ └── LoginViewModel.kt │ │ │ ├── profile │ │ │ │ ├── ProfileKey.kt │ │ │ │ ├── ProfileScreen.kt │ │ │ │ └── ProfileViewModel.kt │ │ │ └── registration │ │ │ │ ├── CreateLoginCredentialsKey.kt │ │ │ │ ├── CreateLoginCredentialsScreen.kt │ │ │ │ ├── EnterProfileDataKey.kt │ │ │ │ ├── EnterProfileDataScreen.kt │ │ │ │ └── RegistrationViewModel.kt │ │ │ └── utils │ │ │ ├── RxRelayUtils.kt │ │ │ └── Utils.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_arrow_back_black_24dp.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_tag_faces_black_24dp.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── zhuinden │ └── simplestackftuecomposesample │ └── ExampleUnitTest.kt ├── example-nested-navigation ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── zhuinden │ │ └── simplestackcomposenestedexample │ │ ├── ComposeKey.kt │ │ ├── FirstNestedScreen.kt │ │ ├── FirstScreen.kt │ │ ├── MainActivity.kt │ │ ├── SecondNestedScreen.kt │ │ ├── SecondScreen.kt │ │ ├── ThirdScreen.kt │ │ └── Utils.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── example-simple ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zhuinden │ │ └── simplestackcomposedogexample │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── zhuinden │ │ │ └── simplestackcomposedogexample │ │ │ ├── ComposeKey.kt │ │ │ ├── FirstScreen.kt │ │ │ ├── MainActivity.kt │ │ │ ├── SecondScreen.kt │ │ │ └── Utils.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── zhuinden │ └── simplestackcomposedogexample │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── services ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── zhuinden │ │ └── simplestackcomposeintegration │ │ └── services │ │ └── ComposeIntegrationServices.kt │ └── test │ └── java │ └── com │ └── zhuinden │ └── simplestackcomposeintegration │ └── TestSuite.java └── settings.gradle /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | ## other configuration excluded from example... 2 | exclude_patterns: 3 | - "simple-stack-example-**" 4 | - "simple-stack/src/test/" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .idea 6 | /.idea/workspace.xml 7 | /.idea/libraries 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | projectFilesBackup 13 | 14 | # Project exclude paths 15 | /fragments-ktx/build/ 16 | /fragments-ktx/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/ 17 | /navigator-ktx/build/ 18 | /navigator-ktx/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/ 19 | /services/build/ 20 | /services/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/ 21 | /services-ktx/build/ 22 | /services-ktx/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/ 23 | /core/build 24 | /services/build 25 | /example-simple/build 26 | /example-dogs/build -------------------------------------------------------------------------------- /.jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: openjdk11 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | -Simple Stack Compose Integration 0.13.1 (2025-01-06) 4 | -------------------------------- 5 | 6 | - Fix that `rememberBackstack()` keys did not fully consider `id` as a change. (fixes #33) 7 | - 8 | - Fix that `SaveBackstackState` function was not prepared for multiple `id` parameters, as despite there being a ViewModelStore for each Backstack, there's no separate SaveableStateRegistry (possibly related to #23, which was never implemented). 9 | 10 | -Simple Stack Compose Integration 0.13.0 (2024-12-16) 11 | -------------------------------- 12 | 13 | - Add `parentServices: Backstack?` and `parentScopeTag: String?` parameters to `ComposeNavigatorInitializer`. This should allow nested hierarchical service lookups. 14 | 15 | This is available through both `rememberBackstack` and `ComposeNavigator`. 16 | 17 | -Simple Stack Compose Integration 0.12.3 (2024-05-06) 18 | -------------------------------- 19 | 20 | - Use `simple-stack 2.9.0` and `simple-stack-extensions 2.3.4`. 21 | 22 | -Simple Stack Compose Integration 0.12.2 (2023-07-03) 23 | -------------------------------- 24 | 25 | - Use `simple-stack 2.8.0` and `simple-stack-extensions 2.3.3`. 26 | 27 | - Update Compose to 1.4.3 and Kotlin to 1.8.22. 28 | 29 | - Update underlying AndroidX dependencies. 30 | 31 | -Simple Stack Compose Integration 0.12.1 (2023-04-15) 32 | -------------------------------- 33 | 34 | - Use `simple-stack-extensions 2.3.2`. 35 | 36 | - ADD: `rememberServiceFrom(scopeTag, ...)` and `rememberServiceFrom(scopeKey, ...)`. 37 | 38 | -Simple Stack Compose Integration 0.12.0 (2023-04-08) 39 | -------------------------------- 40 | 41 | - ADDED NEW MAJOR FEATURE (added by @matejdro): Support for `Backstack` managed by Compose. 42 | 43 | This allows `Backstack` to be created at any arbitrary nesting level within composables, including nested stacks. 44 | 45 | ```kotlin 46 | class MainActivity : AppCompatActivity() { 47 | 48 | override fun onCreate(savedInstanceState: Bundle?) { 49 | super.onCreate(savedInstanceState) 50 | 51 | setContent { 52 | ComposeNavigator { 53 | createBackstack( 54 | History.of(InitialKey()), 55 | scopedServices = DefaultServiceProvider() 56 | ) 57 | } 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | Nesting it like so 64 | 65 | ```kotlin 66 | Box( 67 | Modifier 68 | .weight(1f) 69 | .fillMaxWidth() 70 | .padding(bottom = 8.dp), 71 | propagateMinConstraints = true 72 | ) { 73 | ComposeNavigator(id = "TOP", interceptBackButton = false) { 74 | createBackstack( 75 | History.of(FirstNestedKey()), 76 | scopedServices = DefaultServiceProvider() 77 | ) 78 | } 79 | } 80 | ``` 81 | 82 | and 83 | 84 | ```kotlin 85 | Box( 86 | Modifier 87 | .weight(1f) 88 | .fillMaxWidth() 89 | .padding(bottom = 8.dp), 90 | propagateMinConstraints = true 91 | ) { 92 | ComposeNavigator(id = "BOTTOM", interceptBackButton = false) { 93 | createBackstack( 94 | History.of(FirstNestedKey()), 95 | scopedServices = DefaultServiceProvider() 96 | ) 97 | } 98 | } 99 | ``` 100 | 101 | -Simple Stack Compose Integration 0.11.0 (2023-03-31) 102 | -------------------------------- 103 | 104 | - UPDATE: simple-stack to 2.7.0, simple-stack-extensions 2.3.0. 105 | 106 | -Simple Stack Compose Integration 0.10.0 (2023-02-23) 107 | -------------------------------- 108 | 109 | - BREAKING CHANGE (recommended by @matejdro): `DefaultComposeKey.RenderComposable()` no longer receives a `Modifier`. This parameter was completely pointless, and a possible source of bugs. 110 | 111 | - BREAKING CHANGE (recommended by @matejdro): The signature of `AnimationConfiguration` and specifically `ComposableTransition` has changed, and no longer receives `fullWidth` and `fullHeight`. This info can be accessed using `Modifier.drawWithContent {}` and is readily available. Also, `ComposableTransition` now receives `animationProgress` as a `State`, and not a `Float`. Using `Float` directly results in excessive recompositions during animation, and is bad for performance, therefore this value must be passed lazily. This is effectively a fix for a long-lasting design issue in how animation progress had been handled, created before recomposition-related best practices on deferred read were documented. 112 | 113 | - CHANGE (required for new features): `core` now has an `api` dependency on `androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1` to support `LocalLifecycleOwner`/`LocalViewModelStoreOwner` per screen. 114 | 115 | - NEW FEATURE (added by @matejdro): `core` now supports creating a `LocalLifecycleOwner`/`LocalViewModelStoreOwner` per screen, this change is automatically applied by updating. Please note that this means from now on, AndroidX components scope themselves to the screen, and not the Activity. If you relied on the nearest `LocalViewModelStoreOwner` to be the `Activity`, then this is no longer the case (although in most cases, using Activity-scoped ViewModels for parameter passing typically results in stale state not being reset, and is a common source of bugs.) 116 | 117 | - NEW FEATURE: `DefaultComposeKey` now has a `open val modifier: Modifier = Modifier` which is passed to the `ScreenComposable`. 118 | 119 | - UPDATE: compileSdk 33. 120 | 121 | - UPDATE: simple-stack to 2.6.5, simple-stack-extensions 2.2.5. 122 | 123 | - UPDATE: Kotlin to 1.8.10. 124 | 125 | - UPDATE: Compose library versions to BOM 2023.01.00. 126 | - 127 | - UPDATE: Compose Compiler to 1.4.3. 128 | 129 | 130 | -Simple Stack Compose Integration 0.9.5 (2022-04-21) 131 | -------------------------------- 132 | 133 | - UPDATE: simple-stack to 2.6.4. 134 | 135 | - UPDATE: Compose to 1.1.1. 136 | 137 | - UPDATE: Kotlin to 1.6.10. 138 | 139 | - Moved to `maven-publish`, ensure that sources-jar gets added. 140 | 141 | 142 | -Simple Stack Compose Integration 0.9.4 (2021-10-21) 143 | -------------------------------- 144 | 145 | - UPDATE: Compose to 1.0.4. 146 | 147 | - UPDATE: Kotlin to 1.5.31. 148 | 149 | -Simple Stack Compose Integration 0.9.3 (2021-08-30) 150 | -------------------------------- 151 | 152 | - UPDATE: Compose to 1.0.3. 153 | 154 | - UPDATE: Kotlin to 1.5.30. 155 | 156 | -Simple Stack Compose Integration 0.9.1 (2021-08-10) 157 | -------------------------------- 158 | 159 | - UPDATE: Compose to 1.0.1. 160 | 161 | - UPDATE: Kotlin to 1.5.21. 162 | 163 | (Kotlin version is still 1.5.10 as expected by 1.0.0) 164 | 165 | -Simple Stack Compose Integration 0.9.0 (2021-07-29) 166 | -------------------------------- 167 | 168 | - UPDATE: Compose to 1.0.0. 169 | 170 | (Kotlin version is still 1.5.10 as expected by 1.0.0) 171 | 172 | -Simple Stack Compose Integration 0.5.1 (2021-07-28) 173 | -------------------------------- 174 | 175 | - UPDATE: Compose to 1.0.0-rc02. 176 | 177 | (Kotlin version is still 1.5.10 as expected by 1.0.0-rc02) 178 | 179 | -Simple Stack Compose Integration 0.5.0 (2021-07-06) 180 | -------------------------------- 181 | 182 | - BREAKING CHANGE: Separate `AnimationSpec` from the global transition definition. This allows for different animation specs for different screen transitions. 183 | 184 | - BREAKING CHANGE: Kill `AnimationConfiguration.CustomComposableTransitions`. The two `ComposableTransition`s are now top-level property of `AnimationConfiguration` along with `ComposableAnimationSpec`. 185 | 186 | - ADD: `AnimationConfiguration.ComposableContentWrapper`, which is a block around the animated content that can be customized. 187 | 188 | - UPDATE: Compose to 1.0.0-rc01. 189 | 190 | -Simple Stack Compose Integration 0.4.3 (2021-06-23) 191 | -------------------------------- 192 | 193 | - INTERNAL/FIX: Change usage of `LaunchedEffect` to `DisposableEffect` in ComposeStateChanger. This should potentially fix the elusive issue #7. 194 | 195 | -Simple Stack Compose Integration 0.4.2 (2021-06-23) 196 | -------------------------------- 197 | 198 | - No significant changes. 199 | 200 | - Update Compose to 1.0.0-beta09. 201 | 202 | -Simple Stack Compose Integration 0.4.1 (2021-06-07) 203 | -------------------------------- 204 | 205 | - No significant changes. 206 | 207 | - Update simple-stack to 2.6.2. 208 | 209 | -Simple Stack Compose Integration 0.4.0 (2021-06-04) 210 | -------------------------------- 211 | 212 | - Update Compose to 1.0.0-beta08. 213 | 214 | - Update Kotlin to 1.5.10. 215 | 216 | -Simple Stack Compose Integration 0.3.1 (2021-05-19) 217 | -------------------------------- 218 | 219 | - Update Compose to 1.0.0-beta07. 220 | 221 | -Simple Stack Compose Integration 0.3.0 (2021-05-06) 222 | -------------------------------- 223 | - Remove `SimpleComposeStateChanger` because new Compose version killed it for some reason. 224 | 225 | - Renamed `AnimatingComposeStateChanger` to `ComposeStateChanger`. It is used with `AsyncStateChanger`. 226 | 227 | - Update Jetpack Compose to beta06. 228 | 229 | - Update Simple-Stack to 2.6.1. 230 | 231 | - Update Simple-Stack Extensions to 2.2.1. 232 | 233 | -Simple Stack Compose Integration 0.2.0 (2021-03-08) 234 | -------------------------------- 235 | - Actually fix Saver :) 236 | 237 | -Simple Stack Compose Integration 0.1.3 (2021-03-08) 238 | -------------------------------- 239 | 240 | - API CHANGE: `DefaultComposeKey` now requires a `saveableStateProviderKey` that is `Any`. The examples return `this` because keys are already immutable and Parcelable (and data class). 241 | 242 | Note: Saver should work, but it does not seem to work yet. 243 | 244 | -Simple Stack Compose Integration 0.1.2 (2021-03-04) 245 | -------------------------------- 246 | 247 | - API CHANGE: simplified transition configuration. Now it's a single interface (`ComposableTransition`). 248 | 249 | - FIX: Flickering on navigation. (However, CoilImage still flickers.) 250 | 251 | Note: Saver still does not work yet. 252 | 253 | -Simple Stack Compose Integration 0.1.1 (2021-03-03) 254 | -------------------------------- 255 | - API CHANGE: `ComposeStateChanger` -> `AnimatingComposeStateChanger`, must be wrapped as `AsyncStateChanger`. 256 | 257 | Please note that it is still flickering and you probably don't want to use it yet. 258 | 259 | - ADD: `SimpleComposeStateChanger`, must be wrapped as `SimpleStateChanger`, without animations. 260 | 261 | - API CHANGE: Add `fullHeight` to `AnimatingComposeStateChanger`'s transition configuration, because it was missing. 262 | 263 | - ADD: `simple-stack-compose-dog-example`. 264 | 265 | -Simple Stack Compose Integration 0.1.0 (2021-03-02) 266 | -------------------------------- 267 | - Initial release (built against simple-stack 2.5.0, simple-stack-extensions 2.1.0, Compose 1.0.0-beta01). 268 | 269 | - Known issue: on forward -> back -> forward animation, the new key's composable seems to flicker in at the start of animation for some reason. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Stack Compose Integration 2 | 3 | Default behavior for Jetpack Compose using Simple-Stack. 4 | 5 | ## Using Simple Stack Compose Integration 6 | 7 | In order to use Simple Stack Compose Integration, you need to add `jitpack` to your project root `build.gradle.kts` 8 | (or `build.gradle`): 9 | 10 | ``` kotlin 11 | // build.gradle.kts 12 | allprojects { 13 | repositories { 14 | // ... 15 | maven { setUrl("https://jitpack.io") } 16 | } 17 | // ... 18 | } 19 | ``` 20 | 21 | or 22 | 23 | ``` groovy 24 | // build.gradle 25 | allprojects { 26 | repositories { 27 | // ... 28 | maven { url "https://jitpack.io" } 29 | } 30 | // ... 31 | } 32 | ``` 33 | 34 | In newer projects, you need to also update the `settings.gradle` file's `dependencyResolutionManagement` block: 35 | 36 | ``` 37 | dependencyResolutionManagement { 38 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 39 | repositories { 40 | google() 41 | mavenCentral() 42 | maven { url 'https://jitpack.io' } // <-- 43 | jcenter() // Warning: this repository is going to shut down soon 44 | } 45 | } 46 | ``` 47 | 48 | 49 | and then, add the dependency to your module's `build.gradle.kts` (or `build.gradle`): 50 | 51 | ``` kotlin 52 | // build.gradle.kts 53 | implementation("com.github.Zhuinden:simple-stack-compose-integration:0.13.1") 54 | ``` 55 | 56 | or 57 | 58 | ``` groovy 59 | // build.gradle 60 | implementation 'com.github.Zhuinden:simple-stack-compose-integration:0.13.1' 61 | ``` 62 | 63 | As Compose requires Java-8 bytecode, you need to also add this: 64 | 65 | ``` groovy 66 | android { 67 | compileOptions { 68 | sourceCompatibility JavaVersion.VERSION_1_8 69 | targetCompatibility JavaVersion.VERSION_1_8 70 | } 71 | kotlinOptions { 72 | jvmTarget = '1.8' 73 | languageVersion = '1.9' 74 | } 75 | buildFeatures { 76 | compose true 77 | } 78 | composeOptions { 79 | kotlinCompilerExtensionVersion '1.4.3' 80 | } 81 | } 82 | 83 | kotlin.sourceSets.all { 84 | languageSettings.enableLanguageFeature("DataObjects") 85 | } 86 | ``` 87 | 88 | ## What does it do? 89 | 90 | Provides defaults for Composable-driven navigation and animation support. 91 | 92 | ``` kotlin 93 | class MainActivity : AppCompatActivity() { 94 | override fun onCreate(savedInstanceState: Bundle?) { 95 | super.onCreate(savedInstanceState) 96 | 97 | setContent { 98 | ComposeNavigator { 99 | createBackstack( 100 | History.of(InitialKey()), 101 | scopedServices = DefaultServiceProvider() 102 | ) 103 | } 104 | } 105 | } 106 | } 107 | ``` 108 | 109 | and 110 | 111 | ``` kotlin 112 | abstract class ComposeKey: DefaultComposeKey(), Parcelable { 113 | override val saveableStateProviderKey: Any = this // data class + parcelable! 114 | } 115 | ``` 116 | 117 | and 118 | 119 | ``` kotlin 120 | @Immutable 121 | @Parcelize 122 | data object SecondKey: ComposeKey() { 123 | operator fun invoke() = this 124 | 125 | @Composable 126 | override fun ScreenComposable(modifier: Modifier) { 127 | SecondScreen(modifier) 128 | } 129 | } 130 | ``` 131 | 132 | ## What about ViewModels? 133 | 134 | While Jetpack ViewModels are also supported, but it is recommended to use `ScopedServices` as provided by **Simple-Stack**, because `ScopedServices` have more powerful feature set than `ViewModel`. 135 | 136 | ``` kotlin 137 | abstract class ComposeKey : DefaultComposeKey(), Parcelable, DefaultServiceProvider.HasServices { 138 | override val saveableStateProviderKey: Any = this 139 | 140 | override fun getScopeTag(): String = toString() 141 | 142 | override fun bindServices(serviceBinder: ServiceBinder) { 143 | } 144 | } 145 | ``` 146 | 147 | and 148 | 149 | ``` kotlin 150 | val backstack = Navigator.configure() 151 | .setScopedServices(DefaultServiceProvider()) 152 | // ... 153 | ``` 154 | 155 | and 156 | 157 | ``` kotlin 158 | @Immutable 159 | @Parcelize 160 | data object DogListKey: ComposeKey() { 161 | operator fun invoke() = this 162 | 163 | override fun bindServices(serviceBinder: ServiceBinder) { 164 | with(serviceBinder) { 165 | add(DogListViewModel(lookup(), backstack)) // <-- 166 | } 167 | } 168 | 169 | @Composable 170 | override fun ScreenComposable(modifier: Modifier) { 171 | val viewModel = remember { backstack.lookup() } // <-- 172 | 173 | val dogs by viewModel.dogList.observeAsState() 174 | 175 | DogListScreen(dogs) 176 | } 177 | } 178 | ``` 179 | 180 | ## Note about using Enum parameters in keys 181 | 182 | Unfortunately, `enum.hashCode()` is not stable across process death. so Enum classes shouldn't be passed directly to keys as arguments. 183 | 184 | It is preferable to preserve them as a private String, and expose the value as an enum vie a custom getter. 185 | 186 | ```kotlin 187 | // THIS BREAKS! 188 | // data class DemoKey(val enum: DemoEnum): DefaultComposeKey // <-- breaks! 189 | 190 | // DO THIS INSTEAD 191 | data class DemoKey(private val enumName: String): DefaultComposeKey { 192 | constructor(enum: DemoEnum): this(enum.name) 193 | 194 | val enum: DemoEnum get() = DemoEnum.valueOf(enumName) 195 | } 196 | ``` 197 | 198 | Unfortunately, this is a limitation of the JVM, and not of Simple-Stack, meaning it's something we need to remember to do. 199 | 200 | ## License 201 | 202 | Copyright 2021-2025 Gabor Varadi 203 | 204 | Licensed under the Apache License, Version 2.0 (the "License"); 205 | you may not use this file except in compliance with the License. 206 | You may obtain a copy of the License at 207 | 208 | http://www.apache.org/licenses/LICENSE-2.0 209 | 210 | Unless required by applicable law or agreed to in writing, software 211 | distributed under the License is distributed on an "AS IS" BASIS, 212 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 213 | See the License for the specific language governing permissions and 214 | limitations under the License. 215 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | maven { url "https://clojars.org/repo/" } 8 | maven { url "https://jitpack.io" } 9 | maven { url 'https://maven.google.com' } 10 | jcenter() 11 | } 12 | dependencies { 13 | classpath 'com.android.tools.build:gradle:7.4.1' 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22" 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | mavenCentral() 24 | maven { url "https://clojars.org/repo/" } 25 | maven { url "https://jitpack.io" } 26 | maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } 27 | maven { url 'https://maven.google.com' } 28 | jcenter() 29 | } 30 | } 31 | 32 | task clean(type: Delete) { 33 | delete rootProject.buildDir 34 | } 35 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | ANDROID_HOME: /usr/local/android-sdk-linux 4 | 5 | dependencies: 6 | pre: 7 | - echo y | android update sdk --no-ui --all --filter "tools,platform-tools,android-25" 8 | - echo y | android update sdk --no-ui --all --filter "build-tools-25.0.2" 9 | - echo y | android update sdk --no-ui --all --filter "extra-android-support"; 10 | - echo y | android update sdk --no-ui --all --filter "extra-android-m2repository"; 11 | - echo y | android update sdk --no-ui --all --filter "extra-google-google_play_services"; 12 | 13 | test: 14 | override: 15 | - ./gradlew build check 16 | 17 | # copy lint report 18 | - mkdir $CIRCLE_TEST_REPORTS/Lint 19 | - mv simple-stack/build/outputs/lint-results-debug.xml $CIRCLE_TEST_REPORTS/lint 20 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'maven-publish' 4 | 5 | group = "com.github.Zhuinden.simple-stack-compose-integration" 6 | version = "0.13.1" 7 | 8 | android { 9 | compileSdkVersion 33 10 | defaultConfig { 11 | minSdkVersion 16 12 | targetSdkVersion 31 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | lintOptions { 22 | abortOnError false 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | kotlinOptions { 29 | jvmTarget = '1.8' 30 | } 31 | buildFeatures { 32 | compose true 33 | } 34 | composeOptions { 35 | kotlinCompilerExtensionVersion '1.4.8' 36 | } 37 | } 38 | 39 | dependencies { 40 | implementation fileTree(dir: 'libs', include: ['*.jar']) 41 | 42 | api "com.google.code.findbugs:jsr305:3.0.2" 43 | api("com.github.Zhuinden:simple-stack:2.9.0") { 44 | transitive = true 45 | } 46 | api "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1" 47 | 48 | implementation "androidx.compose.runtime:runtime:1.4.3" 49 | implementation "androidx.compose.ui:ui:1.4.3" 50 | implementation "androidx.compose.ui:ui-util:1.4.3" 51 | implementation "androidx.compose.ui:ui-text:1.4.3" 52 | implementation "androidx.compose.ui:ui-unit:1.4.3" 53 | implementation "androidx.compose.ui:ui-geometry:1.4.3" 54 | implementation "androidx.compose.foundation:foundation:1.4.3" 55 | implementation "androidx.compose.foundation:foundation-layout:1.4.3" 56 | implementation "androidx.compose.material:material:1.4.3" 57 | implementation "androidx.compose.material:material-icons-extended:1.4.3" 58 | implementation "androidx.compose.animation:animation:1.4.3" 59 | implementation "androidx.compose.ui:ui-tooling:1.4.3" 60 | implementation "androidx.activity:activity-compose:1.7.2" 61 | 62 | testImplementation 'junit:junit:4.13.2' 63 | testImplementation 'org.assertj:assertj-core:3.16.1' 64 | testImplementation 'org.mockito:mockito-core:3.8.0' 65 | testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3' 66 | androidTestImplementation 'junit:junit:4.13.2' 67 | androidTestImplementation 'com.github.Zhuinden:espresso-helper:1.0.0' 68 | } 69 | 70 | task javadoc(type: Javadoc) { 71 | configurations.implementation.canBeResolved(true) 72 | configurations.api.canBeResolved(true) 73 | 74 | failOnError false 75 | 76 | source = android.sourceSets.main.java.srcDirs 77 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 78 | //destinationDir = file("../javadoc/") 79 | classpath += configurations.api 80 | } 81 | 82 | tasks.withType(Javadoc).all { 83 | enabled = false 84 | } 85 | 86 | task sourcesJar(type: Jar) { 87 | from android.sourceSets.main.java.srcDirs 88 | archiveClassifier = "sources" 89 | } 90 | 91 | task javadocJar(type: Jar, dependsOn: javadoc) { 92 | classifier = 'javadoc' 93 | from javadoc.destinationDir 94 | } 95 | 96 | artifacts { 97 | archives sourcesJar 98 | archives javadocJar 99 | } 100 | 101 | // Because the components are created only during the afterEvaluate phase, you must 102 | // configure your publications using the afterEvaluate() lifecycle method. 103 | afterEvaluate { 104 | publishing { 105 | publications { 106 | // Creates a Maven publication called "release". 107 | release(MavenPublication) { 108 | // Applies the component for the release build variant. 109 | from components.release 110 | artifact(sourcesJar) 111 | 112 | // You can then customize attributes of the publication as shown below. 113 | groupId = 'com.github.Zhuinden.simple-stack-compose-integration' 114 | artifactId = 'core' 115 | version = '0.13.1' 116 | 117 | pom.withXml { 118 | def dependenciesNode = (asNode().get("dependencies") as groovy.util.NodeList).get(0) as groovy.util.Node 119 | def configurationNames = ["implementation", "api"] 120 | 121 | configurationNames.forEach { configurationName -> 122 | configurations[configurationName].allDependencies.forEach { 123 | if (it.group != null && it.version != "unspecified") { 124 | def dependencyNode = dependenciesNode.appendNode("dependency") 125 | dependencyNode.appendNode("groupId", it.group) 126 | dependencyNode.appendNode("artifactId", it.name) 127 | dependencyNode.appendNode("version", it.version) 128 | // dependencyNode.appendNode("scope", configurationName) 129 | } 130 | } 131 | } 132 | 133 | def dependencyNode = dependenciesNode.appendNode("dependency") 134 | dependencyNode.appendNode("groupId", "com.github.Zhuinden") 135 | dependencyNode.appendNode("artifactId", "state-bundle") 136 | dependencyNode.appendNode("version", "1.4.0") 137 | } 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /core/src/main/java/com/zhuinden/simplestackcomposeintegration/core/BackstackHolderViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposeintegration.core 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.zhuinden.simplestack.BackHandlingModel 5 | import com.zhuinden.simplestack.Backstack 6 | import com.zhuinden.simplestack.GlobalServices 7 | import com.zhuinden.simplestack.KeyFilter 8 | import com.zhuinden.simplestack.KeyParceler 9 | import com.zhuinden.simplestack.ScopedServices 10 | 11 | internal class BackstackHolderViewModel : ViewModel() { 12 | private val backstacks = HashMap() 13 | 14 | fun getBackstack(id: String): Backstack? { 15 | return backstacks[id] 16 | } 17 | 18 | fun createInitializer(id: String) = object : ComposeNavigatorInitializer { 19 | override fun createBackstack( 20 | initialKeys: List<*>, 21 | keyFilter: KeyFilter, 22 | keyParceler: KeyParceler, 23 | stateClearStrategy: Backstack.StateClearStrategy, 24 | scopedServices: ScopedServices?, 25 | globalServices: GlobalServices?, 26 | globalServicesFactory: GlobalServices.Factory?, 27 | parentServices: Backstack?, 28 | parentScopeTag: String?, 29 | ): Backstack { 30 | val backstack = Backstack() 31 | 32 | backstack.setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME) 33 | backstack.setKeyFilter(keyFilter) 34 | backstack.setKeyParceler(keyParceler) 35 | backstack.setStateClearStrategy(stateClearStrategy) 36 | scopedServices?.let { backstack.setScopedServices(it) } 37 | globalServices?.let { backstack.setGlobalServices(it) } 38 | globalServicesFactory?.let { backstack.setGlobalServices(it) } 39 | parentServices?.let { 40 | if (parentScopeTag != null) { 41 | backstack.setParentServices(parentServices, parentScopeTag) 42 | } else { 43 | backstack.parentServices = parentServices 44 | } 45 | } 46 | 47 | backstack.setup(initialKeys) 48 | backstacks[id] = backstack 49 | 50 | return backstack 51 | } 52 | } 53 | 54 | override fun onCleared() { 55 | for (backstack in backstacks.values) { 56 | backstack.finalizeScopes() 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/java/com/zhuinden/simplestackcomposeintegration/core/ComposeBackstackStore.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposeintegration.core 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.DisposableEffect 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.saveable.LocalSaveableStateRegistry 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.platform.LocalLifecycleOwner 12 | import androidx.lifecycle.Lifecycle 13 | import androidx.lifecycle.LifecycleEventObserver 14 | import androidx.lifecycle.viewmodel.compose.viewModel 15 | import com.zhuinden.simplestack.AheadOfTimeWillHandleBackChangedListener 16 | import com.zhuinden.simplestack.BackHandlingModel 17 | import com.zhuinden.simplestack.Backstack 18 | import com.zhuinden.simplestack.Backstack.StateClearStrategy 19 | import com.zhuinden.simplestack.DefaultKeyFilter 20 | import com.zhuinden.simplestack.DefaultKeyParceler 21 | import com.zhuinden.simplestack.DefaultStateClearStrategy 22 | import com.zhuinden.simplestack.GlobalServices 23 | import com.zhuinden.simplestack.KeyFilter 24 | import com.zhuinden.simplestack.KeyParceler 25 | import com.zhuinden.simplestack.ScopedServices 26 | import com.zhuinden.simplestack.StateChanger 27 | import com.zhuinden.statebundle.StateBundle 28 | 29 | /** 30 | * Create a [Backstack] for navigation and remember it across state changes and process kills. 31 | * 32 | * [stateChanger] wil not be remembered and will be re-initialized every time this composable 33 | * exits scope. It MUST be remembered by the caller. 34 | * 35 | * [init] argument will only be called once (or after process kill). In that lambda, you have to 36 | * call [ComposeNavigatorInitializer.createBackstack] and return provided value. 37 | * Backstack will not perform any navigation until you return from that lambda, 38 | * so you can initialize your own services that require a [Backstack] instance, before you return. 39 | * 40 | * optional [id] argument allows you to have multiple backstacks inside single screen. To do that, 41 | * you have to provide unique ID to every distinct [rememberBackstack] call. 42 | * 43 | * Created backstack will automatically intercept all back button presses when necessary, if 44 | * [interceptBackButton] flag is enabled. Otherwise it is up to the caller to manually call 45 | * [Backstack.goBack]. 46 | * 47 | * Note that backstack created with this method 48 | * uses [BackHandlingModel.AHEAD_OF_TIME] back handling model. 49 | */ 50 | @Composable 51 | fun rememberBackstack( 52 | stateChanger: StateChanger, 53 | id: String = "DEFAULT_SINGLE_COMPOSE_STACK_IDENTIFIER", 54 | interceptBackButton: Boolean = true, 55 | init: ComposeNavigatorInitializer.() -> Backstack, 56 | ): Backstack { 57 | val viewModel = viewModel() 58 | val backstack = viewModel.getBackstack(id) ?: init(viewModel.createInitializer(id)) 59 | 60 | SaveBackstackState(backstack, id) 61 | ListenToLifecycleEvents(backstack) 62 | 63 | if (interceptBackButton) { 64 | BackHandler(backstack) 65 | } 66 | 67 | remember(backstack, stateChanger, id) { 68 | // Attach state changer after init call to defer first navigation. That way, 69 | // caller can use backstack to init their own things with Backstack instance 70 | // before navigation is performed. 71 | backstack.setStateChanger(stateChanger) 72 | true 73 | } 74 | 75 | return backstack 76 | } 77 | 78 | @Composable 79 | private fun BackHandler(backstack: Backstack) { 80 | var backButtonEnabled by remember { mutableStateOf(backstack.willHandleAheadOfTimeBack()) } 81 | 82 | DisposableEffect(backstack) { 83 | val listener = AheadOfTimeWillHandleBackChangedListener { 84 | backButtonEnabled = it 85 | } 86 | 87 | 88 | backButtonEnabled = backstack.willHandleAheadOfTimeBack() 89 | backstack.addAheadOfTimeWillHandleBackChangedListener(listener) 90 | 91 | onDispose { 92 | backstack.removeAheadOfTimeWillHandleBackChangedListener(listener) 93 | } 94 | } 95 | 96 | BackHandler(enabled = backButtonEnabled) { 97 | backstack.goBack() 98 | } 99 | } 100 | 101 | @Composable 102 | private fun SaveBackstackState(backstack: Backstack, id: String) { 103 | val stateSavingRegistry = LocalSaveableStateRegistry.current 104 | 105 | remember(backstack, id, stateSavingRegistry) { 106 | if (stateSavingRegistry == null) { 107 | return@remember true 108 | } 109 | 110 | val stateId = "$STATE_SAVING_KEY[$id]" 111 | val oldState = stateSavingRegistry.consumeRestored(stateId) as StateBundle? 112 | oldState?.let { 113 | backstack.fromBundle(it) 114 | } 115 | 116 | stateSavingRegistry.registerProvider(stateId) { 117 | backstack.toBundle() 118 | } 119 | } 120 | } 121 | 122 | @Composable 123 | private fun ListenToLifecycleEvents(backstack: Backstack) { 124 | val lifecycle = LocalLifecycleOwner.current.lifecycle 125 | 126 | DisposableEffect(lifecycle) { 127 | val lifecycleListener = LifecycleEventObserver { _, event -> 128 | val isResumed = event.targetState.isAtLeast(Lifecycle.State.RESUMED) 129 | val isStateChangerAlreadyAttached = backstack.hasStateChanger() 130 | if (isResumed != isStateChangerAlreadyAttached) { 131 | if (isResumed) { 132 | backstack.reattachStateChanger() 133 | } else { 134 | backstack.detachStateChanger() 135 | } 136 | } 137 | } 138 | 139 | lifecycle.addObserver(lifecycleListener) 140 | 141 | onDispose { 142 | lifecycle.removeObserver(lifecycleListener) 143 | backstack.executePendingStateChange() 144 | backstack.setStateChanger(null) 145 | } 146 | } 147 | } 148 | 149 | interface ComposeNavigatorInitializer { 150 | fun createBackstack( 151 | initialKeys: List<*>, 152 | keyFilter: KeyFilter = DefaultKeyFilter(), 153 | keyParceler: KeyParceler = DefaultKeyParceler(), 154 | stateClearStrategy: StateClearStrategy = DefaultStateClearStrategy(), 155 | scopedServices: ScopedServices? = null, 156 | globalServices: GlobalServices? = null, 157 | globalServicesFactory: GlobalServices.Factory? = null, 158 | parentServices: Backstack? = null, 159 | parentScopeTag: String? = null, 160 | ): Backstack 161 | } 162 | 163 | private const val STATE_SAVING_KEY = "BackstackState" 164 | -------------------------------------------------------------------------------- /core/src/main/java/com/zhuinden/simplestackcomposeintegration/core/ComposeNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposeintegration.core 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.Modifier 6 | import com.zhuinden.simplestack.AsyncStateChanger 7 | import com.zhuinden.simplestack.Backstack 8 | 9 | /** 10 | * Create a Simple Stack navigator that will handle backstack and display screens. 11 | * 12 | * @see ComposeStateChanger for documentation on [animationConfiguration] argument. 13 | * @see rememberBackstack for documentation on the rest of the arguments 14 | */ 15 | @Composable 16 | fun ComposeNavigator( 17 | modifier: Modifier = Modifier, 18 | animationConfiguration: ComposeStateChanger.AnimationConfiguration = 19 | ComposeStateChanger.AnimationConfiguration(), 20 | id: String = "DEFAULT_SINGLE_COMPOSE_STACK_IDENTIFIER", 21 | interceptBackButton: Boolean = true, 22 | init: ComposeNavigatorInitializer.() -> Backstack, 23 | ) { 24 | val composeStateChanger = remember(id, animationConfiguration) { ComposeStateChanger(animationConfiguration) } 25 | val asyncStateChanger = remember(id, composeStateChanger) { AsyncStateChanger(composeStateChanger) } 26 | 27 | val backstack = rememberBackstack(asyncStateChanger, id, interceptBackButton, init) 28 | 29 | BackstackProvider(backstack) { 30 | composeStateChanger.RenderScreen(modifier) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/java/com/zhuinden/simplestackcomposeintegration/core/StoreHolderViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposeintegration.core 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.ViewModelStore 7 | import androidx.lifecycle.ViewModelStoreOwner 8 | import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner 9 | 10 | internal class StoreHolderViewModel : ViewModel() { 11 | private val viewModelStores = HashMap() 12 | 13 | fun removeKey(key: Any) { 14 | viewModelStores.remove(key)?.viewModelStore?.clear() 15 | } 16 | 17 | @Composable 18 | fun WithLocalViewModelStore(key: Any, block: @Composable () -> Unit) { 19 | val storeOwner = viewModelStores.getOrPut(key) { 20 | val store = ViewModelStore() 21 | 22 | object: ViewModelStoreOwner { 23 | override val viewModelStore: ViewModelStore 24 | get() = store 25 | 26 | } 27 | } 28 | 29 | CompositionLocalProvider(LocalViewModelStoreOwner provides storeOwner) { 30 | block() 31 | } 32 | } 33 | 34 | override fun onCleared() { 35 | for (store in viewModelStores.values) { 36 | store.viewModelStore.clear() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/src/main/java/com/zhuinden/simplestackcomposeintegration/core/util/BackstackCompose.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposeintegration.core.util 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.runtime.* 5 | import com.zhuinden.simplestack.Backstack 6 | import com.zhuinden.simplestack.History 7 | 8 | /** 9 | * Get current history as a compose [State] that will automatically be updated on every navigation. 10 | */ 11 | @Composable 12 | fun Backstack.historyAsState(): State> { 13 | // History is a Mutable class, but we return a copy, so mutating has no effect 14 | @SuppressLint("MutableCollectionMutableState") 15 | val state = remember { mutableStateOf>(getHistory()) } 16 | 17 | DisposableEffect(this) { 18 | val listener = Backstack.CompletionListener { 19 | state.value = it.getNewKeys() 20 | } 21 | 22 | addStateChangeCompletionListener(listener) 23 | 24 | onDispose { 25 | removeStateChangeCompletionListener(listener) 26 | } 27 | } 28 | 29 | return state 30 | } 31 | -------------------------------------------------------------------------------- /core/src/test/java/com/zhuinden/simplestackcomposeintegration/TestSuite.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Gabor Varadi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zhuinden.simplestackcomposeintegration; 18 | 19 | import org.junit.runner.RunWith; 20 | import org.junit.runners.Suite; 21 | 22 | @RunWith(Suite.class) 23 | @Suite.SuiteClasses({ 24 | }) 25 | public class TestSuite { 26 | } 27 | -------------------------------------------------------------------------------- /example-dogs/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /example-dogs/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-parcelize' 4 | 5 | android { 6 | compileSdkVersion 33 7 | 8 | defaultConfig { 9 | applicationId "com.zhuinden.simplestackcomposedogexample" 10 | minSdkVersion 21 11 | targetSdkVersion 31 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | kotlinOptions { 29 | jvmTarget = '1.8' 30 | languageVersion = '1.9' 31 | } 32 | buildFeatures { 33 | compose true 34 | } 35 | composeOptions { 36 | kotlinCompilerExtensionVersion '1.4.8' 37 | } 38 | } 39 | 40 | kotlin.sourceSets.all { 41 | languageSettings.enableLanguageFeature("DataObjects") 42 | } 43 | 44 | dependencies { 45 | implementation fileTree(dir: "libs", include: ["*.jar"]) 46 | implementation 'androidx.core:core-ktx:1.10.1' 47 | implementation 'androidx.activity:activity-ktx:1.7.2' 48 | implementation 'androidx.appcompat:appcompat:1.6.1' 49 | 50 | implementation(project(":core")) 51 | implementation(project(":services")) 52 | 53 | implementation "androidx.compose.runtime:runtime:1.4.3" 54 | implementation "androidx.compose.ui:ui:1.4.3" 55 | implementation "androidx.compose.ui:ui-util:1.4.3" 56 | implementation "androidx.compose.ui:ui-text:1.4.3" 57 | implementation "androidx.compose.ui:ui-unit:1.4.3" 58 | implementation "androidx.compose.ui:ui-geometry:1.4.3" 59 | implementation "androidx.compose.foundation:foundation:1.4.3" 60 | implementation "androidx.compose.foundation:foundation-layout:1.4.3" 61 | implementation "androidx.compose.material:material:1.4.3" 62 | implementation "androidx.compose.material:material-icons-extended:1.4.3" 63 | implementation "androidx.compose.animation:animation:1.4.3" 64 | implementation "androidx.compose.ui:ui-tooling:1.4.3" 65 | 66 | implementation 'androidx.activity:activity-compose:1.7.2' 67 | 68 | implementation('com.github.Zhuinden:simple-stack:2.9.0') { 69 | transitive = true 70 | } 71 | implementation 'com.github.Zhuinden:simple-stack-extensions:2.3.4' 72 | 73 | implementation 'com.google.code.gson:gson:2.10.1' 74 | 75 | implementation "io.coil-kt:coil-compose:1.3.1" 76 | 77 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 78 | 79 | implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.1' 80 | implementation "io.reactivex.rxjava2:rxjava:2.2.21" 81 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' 82 | implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' 83 | 84 | implementation "androidx.compose.runtime:runtime-rxjava2:1.4.3" 85 | 86 | testImplementation 'junit:junit:4.13.2' 87 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 88 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 89 | 90 | } -------------------------------------------------------------------------------- /example-dogs/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /example-dogs/src/androidTest/java/com/zhuinden/simplestackcomposedogexample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.zhuinden.firstcomposeapp", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /example-dogs/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/app/CustomApplication.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.app 2 | 3 | import android.app.Application 4 | import com.zhuinden.simplestack.GlobalServices 5 | import com.zhuinden.simplestackcomposedogexample.data.datasource.DogDataSource 6 | import com.zhuinden.simplestackextensions.servicesktx.add 7 | import io.reactivex.android.schedulers.AndroidSchedulers 8 | import kotlin.random.Random 9 | 10 | class CustomApplication : Application() { 11 | lateinit var globalServices: GlobalServices 12 | private set 13 | 14 | override fun onCreate() { 15 | super.onCreate() 16 | 17 | val random = Random.Default 18 | 19 | val mainThreadScheduler = AndroidSchedulers.mainThread() 20 | 21 | globalServices = GlobalServices.builder() 22 | .add(DogDataSource(random, mainThreadScheduler)) 23 | .build() 24 | } 25 | } -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.app 2 | 3 | import android.os.Bundle 4 | import androidx.activity.OnBackPressedCallback 5 | import androidx.activity.compose.setContent 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.Surface 11 | import androidx.compose.ui.Modifier 12 | import com.zhuinden.simplestack.AsyncStateChanger 13 | import com.zhuinden.simplestack.BackHandlingModel 14 | import com.zhuinden.simplestack.Backstack 15 | import com.zhuinden.simplestack.History 16 | import com.zhuinden.simplestack.navigator.Navigator 17 | import com.zhuinden.simplestackcomposedogexample.features.doglist.DogListKey 18 | import com.zhuinden.simplestackcomposeintegration.core.BackstackProvider 19 | import com.zhuinden.simplestackcomposeintegration.core.ComposeStateChanger 20 | import com.zhuinden.simplestackextensions.lifecyclektx.observeAheadOfTimeWillHandleBackChanged 21 | import com.zhuinden.simplestackextensions.navigatorktx.androidContentFrame 22 | import com.zhuinden.simplestackextensions.services.DefaultServiceProvider 23 | 24 | class MainActivity : AppCompatActivity() { 25 | private val composeStateChanger = ComposeStateChanger() 26 | 27 | private lateinit var backstack: Backstack 28 | 29 | private val backPressedCallback = object : OnBackPressedCallback(false) { 30 | override fun handleOnBackPressed() { 31 | backstack.goBack() 32 | } 33 | } 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | 38 | onBackPressedDispatcher.addCallback(backPressedCallback) 39 | 40 | val app = application as CustomApplication 41 | 42 | backstack = Navigator.configure() 43 | .setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME) 44 | .setGlobalServices(app.globalServices) 45 | .setScopedServices(DefaultServiceProvider()) 46 | .setStateChanger(AsyncStateChanger(composeStateChanger)) 47 | .install(this, androidContentFrame, History.of(DogListKey())) 48 | 49 | setContent { 50 | BackstackProvider(backstack) { 51 | MaterialTheme { 52 | Surface(color = MaterialTheme.colors.background) { 53 | Box(Modifier.fillMaxSize()) { 54 | composeStateChanger.RenderScreen() 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | backPressedCallback.isEnabled = backstack.willHandleAheadOfTimeBack() 62 | 63 | backstack.observeAheadOfTimeWillHandleBackChanged(this) { 64 | backPressedCallback.isEnabled = it 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/core/models/ModelExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.core.models 2 | 3 | import com.zhuinden.simplestackcomposedogexample.data.models.Dog 4 | 5 | fun Dog.contentDescription(): String = run { 6 | val dog = this 7 | "${dog.name}, ${dog.determinedSex.name}, ${dog.breed}, ${dog.age} years old" 8 | } -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/core/navigation/ComposeKey.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.core.navigation 2 | 3 | import android.os.Parcelable 4 | import com.zhuinden.simplestack.ServiceBinder 5 | import com.zhuinden.simplestackcomposeintegration.core.DefaultComposeKey 6 | import com.zhuinden.simplestackextensions.services.DefaultServiceProvider 7 | 8 | abstract class ComposeKey : DefaultComposeKey(), Parcelable, DefaultServiceProvider.HasServices { 9 | override val saveableStateProviderKey: Any = this 10 | 11 | override fun getScopeTag(): String = javaClass.name 12 | 13 | override fun bindServices(serviceBinder: ServiceBinder) { 14 | } 15 | } -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/core/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.core.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.material.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.text.TextStyle 9 | import androidx.compose.ui.text.font.FontFamily 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.unit.dp 12 | import androidx.compose.ui.unit.sp 13 | 14 | val purple200 = Color(0xFFBB86FC) 15 | val purple500 = Color(0xFF6200EE) 16 | val purple700 = Color(0xFF3700B3) 17 | val teal200 = Color(0xFF03DAC5) 18 | 19 | private val DarkColorPalette = darkColors( 20 | primary = purple200, 21 | primaryVariant = purple700, 22 | secondary = teal200 23 | ) 24 | 25 | private val LightColorPalette = lightColors( 26 | primary = purple500, 27 | primaryVariant = purple700, 28 | secondary = teal200 29 | ) 30 | 31 | val typography = Typography( 32 | body1 = TextStyle( 33 | fontFamily = FontFamily.Default, 34 | fontWeight = FontWeight.Normal, 35 | fontSize = 16.sp 36 | ) 37 | ) 38 | 39 | val shapes = Shapes( 40 | small = RoundedCornerShape(4.dp), 41 | medium = RoundedCornerShape(4.dp), 42 | large = RoundedCornerShape(0.dp) 43 | ) 44 | 45 | @Composable 46 | fun MyTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { 47 | val colors = when { 48 | darkTheme -> DarkColorPalette 49 | else -> LightColorPalette 50 | } 51 | 52 | MaterialTheme( 53 | colors = colors, 54 | typography = typography, 55 | shapes = shapes, 56 | content = content 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/data/models/Dog.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.data.models 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class Dog( 8 | val name: String, 9 | val age: Int, 10 | val determinedSex: DeterminedSex, 11 | val breed: String, 12 | val imageUrl: String, 13 | ) : Parcelable { 14 | enum class DeterminedSex { 15 | MALE, 16 | FEMALE; 17 | } 18 | } -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/features/dogdetail/DogDetailKey.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.features.dogdetail 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Immutable 5 | import androidx.compose.ui.Modifier 6 | import com.zhuinden.simplestackcomposedogexample.core.navigation.ComposeKey 7 | import com.zhuinden.simplestackcomposedogexample.data.models.Dog 8 | import kotlinx.parcelize.Parcelize 9 | 10 | @Immutable 11 | @Parcelize 12 | data class DogDetailKey(val dog: Dog) : ComposeKey() { 13 | @Composable 14 | override fun ScreenComposable(modifier: Modifier) { 15 | DogDetailScreen(dog = dog) 16 | } 17 | } -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/features/dogdetail/DogDetailScreen.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.features.dogdetail 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.heightIn 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.rememberScrollState 11 | import androidx.compose.foundation.verticalScroll 12 | import androidx.compose.material.MaterialTheme 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.layout.ContentScale 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.unit.dp 20 | import coil.compose.rememberImagePainter 21 | import coil.request.ImageRequest 22 | import com.zhuinden.simplestackcomposedogexample.core.models.contentDescription 23 | import com.zhuinden.simplestackcomposedogexample.data.models.Dog 24 | import okhttp3.HttpUrl 25 | import java.util.Locale 26 | 27 | @Composable 28 | fun DogDetailScreen(dog: Dog) { 29 | val context = LocalContext.current 30 | 31 | val scrollState = rememberScrollState() 32 | 33 | Column(modifier = Modifier.verticalScroll(state = scrollState)) { 34 | val painter = rememberImagePainter(ImageRequest.Builder(context).data(HttpUrl.parse(dog.imageUrl)).build()) 35 | 36 | Image( 37 | painter = painter, 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .heightIn(min = 240.dp, max = 280.dp), 41 | contentDescription = dog.contentDescription(), 42 | alignment = Alignment.TopCenter, 43 | contentScale = ContentScale.FillWidth, 44 | ) 45 | 46 | Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp)) { 47 | Text( 48 | text = dog.name, 49 | style = MaterialTheme.typography.h2, 50 | ) 51 | 52 | Spacer(modifier = Modifier.height(8.dp)) 53 | 54 | Text( 55 | text = "${dog.age}, ${dog.determinedSex.name.toLowerCase(Locale.getDefault())}", 56 | style = MaterialTheme.typography.h3, 57 | ) 58 | 59 | Spacer(modifier = Modifier.height(8.dp)) 60 | 61 | Text( 62 | text = "${dog.name} is a cute ${dog.breed} and is waiting to be adopted, taken home, handled with care and treated with love. You should definitely do your best to adopt ${dog.name}!", 63 | style = MaterialTheme.typography.body1, 64 | ) 65 | 66 | Spacer(modifier = Modifier.height(32.dp)) 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/features/doglist/DogListKey.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.features.doglist 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Immutable 5 | import androidx.compose.runtime.rxjava2.subscribeAsState 6 | import androidx.compose.ui.Modifier 7 | import com.zhuinden.simplestack.ServiceBinder 8 | import com.zhuinden.simplestackcomposedogexample.core.navigation.ComposeKey 9 | import com.zhuinden.simplestackcomposedogexample.data.datasource.DogDataSource 10 | import com.zhuinden.simplestackcomposedogexample.utils.OptionalWrapper 11 | import com.zhuinden.simplestackcomposeintegration.services.rememberService 12 | import com.zhuinden.simplestackextensions.servicesktx.add 13 | import com.zhuinden.simplestackextensions.servicesktx.lookup 14 | import kotlinx.parcelize.Parcelize 15 | 16 | @Immutable 17 | @Parcelize 18 | data class DogListKey(private val noArgPlaceholder: String = "") : ComposeKey() { 19 | @Suppress("RemoveExplicitTypeArguments") 20 | override fun bindServices(serviceBinder: ServiceBinder) { 21 | super.bindServices(serviceBinder) 22 | with(serviceBinder) { 23 | add(DogListViewModel(lookup())) 24 | } 25 | } 26 | 27 | @Composable 28 | override fun ScreenComposable(modifier: Modifier) { 29 | val viewModel = rememberService() 30 | 31 | val dogs = viewModel.dogList.subscribeAsState(OptionalWrapper.absent()) 32 | 33 | DogListScreen(dogs.value.value) 34 | } 35 | } -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/features/doglist/DogListScreen.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.features.doglist 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.foundation.lazy.LazyColumn 13 | import androidx.compose.material.CircularProgressIndicator 14 | import androidx.compose.material.Text 15 | import androidx.compose.material.TopAppBar 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.layout.ContentScale 24 | import androidx.compose.ui.layout.Layout 25 | import androidx.compose.ui.platform.LocalContext 26 | import androidx.compose.ui.platform.LocalDensity 27 | import androidx.compose.ui.unit.dp 28 | import androidx.compose.ui.util.fastForEach 29 | import androidx.compose.ui.util.fastMap 30 | import androidx.compose.ui.util.fastMaxBy 31 | import coil.compose.rememberImagePainter 32 | import coil.request.ImageRequest 33 | import coil.size.PixelSize 34 | import com.zhuinden.simplestackcomposedogexample.core.models.contentDescription 35 | import com.zhuinden.simplestackcomposedogexample.data.models.Dog 36 | import com.zhuinden.simplestackcomposedogexample.features.dogdetail.DogDetailKey 37 | import com.zhuinden.simplestackcomposeintegration.core.LocalBackstack 38 | import okhttp3.HttpUrl 39 | 40 | @Composable 41 | fun DogListScreen(dogs: List?) { 42 | @Composable 43 | fun DogItem(dog: Dog) { 44 | val context = LocalContext.current 45 | val backstack = LocalBackstack.current 46 | 47 | var fullWidth by remember { mutableStateOf(0) } 48 | 49 | val density = LocalDensity.current 50 | 51 | Layout(content = { 52 | Box( 53 | modifier = Modifier 54 | .fillMaxWidth() 55 | .height(160.dp), 56 | contentAlignment = Alignment.Center 57 | ) { 58 | if (fullWidth == 0) { 59 | CircularProgressIndicator() 60 | } else { 61 | val painter = rememberImagePainter(ImageRequest.Builder(context) 62 | .size(density.run { PixelSize(fullWidth, 160.dp.toPx().toInt()) }) 63 | .data(HttpUrl.parse(dog.imageUrl)) 64 | .build()) 65 | 66 | Image( 67 | painter = painter, 68 | contentDescription = dog.contentDescription(), 69 | contentScale = ContentScale.FillWidth, 70 | modifier = Modifier.clickable { 71 | backstack.goTo(DogDetailKey(dog)) 72 | }.width(density.run { fullWidth.toDp() }), 73 | ) 74 | } 75 | } 76 | }, measurePolicy = { measurables, constraints -> 77 | val placeables = measurables.fastMap { it.measure(constraints) } 78 | val maxWidth = placeables.fastMaxBy { it.width }?.width ?: 0 79 | val maxHeight = placeables.fastMaxBy { it.height }?.height ?: 0 80 | 81 | if (fullWidth == 0) { 82 | fullWidth = maxWidth 83 | } 84 | 85 | layout(maxWidth, maxHeight) { 86 | placeables.fastForEach { placeable -> 87 | placeable.place(0, 0) 88 | } 89 | } 90 | }) 91 | } 92 | 93 | @Composable 94 | fun DogList(dogs: List) { 95 | Box(modifier = Modifier.fillMaxSize()) { 96 | LazyColumn(modifier = Modifier.fillMaxSize(), content = { 97 | this.items(dogs.size, itemContent = { index -> 98 | DogItem(dogs[index]) 99 | }) 100 | }) 101 | } 102 | } 103 | 104 | @Composable 105 | fun LoadingIndicator() { 106 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 107 | CircularProgressIndicator() 108 | } 109 | } 110 | 111 | @Composable 112 | fun Header() { 113 | TopAppBar(modifier = Modifier.fillMaxWidth(), title = { 114 | Row { 115 | Text("Adopt A Dog") 116 | } 117 | }) 118 | } 119 | 120 | Column(modifier = Modifier.fillMaxSize()) { 121 | Header() 122 | 123 | if (dogs == null) { 124 | LoadingIndicator() 125 | } else { 126 | DogList(dogs) 127 | } 128 | } 129 | } 130 | 131 | 132 | -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/features/doglist/DogListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.features.doglist 2 | 3 | import com.jakewharton.rxrelay2.BehaviorRelay 4 | import com.zhuinden.simplestack.ScopedServices 5 | import com.zhuinden.simplestackcomposedogexample.data.datasource.DogDataSource 6 | import com.zhuinden.simplestackcomposedogexample.data.models.Dog 7 | import com.zhuinden.simplestackcomposedogexample.utils.OptionalWrapper 8 | import io.reactivex.Observable 9 | import io.reactivex.disposables.CompositeDisposable 10 | import io.reactivex.rxkotlin.plusAssign 11 | import io.reactivex.rxkotlin.subscribeBy 12 | 13 | class DogListViewModel( 14 | private val dogDataSource: DogDataSource 15 | ) : ScopedServices.Registered { 16 | private val dogListRelay = 17 | BehaviorRelay.createDefault>>(OptionalWrapper.absent()) 18 | val dogList: Observable>> = dogListRelay 19 | 20 | private val compositeDisposable = CompositeDisposable() 21 | 22 | override fun onServiceRegistered() { 23 | compositeDisposable += dogDataSource.getDogs().subscribeBy { dogs -> 24 | dogListRelay.accept(OptionalWrapper(dogs)) 25 | } 26 | } 27 | 28 | override fun onServiceUnregistered() { 29 | compositeDisposable.clear() 30 | } 31 | } -------------------------------------------------------------------------------- /example-dogs/src/main/java/com/zhuinden/simplestackcomposedogexample/utils/OptionalWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposedogexample.utils 2 | 3 | class OptionalWrapper(val value: T?) { 4 | companion object { 5 | fun absent(): OptionalWrapper = OptionalWrapper(null) 6 | } 7 | } -------------------------------------------------------------------------------- /example-dogs/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /example-dogs/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-dogs/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-dogs/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-dogs/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-dogs/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-dogs/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-dogs/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-dogs/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-dogs/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-dogs/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-dogs/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-dogs/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-dogs/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /example-dogs/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Simple-Stack Compose Dogs 3 | -------------------------------------------------------------------------------- /example-dogs/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example-ftue/src/test/java/com/zhuinden/simplestackftuecomposesample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackftuecomposesample 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example-nested-navigation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /example-nested-navigation/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-parcelize' 4 | 5 | android { 6 | compileSdkVersion 33 7 | 8 | defaultConfig { 9 | applicationId "com.zhuinden.simplestackcomposenestedexample" 10 | minSdkVersion 21 11 | targetSdkVersion 31 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | buildFeatures { 29 | compose true 30 | } 31 | composeOptions { 32 | kotlinCompilerExtensionVersion '1.4.8' 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = "1.8" 37 | languageVersion = "1.9" // data objects 38 | } 39 | } 40 | 41 | kotlin.sourceSets.all { 42 | languageSettings.enableLanguageFeature("DataObjects") 43 | } 44 | 45 | dependencies { 46 | implementation fileTree(dir: "libs", include: ["*.jar"]) 47 | implementation 'androidx.core:core-ktx:1.10.1' 48 | implementation 'androidx.appcompat:appcompat:1.6.1' 49 | 50 | implementation(project(":core")) 51 | implementation(project(":services")) 52 | 53 | implementation "androidx.compose.runtime:runtime:1.4.3" 54 | implementation "androidx.compose.ui:ui:1.4.3" 55 | implementation "androidx.compose.ui:ui-util:1.4.3" 56 | implementation "androidx.compose.ui:ui-text:1.4.3" 57 | implementation "androidx.compose.ui:ui-unit:1.4.3" 58 | implementation "androidx.compose.ui:ui-geometry:1.4.3" 59 | implementation "androidx.compose.foundation:foundation:1.4.3" 60 | implementation "androidx.compose.foundation:foundation-layout:1.4.3" 61 | implementation "androidx.compose.material:material:1.4.3" 62 | implementation "androidx.compose.material:material-icons-extended:1.4.3" 63 | implementation "androidx.compose.animation:animation:1.4.3" 64 | implementation "androidx.compose.ui:ui-tooling:1.4.3" 65 | 66 | implementation 'androidx.activity:activity-compose:1.7.2' 67 | 68 | implementation('com.github.Zhuinden:simple-stack:2.9.0') { 69 | transitive = true 70 | } 71 | implementation 'com.github.Zhuinden:simple-stack-extensions:2.3.4' 72 | 73 | implementation 'com.google.code.gson:gson:2.10.1' 74 | 75 | testImplementation 'junit:junit:4.13.2' 76 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 77 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 78 | 79 | } -------------------------------------------------------------------------------- /example-nested-navigation/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /example-nested-navigation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/java/com/zhuinden/simplestackcomposenestedexample/ComposeKey.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposenestedexample 2 | 3 | import android.os.Parcelable 4 | import com.zhuinden.simplestack.ServiceBinder 5 | import com.zhuinden.simplestackcomposeintegration.core.DefaultComposeKey 6 | import com.zhuinden.simplestackextensions.services.DefaultServiceProvider 7 | 8 | abstract class ComposeKey: DefaultComposeKey(), Parcelable, DefaultServiceProvider.HasServices { 9 | override val saveableStateProviderKey: Any = this 10 | 11 | override fun getScopeTag(): String = javaClass.name 12 | 13 | override fun bindServices(serviceBinder: ServiceBinder) { 14 | } 15 | } -------------------------------------------------------------------------------- /example-nested-navigation/src/main/java/com/zhuinden/simplestackcomposenestedexample/FirstNestedScreen.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposenestedexample 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material.Button 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.Immutable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import com.zhuinden.simplestack.Backstack 18 | import com.zhuinden.simplestack.ServiceBinder 19 | import com.zhuinden.simplestackcomposeintegration.core.BackstackProvider 20 | import com.zhuinden.simplestackcomposeintegration.services.rememberService 21 | import com.zhuinden.simplestackextensions.servicesktx.add 22 | import com.zhuinden.simplestackextensions.servicesktx.rebind 23 | import kotlinx.parcelize.Parcelize 24 | 25 | class FirstNestedModel( 26 | private val backstack: Backstack 27 | ): FirstNestedScreen.ActionHandler { 28 | override fun navigateToSecond() { 29 | backstack.goTo(SecondNestedKey()) 30 | } 31 | } 32 | 33 | @Immutable 34 | @Parcelize 35 | data class FirstNestedKey(val title: String) : ComposeKey() { 36 | constructor() : this("Hello First Nested Screen!") 37 | 38 | @Composable 39 | override fun ScreenComposable(modifier: Modifier) { 40 | FirstNestedScreen(title, modifier) 41 | } 42 | 43 | override fun bindServices(serviceBinder: ServiceBinder) { 44 | with(serviceBinder) { 45 | val firstModel = FirstNestedModel(backstack) 46 | 47 | add(firstModel) 48 | rebind(firstModel) 49 | } 50 | } 51 | } 52 | 53 | class FirstNestedScreen private constructor() { 54 | fun interface ActionHandler { 55 | fun navigateToSecond() 56 | } 57 | 58 | companion object { 59 | @Composable 60 | @SuppressLint("ComposableNaming") 61 | operator fun invoke(title: String, modifier: Modifier = Modifier) { 62 | val eventHandler = rememberService() 63 | 64 | Column( 65 | modifier = modifier 66 | .background(Color(0x80, 0x80, 0xFF)) 67 | .fillMaxSize(), 68 | verticalArrangement = Arrangement.Center, 69 | horizontalAlignment = Alignment.CenterHorizontally 70 | ) { 71 | Button(onClick = { 72 | // onClick is not a composition context, must get ambients above 73 | eventHandler.navigateToSecond() 74 | }, content = { 75 | Text(title) 76 | }) 77 | } 78 | } 79 | } 80 | } 81 | 82 | @Preview 83 | @Composable 84 | fun FirstNestedScreenPreview() { 85 | BackstackProvider(backstack = Backstack()) { 86 | MaterialTheme { 87 | FirstNestedScreen("This is a preview") 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/java/com/zhuinden/simplestackcomposenestedexample/FirstScreen.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposenestedexample 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material.Button 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.Immutable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.tooling.preview.Preview 15 | import com.zhuinden.simplestack.Backstack 16 | import com.zhuinden.simplestack.ServiceBinder 17 | import com.zhuinden.simplestackcomposeintegration.core.BackstackProvider 18 | import com.zhuinden.simplestackcomposeintegration.services.rememberService 19 | import com.zhuinden.simplestackextensions.servicesktx.add 20 | import com.zhuinden.simplestackextensions.servicesktx.rebind 21 | import kotlinx.parcelize.Parcelize 22 | 23 | class FirstModel( 24 | private val backstack: Backstack 25 | ): FirstScreen.ActionHandler { 26 | override fun navigateToSecond() { 27 | backstack.goTo(SecondKey()) 28 | } 29 | } 30 | 31 | @Immutable 32 | @Parcelize 33 | data class FirstKey(val title: String) : ComposeKey() { 34 | constructor() : this("Open nested stack screen") 35 | 36 | @Composable 37 | override fun ScreenComposable(modifier: Modifier) { 38 | FirstScreen(title, modifier) 39 | } 40 | 41 | override fun bindServices(serviceBinder: ServiceBinder) { 42 | with(serviceBinder) { 43 | val firstModel = FirstModel(backstack) 44 | 45 | add(firstModel) 46 | rebind(firstModel) 47 | } 48 | } 49 | } 50 | 51 | class FirstScreen private constructor() { 52 | fun interface ActionHandler { 53 | fun navigateToSecond() 54 | } 55 | 56 | companion object { 57 | @Composable 58 | @SuppressLint("ComposableNaming") 59 | operator fun invoke(title: String, modifier: Modifier = Modifier) { 60 | val eventHandler = rememberService() 61 | 62 | Column( 63 | modifier = modifier.fillMaxSize(), 64 | verticalArrangement = Arrangement.Center, 65 | horizontalAlignment = Alignment.CenterHorizontally 66 | ) { 67 | Button(onClick = { 68 | // onClick is not a composition context, must get ambients above 69 | eventHandler.navigateToSecond() 70 | }, content = { 71 | Text(title) 72 | }) 73 | } 74 | } 75 | } 76 | } 77 | 78 | @Preview 79 | @Composable 80 | fun FirstScreenPreview() { 81 | MaterialTheme { 82 | BackstackProvider(backstack = Backstack()) { 83 | FirstScreen("This is a preview") 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/java/com/zhuinden/simplestackcomposenestedexample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposenestedexample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import com.zhuinden.simplestack.AsyncStateChanger 13 | import com.zhuinden.simplestack.History 14 | import com.zhuinden.simplestackcomposeintegration.core.BackstackProvider 15 | import com.zhuinden.simplestackcomposeintegration.core.ComposeStateChanger 16 | import com.zhuinden.simplestackcomposeintegration.core.rememberBackstack 17 | import com.zhuinden.simplestackextensions.services.DefaultServiceProvider 18 | 19 | class MainActivity : AppCompatActivity() { 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | 24 | setContent { 25 | // This is an example of a longer, more customizable way to display a backstack 26 | // You can just use ComposeNavigator() in most cases, see SecondScreen(). 27 | val composeStateChanger = remember { ComposeStateChanger() } 28 | val asyncStateChanger = remember(composeStateChanger) { AsyncStateChanger(composeStateChanger)} 29 | 30 | val backstack = rememberBackstack(asyncStateChanger) { 31 | createBackstack( 32 | scopedServices = DefaultServiceProvider(), 33 | initialKeys = History.of(FirstKey()) 34 | ) 35 | } 36 | 37 | BackstackProvider(backstack) { 38 | MaterialTheme { 39 | Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 40 | composeStateChanger.RenderScreen() 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/java/com/zhuinden/simplestackcomposenestedexample/SecondNestedScreen.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposenestedexample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material.Button 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.Immutable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import com.zhuinden.simplestack.Backstack 17 | import com.zhuinden.simplestackcomposeintegration.core.BackstackProvider 18 | import com.zhuinden.simplestackcomposeintegration.core.LocalBackstack 19 | import kotlinx.parcelize.Parcelize 20 | 21 | 22 | @Immutable 23 | @Parcelize 24 | data object SecondNestedKey: ComposeKey() { 25 | operator fun invoke() = this 26 | 27 | @Composable 28 | override fun ScreenComposable(modifier: Modifier) { 29 | SecondNestedScreen(modifier) 30 | } 31 | } 32 | 33 | @Composable 34 | fun SecondNestedScreen(modifier: Modifier = Modifier) { 35 | val backstack = LocalBackstack.current 36 | 37 | Column( 38 | modifier = modifier 39 | .background(Color(0xFF, 0x80, 0x80)) 40 | .fillMaxSize(), 41 | verticalArrangement = Arrangement.Center, 42 | horizontalAlignment = Alignment.CenterHorizontally 43 | ) { 44 | Button(onClick = { 45 | backstack.goBack() 46 | }, content = { 47 | Text("Welcome to Second Nested Screen!") 48 | }) 49 | } 50 | } 51 | 52 | @Preview 53 | @Composable 54 | fun SecondNestedScreenPreview() { 55 | MaterialTheme { 56 | BackstackProvider(backstack = Backstack()) { 57 | SecondNestedScreen() 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/java/com/zhuinden/simplestackcomposenestedexample/SecondScreen.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposenestedexample 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material.Button 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.Immutable 8 | import androidx.compose.ui.Alignment.Companion.CenterHorizontally 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | import com.zhuinden.simplestack.Backstack 14 | import com.zhuinden.simplestack.History 15 | import com.zhuinden.simplestackcomposeintegration.core.BackstackProvider 16 | import com.zhuinden.simplestackcomposeintegration.core.ComposeNavigator 17 | import com.zhuinden.simplestackcomposeintegration.core.LocalBackstack 18 | import com.zhuinden.simplestackextensions.services.DefaultServiceProvider 19 | import kotlinx.parcelize.Parcelize 20 | 21 | @Immutable 22 | @Parcelize 23 | data object SecondKey: ComposeKey() { 24 | operator fun invoke() = this 25 | 26 | @Composable 27 | override fun ScreenComposable(modifier: Modifier) { 28 | SecondScreen() 29 | } 30 | } 31 | 32 | @Composable 33 | fun SecondScreen() { 34 | val backstack = LocalBackstack.current 35 | 36 | Column( 37 | modifier = Modifier.fillMaxSize(), 38 | ) { 39 | Column(modifier = Modifier.padding(16.dp)) { 40 | Text(fontWeight = FontWeight.Bold, text = "Single nested stack:") 41 | 42 | Spacer(modifier = Modifier.height(16.dp)) 43 | } 44 | 45 | Box( 46 | modifier = Modifier 47 | .weight(1f) 48 | .fillMaxWidth(), 49 | propagateMinConstraints = true, 50 | ) { 51 | ComposeNavigator { 52 | createBackstack( 53 | History.of(FirstNestedKey()), 54 | scopedServices = DefaultServiceProvider() 55 | ) 56 | } 57 | } 58 | 59 | Button( 60 | modifier = Modifier.align(CenterHorizontally), 61 | onClick = { 62 | backstack.goTo(ThirdKey()) 63 | }, 64 | content = { 65 | Text("Open third Screen") 66 | }, 67 | ) 68 | } 69 | } 70 | 71 | @Preview 72 | @Composable 73 | fun SecondScreenPreview() { 74 | BackstackProvider(backstack = Backstack()) { 75 | SecondScreen() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/java/com/zhuinden/simplestackcomposenestedexample/ThirdScreen.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposenestedexample 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material.Button 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.Immutable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | import com.zhuinden.simplestack.Backstack 14 | import com.zhuinden.simplestack.History 15 | import com.zhuinden.simplestackcomposeintegration.core.BackstackProvider 16 | import com.zhuinden.simplestackcomposeintegration.core.ComposeNavigator 17 | import com.zhuinden.simplestackcomposeintegration.core.LocalBackstack 18 | import com.zhuinden.simplestackextensions.services.DefaultServiceProvider 19 | import kotlinx.parcelize.Parcelize 20 | 21 | @Immutable 22 | @Parcelize 23 | data object ThirdKey: ComposeKey() { 24 | operator fun invoke() = this 25 | 26 | @Composable 27 | override fun ScreenComposable(modifier: Modifier) { 28 | ThirdScreen() 29 | } 30 | } 31 | 32 | @Composable 33 | fun ThirdScreen() { 34 | val backstack = LocalBackstack.current 35 | 36 | Column( 37 | Modifier.fillMaxSize() 38 | ) { 39 | Column(modifier = Modifier.padding(16.dp)) { 40 | Text(fontWeight = FontWeight.Bold, text = "Multiple nested stacks:") 41 | 42 | Spacer(modifier = Modifier.height(16.dp)) 43 | } 44 | 45 | Box( 46 | Modifier 47 | .weight(1f) 48 | .fillMaxWidth() 49 | .padding(bottom = 8.dp), 50 | propagateMinConstraints = true 51 | ) { 52 | ComposeNavigator(id = "TOP", interceptBackButton = false) { 53 | createBackstack( 54 | History.of(FirstNestedKey()), 55 | scopedServices = DefaultServiceProvider() 56 | ) 57 | } 58 | } 59 | 60 | Box( 61 | Modifier 62 | .weight(1f) 63 | .fillMaxWidth() 64 | .padding(bottom = 8.dp), 65 | propagateMinConstraints = true 66 | ) { 67 | ComposeNavigator(id = "BOTTOM", interceptBackButton = false) { 68 | createBackstack( 69 | History.of(FirstNestedKey()), 70 | scopedServices = DefaultServiceProvider() 71 | ) 72 | } 73 | } 74 | 75 | Button( 76 | modifier = Modifier.align(Alignment.CenterHorizontally), 77 | onClick = { 78 | backstack.goBack() 79 | }, 80 | content = { 81 | Text("Go back (main)!") 82 | }, 83 | ) 84 | } 85 | } 86 | 87 | @Preview 88 | @Composable 89 | fun ThirdScreenPreview() { 90 | BackstackProvider(backstack = Backstack()) { 91 | ThirdScreen() 92 | } 93 | } -------------------------------------------------------------------------------- /example-nested-navigation/src/main/java/com/zhuinden/simplestackcomposenestedexample/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.zhuinden.simplestackcomposenestedexample 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | 6 | fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) { 7 | Toast.makeText(this, message, duration).show() 8 | } -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-nested-navigation/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-nested-navigation/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-nested-navigation/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-nested-navigation/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-nested-navigation/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-nested-navigation/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-nested-navigation/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-nested-navigation/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-nested-navigation/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuinden/simple-stack-compose-integration/e55701821ed4cdb77eb7b664f4829b496ee2226f/example-nested-navigation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FirstComposeApp 3 | -------------------------------------------------------------------------------- /example-nested-navigation/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 | 9 | 10 | 14 | 15 |