Refactored NiaNavigator

Made NiaNavigator a stateless class only responsibly for navigating and pop (modifying backStack).
Navigation state now lives in a new class called NiaNavigatorState.

The state of this class is saved and restored by ViewModel.
pull/1902/head
Clara Fok 4 months ago
parent a878b170dc
commit b91a965ae2

@ -12,47 +12,49 @@ androidx.browser:browser:1.8.0
androidx.collection:collection-jvm:1.5.0
androidx.collection:collection-ktx:1.5.0
androidx.collection:collection:1.5.0
androidx.compose.animation:animation-android:1.10.0-alpha02
androidx.compose.animation:animation-core-android:1.10.0-alpha02
androidx.compose.animation:animation-core:1.10.0-alpha02
androidx.compose.animation:animation:1.10.0-alpha02
androidx.compose.foundation:foundation-android:1.10.0-alpha02
androidx.compose.foundation:foundation-layout-android:1.10.0-alpha02
androidx.compose.foundation:foundation-layout:1.10.0-alpha02
androidx.compose.foundation:foundation:1.10.0-alpha02
androidx.compose.material3.adaptive:adaptive-android:1.2.0-beta01
androidx.compose.material3.adaptive:adaptive:1.2.0-beta01
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.5.0-alpha03
androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha03
androidx.compose.material3:material3-android:1.5.0-alpha03
androidx.compose.material3:material3:1.5.0-alpha03
androidx.compose.animation:animation-android:1.10.0-alpha04
androidx.compose.animation:animation-core-android:1.10.0-alpha04
androidx.compose.animation:animation-core:1.10.0-alpha04
androidx.compose.animation:animation:1.10.0-alpha04
androidx.compose.foundation:foundation-android:1.10.0-alpha04
androidx.compose.foundation:foundation-layout-android:1.10.0-alpha04
androidx.compose.foundation:foundation-layout:1.10.0-alpha04
androidx.compose.foundation:foundation:1.10.0-alpha04
androidx.compose.material3.adaptive:adaptive-android:1.2.0-beta03
androidx.compose.material3.adaptive:adaptive:1.2.0-beta03
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.5.0-alpha04
androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha04
androidx.compose.material3:material3-android:1.5.0-alpha04
androidx.compose.material3:material3:1.5.0-alpha04
androidx.compose.material:material-icons-core-android:1.7.8
androidx.compose.material:material-icons-core:1.7.8
androidx.compose.material:material-icons-extended-android:1.7.8
androidx.compose.material:material-icons-extended:1.7.8
androidx.compose.material:material-ripple-android:1.10.0-alpha02
androidx.compose.material:material-ripple:1.10.0-alpha02
androidx.compose.runtime:runtime-android:1.10.0-alpha02
androidx.compose.runtime:runtime-annotation-android:1.10.0-alpha02
androidx.compose.runtime:runtime-annotation:1.10.0-alpha02
androidx.compose.runtime:runtime-saveable-android:1.10.0-alpha02
androidx.compose.runtime:runtime-saveable:1.10.0-alpha02
androidx.compose.runtime:runtime:1.10.0-alpha02
androidx.compose.ui:ui-android:1.10.0-alpha02
androidx.compose.ui:ui-geometry-android:1.10.0-alpha02
androidx.compose.ui:ui-geometry:1.10.0-alpha02
androidx.compose.ui:ui-graphics-android:1.10.0-alpha02
androidx.compose.ui:ui-graphics:1.10.0-alpha02
androidx.compose.ui:ui-text-android:1.10.0-alpha02
androidx.compose.ui:ui-text:1.10.0-alpha02
androidx.compose.ui:ui-tooling-preview-android:1.10.0-alpha02
androidx.compose.ui:ui-tooling-preview:1.10.0-alpha02
androidx.compose.ui:ui-unit-android:1.10.0-alpha02
androidx.compose.ui:ui-unit:1.10.0-alpha02
androidx.compose.ui:ui-util-android:1.10.0-alpha02
androidx.compose.ui:ui-util:1.10.0-alpha02
androidx.compose.ui:ui:1.10.0-alpha02
androidx.compose:compose-bom-alpha:2025.08.01
androidx.compose.material:material-ripple-android:1.10.0-alpha04
androidx.compose.material:material-ripple:1.10.0-alpha04
androidx.compose.runtime:runtime-android:1.10.0-alpha04
androidx.compose.runtime:runtime-annotation-android:1.10.0-alpha04
androidx.compose.runtime:runtime-annotation:1.10.0-alpha04
androidx.compose.runtime:runtime-retain-android:1.10.0-alpha04
androidx.compose.runtime:runtime-retain:1.10.0-alpha04
androidx.compose.runtime:runtime-saveable-android:1.10.0-alpha04
androidx.compose.runtime:runtime-saveable:1.10.0-alpha04
androidx.compose.runtime:runtime:1.10.0-alpha04
androidx.compose.ui:ui-android:1.10.0-alpha04
androidx.compose.ui:ui-geometry-android:1.10.0-alpha04
androidx.compose.ui:ui-geometry:1.10.0-alpha04
androidx.compose.ui:ui-graphics-android:1.10.0-alpha04
androidx.compose.ui:ui-graphics:1.10.0-alpha04
androidx.compose.ui:ui-text-android:1.10.0-alpha04
androidx.compose.ui:ui-text:1.10.0-alpha04
androidx.compose.ui:ui-tooling-preview-android:1.10.0-alpha04
androidx.compose.ui:ui-tooling-preview:1.10.0-alpha04
androidx.compose.ui:ui-unit-android:1.10.0-alpha04
androidx.compose.ui:ui-unit:1.10.0-alpha04
androidx.compose.ui:ui-util-android:1.10.0-alpha04
androidx.compose.ui:ui-util:1.10.0-alpha04
androidx.compose.ui:ui:1.10.0-alpha04
androidx.compose:compose-bom-alpha:2025.09.01
androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.16.0
androidx.core:core-viewtree:1.0.0
@ -69,34 +71,34 @@ androidx.graphics:graphics-shapes-android:1.0.1
androidx.graphics:graphics-shapes:1.0.1
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.10.0-alpha03
androidx.lifecycle:lifecycle-common-jvm:2.10.0-alpha03
androidx.lifecycle:lifecycle-common:2.10.0-alpha03
androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0-alpha03
androidx.lifecycle:lifecycle-livedata-core:2.10.0-alpha03
androidx.lifecycle:lifecycle-livedata:2.10.0-alpha03
androidx.lifecycle:lifecycle-process:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime-compose-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime-compose:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime-ktx:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel:2.10.0-alpha03
androidx.lifecycle:lifecycle-common-java8:2.9.4
androidx.lifecycle:lifecycle-common-jvm:2.9.4
androidx.lifecycle:lifecycle-common:2.9.4
androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.4
androidx.lifecycle:lifecycle-livedata-core:2.9.4
androidx.lifecycle:lifecycle-livedata:2.9.4
androidx.lifecycle:lifecycle-process:2.9.4
androidx.lifecycle:lifecycle-runtime-android:2.9.4
androidx.lifecycle:lifecycle-runtime-compose-android:2.9.4
androidx.lifecycle:lifecycle-runtime-compose:2.9.4
androidx.lifecycle:lifecycle-runtime-ktx-android:2.9.4
androidx.lifecycle:lifecycle-runtime-ktx:2.9.4
androidx.lifecycle:lifecycle-runtime:2.9.4
androidx.lifecycle:lifecycle-viewmodel-android:2.9.4
androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.9.4
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.4
androidx.lifecycle:lifecycle-viewmodel:2.9.4
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.metrics:metrics-performance:1.0.0-beta01
androidx.print:print:1.0.0
androidx.profileinstaller:profileinstaller:1.4.0
androidx.savedstate:savedstate-android:1.4.0-alpha03
androidx.savedstate:savedstate-compose-android:1.4.0-alpha03
androidx.savedstate:savedstate-compose:1.4.0-alpha03
androidx.savedstate:savedstate-ktx:1.4.0-alpha03
androidx.savedstate:savedstate:1.4.0-alpha03
androidx.savedstate:savedstate-android:1.3.2
androidx.savedstate:savedstate-compose-android:1.3.2
androidx.savedstate:savedstate-compose:1.3.2
androidx.savedstate:savedstate-ktx:1.3.2
androidx.savedstate:savedstate:1.3.2
androidx.startup:startup-runtime:1.1.1
androidx.tracing:tracing-ktx:1.3.0-alpha02
androidx.tracing:tracing:1.3.0-alpha02
@ -128,10 +130,10 @@ org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
org.jetbrains.kotlin:kotlin-stdlib:2.2.21
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1
org.jetbrains.kotlinx:kotlinx-datetime:0.6.1
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3

@ -1,6 +1,6 @@
androidx.activity:activity-compose:1.12.0-SNAPSHOT
androidx.activity:activity-ktx:1.12.0-SNAPSHOT
androidx.activity:activity:1.12.0-SNAPSHOT
androidx.activity:activity-compose:1.12.0
androidx.activity:activity-ktx:1.12.0
androidx.activity:activity:1.12.0
androidx.annotation:annotation-experimental:1.5.1
androidx.annotation:annotation-jvm:1.9.1
androidx.annotation:annotation:1.9.1
@ -13,56 +13,58 @@ androidx.browser:browser:1.8.0
androidx.collection:collection-jvm:1.5.0
androidx.collection:collection-ktx:1.5.0
androidx.collection:collection:1.5.0
androidx.compose.animation:animation-android:1.10.0-alpha02
androidx.compose.animation:animation-core-android:1.10.0-alpha02
androidx.compose.animation:animation-core:1.10.0-alpha02
androidx.compose.animation:animation:1.10.0-alpha02
androidx.compose.foundation:foundation-android:1.10.0-alpha02
androidx.compose.foundation:foundation-layout-android:1.10.0-alpha02
androidx.compose.foundation:foundation-layout:1.10.0-alpha02
androidx.compose.foundation:foundation:1.10.0-alpha02
androidx.compose.material3.adaptive:adaptive-android:1.2.0-beta01
androidx.compose.material3.adaptive:adaptive-layout-android:1.2.0-beta01
androidx.compose.material3.adaptive:adaptive-layout:1.2.0-beta01
androidx.compose.material3.adaptive:adaptive-navigation-android:1.2.0-beta01
androidx.compose.material3.adaptive:adaptive-navigation3-android:1.0.0-SNAPSHOT
androidx.compose.material3.adaptive:adaptive-navigation3:1.0.0-SNAPSHOT
androidx.compose.material3.adaptive:adaptive-navigation:1.2.0-beta01
androidx.compose.material3.adaptive:adaptive:1.2.0-beta01
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.5.0-alpha03
androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha03
androidx.compose.material3:material3-android:1.5.0-alpha03
androidx.compose.material3:material3-window-size-class-android:1.5.0-alpha03
androidx.compose.material3:material3-window-size-class:1.5.0-alpha03
androidx.compose.material3:material3:1.5.0-alpha03
androidx.compose.animation:animation-android:1.10.0-beta02
androidx.compose.animation:animation-core-android:1.10.0-beta02
androidx.compose.animation:animation-core:1.10.0-beta02
androidx.compose.animation:animation:1.10.0-beta02
androidx.compose.foundation:foundation-android:1.10.0-beta02
androidx.compose.foundation:foundation-layout-android:1.10.0-beta02
androidx.compose.foundation:foundation-layout:1.10.0-beta02
androidx.compose.foundation:foundation:1.10.0-beta02
androidx.compose.material3.adaptive:adaptive-android:1.3.0-alpha04
androidx.compose.material3.adaptive:adaptive-layout-android:1.3.0-alpha04
androidx.compose.material3.adaptive:adaptive-layout:1.3.0-alpha04
androidx.compose.material3.adaptive:adaptive-navigation-android:1.3.0-alpha04
androidx.compose.material3.adaptive:adaptive-navigation3-android:1.3.0-alpha04
androidx.compose.material3.adaptive:adaptive-navigation3:1.3.0-alpha04
androidx.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha04
androidx.compose.material3.adaptive:adaptive:1.3.0-alpha04
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.5.0-alpha04
androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha04
androidx.compose.material3:material3-android:1.5.0-alpha04
androidx.compose.material3:material3-window-size-class-android:1.5.0-alpha04
androidx.compose.material3:material3-window-size-class:1.5.0-alpha04
androidx.compose.material3:material3:1.5.0-alpha04
androidx.compose.material:material-icons-core-android:1.7.8
androidx.compose.material:material-icons-core:1.7.8
androidx.compose.material:material-icons-extended-android:1.7.8
androidx.compose.material:material-icons-extended:1.7.8
androidx.compose.material:material-ripple-android:1.10.0-alpha02
androidx.compose.material:material-ripple:1.10.0-alpha02
androidx.compose.runtime:runtime-android:1.10.0-alpha02
androidx.compose.runtime:runtime-annotation-android:1.10.0-alpha02
androidx.compose.runtime:runtime-annotation:1.10.0-alpha02
androidx.compose.runtime:runtime-saveable-android:1.10.0-alpha02
androidx.compose.runtime:runtime-saveable:1.10.0-alpha02
androidx.compose.runtime:runtime-tracing:1.10.0-alpha02
androidx.compose.runtime:runtime:1.10.0-alpha02
androidx.compose.ui:ui-android:1.10.0-alpha02
androidx.compose.ui:ui-geometry-android:1.10.0-alpha02
androidx.compose.ui:ui-geometry:1.10.0-alpha02
androidx.compose.ui:ui-graphics-android:1.10.0-alpha02
androidx.compose.ui:ui-graphics:1.10.0-alpha02
androidx.compose.ui:ui-text-android:1.10.0-alpha02
androidx.compose.ui:ui-text:1.10.0-alpha02
androidx.compose.ui:ui-tooling-preview-android:1.10.0-alpha02
androidx.compose.ui:ui-tooling-preview:1.10.0-alpha02
androidx.compose.ui:ui-unit-android:1.10.0-alpha02
androidx.compose.ui:ui-unit:1.10.0-alpha02
androidx.compose.ui:ui-util-android:1.10.0-alpha02
androidx.compose.ui:ui-util:1.10.0-alpha02
androidx.compose.ui:ui:1.10.0-alpha02
androidx.compose:compose-bom-alpha:2025.08.01
androidx.compose.material:material-ripple-android:1.10.0-alpha04
androidx.compose.material:material-ripple:1.10.0-alpha04
androidx.compose.runtime:runtime-android:1.10.0-beta02
androidx.compose.runtime:runtime-annotation-android:1.10.0-beta02
androidx.compose.runtime:runtime-annotation:1.10.0-beta02
androidx.compose.runtime:runtime-retain-android:1.10.0-beta02
androidx.compose.runtime:runtime-retain:1.10.0-beta02
androidx.compose.runtime:runtime-saveable-android:1.10.0-beta02
androidx.compose.runtime:runtime-saveable:1.10.0-beta02
androidx.compose.runtime:runtime-tracing:1.10.0-beta02
androidx.compose.runtime:runtime:1.10.0-beta02
androidx.compose.ui:ui-android:1.10.0-beta02
androidx.compose.ui:ui-geometry-android:1.10.0-beta02
androidx.compose.ui:ui-geometry:1.10.0-beta02
androidx.compose.ui:ui-graphics-android:1.10.0-beta02
androidx.compose.ui:ui-graphics:1.10.0-beta02
androidx.compose.ui:ui-text-android:1.10.0-beta02
androidx.compose.ui:ui-text:1.10.0-beta02
androidx.compose.ui:ui-tooling-preview-android:1.10.0-beta02
androidx.compose.ui:ui-tooling-preview:1.10.0-beta02
androidx.compose.ui:ui-unit-android:1.10.0-beta02
androidx.compose.ui:ui-unit:1.10.0-beta02
androidx.compose.ui:ui-util-android:1.10.0-beta02
androidx.compose.ui:ui-util:1.10.0-beta02
androidx.compose.ui:ui:1.10.0-beta02
androidx.compose:compose-bom-alpha:2025.09.01
androidx.concurrent:concurrent-futures-ktx:1.1.0
androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.16.0
@ -72,16 +74,18 @@ androidx.core:core:1.16.0
androidx.cursoradapter:cursoradapter:1.0.0
androidx.customview:customview-poolingcontainer:1.0.0
androidx.customview:customview:1.0.0
androidx.datastore:datastore-android:1.1.1
androidx.datastore:datastore-core-android:1.1.1
androidx.datastore:datastore-core-okio-jvm:1.1.1
androidx.datastore:datastore-core-okio:1.1.1
androidx.datastore:datastore-core:1.1.1
androidx.datastore:datastore-preferences-android:1.1.1
androidx.datastore:datastore-preferences-core-jvm:1.1.1
androidx.datastore:datastore-preferences-core:1.1.1
androidx.datastore:datastore-preferences:1.1.1
androidx.datastore:datastore:1.1.1
androidx.datastore:datastore-android:1.2.0
androidx.datastore:datastore-core-android:1.2.0
androidx.datastore:datastore-core-okio-jvm:1.2.0
androidx.datastore:datastore-core-okio:1.2.0
androidx.datastore:datastore-core:1.2.0
androidx.datastore:datastore-preferences-android:1.2.0
androidx.datastore:datastore-preferences-core-android:1.2.0
androidx.datastore:datastore-preferences-core:1.2.0
androidx.datastore:datastore-preferences-external-protobuf:1.2.0
androidx.datastore:datastore-preferences-proto:1.2.0
androidx.datastore:datastore-preferences:1.2.0
androidx.datastore:datastore:1.2.0
androidx.documentfile:documentfile:1.0.0
androidx.drawerlayout:drawerlayout:1.0.0
androidx.dynamicanimation:dynamicanimation:1.0.0
@ -98,40 +102,40 @@ androidx.hilt:hilt-lifecycle-viewmodel:1.3.0-alpha02
androidx.hilt:hilt-work:1.2.0
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.10.0-alpha03
androidx.lifecycle:lifecycle-common-jvm:2.10.0-alpha03
androidx.lifecycle:lifecycle-common:2.10.0-alpha03
androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0-alpha03
androidx.lifecycle:lifecycle-livedata-core:2.10.0-alpha03
androidx.lifecycle:lifecycle-livedata:2.10.0-alpha03
androidx.lifecycle:lifecycle-process:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime-compose-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime-compose:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime-ktx:2.10.0-alpha03
androidx.lifecycle:lifecycle-runtime:2.10.0-alpha03
androidx.lifecycle:lifecycle-service:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel-navigation3-android:2.10.0-SNAPSHOT
androidx.lifecycle:lifecycle-viewmodel-navigation3:2.10.0-SNAPSHOT
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0-alpha03
androidx.lifecycle:lifecycle-viewmodel:2.10.0-alpha03
androidx.lifecycle:lifecycle-common-java8:2.10.0
androidx.lifecycle:lifecycle-common-jvm:2.10.0
androidx.lifecycle:lifecycle-common:2.10.0
androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0
androidx.lifecycle:lifecycle-livedata-core:2.10.0
androidx.lifecycle:lifecycle-livedata:2.10.0
androidx.lifecycle:lifecycle-process:2.10.0
androidx.lifecycle:lifecycle-runtime-android:2.10.0
androidx.lifecycle:lifecycle-runtime-compose-android:2.10.0
androidx.lifecycle:lifecycle-runtime-compose:2.10.0
androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0
androidx.lifecycle:lifecycle-runtime-ktx:2.10.0
androidx.lifecycle:lifecycle-runtime:2.10.0
androidx.lifecycle:lifecycle-service:2.10.0
androidx.lifecycle:lifecycle-viewmodel-android:2.10.0
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.10.0
androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0
androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0
androidx.lifecycle:lifecycle-viewmodel-navigation3-android:2.10.0
androidx.lifecycle:lifecycle-viewmodel-navigation3:2.10.0
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0
androidx.lifecycle:lifecycle-viewmodel:2.10.0
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.metrics:metrics-performance:1.0.0-beta01
androidx.navigation3:navigation3-runtime-android:1.0.0-SNAPSHOT
androidx.navigation3:navigation3-runtime:1.0.0-SNAPSHOT
androidx.navigation3:navigation3-ui-android:1.0.0-SNAPSHOT
androidx.navigation3:navigation3-ui:1.0.0-SNAPSHOT
androidx.navigationevent:navigationevent-android:1.0.0-SNAPSHOT
androidx.navigationevent:navigationevent-compose-android:1.0.0-SNAPSHOT
androidx.navigationevent:navigationevent-compose:1.0.0-SNAPSHOT
androidx.navigationevent:navigationevent:1.0.0-SNAPSHOT
androidx.navigation3:navigation3-runtime-android:1.0.0
androidx.navigation3:navigation3-runtime:1.0.0
androidx.navigation3:navigation3-ui-android:1.0.0
androidx.navigation3:navigation3-ui:1.0.0
androidx.navigationevent:navigationevent-android:1.0.0
androidx.navigationevent:navigationevent-compose-android:1.0.0
androidx.navigationevent:navigationevent-compose:1.0.0
androidx.navigationevent:navigationevent:1.0.0
androidx.print:print:1.0.0
androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05
androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05
@ -142,11 +146,11 @@ androidx.room:room-common:2.8.3
androidx.room:room-ktx:2.8.3
androidx.room:room-runtime-android:2.8.3
androidx.room:room-runtime:2.8.3
androidx.savedstate:savedstate-android:1.4.0-alpha03
androidx.savedstate:savedstate-compose-android:1.4.0-alpha03
androidx.savedstate:savedstate-compose:1.4.0-alpha03
androidx.savedstate:savedstate-ktx:1.4.0-alpha03
androidx.savedstate:savedstate:1.4.0-alpha03
androidx.savedstate:savedstate-android:1.4.0
androidx.savedstate:savedstate-compose-android:1.4.0
androidx.savedstate:savedstate-compose:1.4.0
androidx.savedstate:savedstate-ktx:1.4.0
androidx.savedstate:savedstate:1.4.0
androidx.sqlite:sqlite-android:2.6.1
androidx.sqlite:sqlite-framework-android:2.6.1
androidx.sqlite:sqlite-framework:2.6.1
@ -160,9 +164,9 @@ androidx.vectordrawable:vectordrawable-animated:1.1.0
androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager:viewpager:1.0.0
androidx.window:window-core-android:1.4.0
androidx.window:window-core:1.4.0
androidx.window:window:1.4.0
androidx.window:window-core-android:1.5.0
androidx.window:window-core:1.5.0
androidx.window:window:1.5.0
androidx.work:work-runtime-ktx:2.10.0
androidx.work:work-runtime:2.10.0
com.caverock:androidsvg-aar:1.4
@ -219,8 +223,8 @@ com.google.protobuf:protobuf-javalite:4.29.2
com.google.protobuf:protobuf-kotlin-lite:4.29.2
com.squareup.okhttp3:logging-interceptor:4.12.0
com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0
com.squareup.okio:okio:3.9.0
com.squareup.okio:okio-jvm:3.9.1
com.squareup.okio:okio:3.9.1
com.squareup.retrofit2:converter-kotlinx-serialization:2.11.0
com.squareup.retrofit2:retrofit:2.11.0
io.coil-kt:coil-base:2.7.0
@ -231,8 +235,6 @@ io.coil-kt:coil:2.7.0
jakarta.inject:jakarta.inject-api:2.0.1
javax.inject:javax.inject:1
org.checkerframework:checker-qual:3.12.0
org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.22
org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.22
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0

@ -32,7 +32,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.metrics.performance.JankStats
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.EntryProviderScope
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
@ -43,6 +43,7 @@ import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackViewModel
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.ui.LocalTimeZone
import com.google.samples.apps.nowinandroid.ui.NiaApp
import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState
@ -54,6 +55,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@ -75,12 +77,16 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var userNewsResourceRepository: UserNewsResourceRepository
private val viewModel: MainActivityViewModel by viewModels()
private val backStackViewModel: NiaBackStackViewModel by viewModels()
@Inject
lateinit var entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderScope<NiaNavKey>.() -> Unit>
@Inject
lateinit var entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder<NiaNavKey>.() -> Unit>
lateinit var niaNavigator: NiaNavigator
private val viewModel: MainActivityViewModel by viewModels()
private val backStackViewModel: NiaBackStackViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
@ -144,7 +150,7 @@ class MainActivity : ComponentActivity() {
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
niaBackStack = backStackViewModel.niaBackStack,
niaNavigator = niaNavigator,
)
val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle()
@ -160,7 +166,7 @@ class MainActivity : ComponentActivity() {
) {
NiaApp(
appState,
entryProviderBuilders,
entryProviderBuilders = entryProviderBuilders
)
}
}

@ -16,25 +16,41 @@
package com.google.samples.apps.nowinandroid.di
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import androidx.navigation3.runtime.EntryProviderScope
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigatorState
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityRetainedScoped
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.modules.PolymorphicModuleBuilder
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import javax.inject.Provider
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object BackStackProvider {
object NiaNavigatorProvider {
@Provides
@Singleton
fun provideNiaBackStack(): NiaBackStack =
NiaBackStack(startKey = TopLevelDestination.FOR_YOU.key)
fun providerNiaNavigatorState(): NiaNavigatorState =
NiaNavigatorState(
startKey = TopLevelDestination.FOR_YOU.key,
)
//
// @Provides
// @Singleton
// fun provideNiaNavigator(
// state: NiaNavigatorState
// ): NiaNavigator =
// NiaNavigator(state)
/**
* Registers feature modules' polymorphic serializers to support

@ -19,36 +19,37 @@ package com.google.samples.apps.nowinandroid.navigation
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavEntryDecorator
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.ui.NavDisplay
import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.core.navigation.getEntries
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute
import kotlin.collections.forEach
import kotlin.collections.plus
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun NiaNavDisplay(
niaBackStack: NiaBackStack,
entryProviderBuilders: Set<EntryProviderBuilder<NiaNavKey>.() -> Unit>,
niaNavigator: NiaNavigator,
entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>,
) {
val listDetailStrategy = rememberListDetailSceneStrategy<NiaNavKey>()
val entries = niaNavigator.navigatorState.getEntries(entryProviderBuilders)
NavDisplay(
backStack = niaBackStack.backStack,
entries = entries,
sceneStrategy = listDetailStrategy,
onBack = { count -> niaBackStack.popLast(count) },
entryDecorators = listOf(
rememberSceneSetupNavEntryDecorator(),
rememberSavedStateNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider = entryProvider {
entryProviderBuilders.forEach { builder ->
builder()
}
},
onBack = { niaNavigator.pop() },
)
}

@ -59,7 +59,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.EntryProviderScope
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
@ -79,9 +79,9 @@ import com.google.samples.apps.nowinandroid.feature.settings.api.R as settingsR
@Composable
fun NiaApp(
appState: NiaAppState,
entryProviderBuilders: Set<EntryProviderBuilder<NiaNavKey>.() -> Unit>,
modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>,
) {
val shouldShowGradientBackground =
appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
@ -112,11 +112,11 @@ fun NiaApp(
CompositionLocalProvider(LocalSnackbarHostState provides snackbarHostState) {
NiaApp(
appState = appState,
entryProviderBuilders = entryProviderBuilders,
showSettingsDialog = showSettingsDialog,
onSettingsDismissed = { showSettingsDialog = false },
onTopAppBarActionClick = { showSettingsDialog = true },
windowAdaptiveInfo = windowAdaptiveInfo,
entryProviderBuilders = entryProviderBuilders,
)
}
}
@ -130,12 +130,12 @@ fun NiaApp(
)
internal fun NiaApp(
appState: NiaAppState,
entryProviderBuilders: Set<EntryProviderBuilder<NiaNavKey>.() -> Unit>,
showSettingsDialog: Boolean,
onSettingsDismissed: () -> Unit,
onTopAppBarActionClick: () -> Unit,
modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>,
) {
val unreadDestinations by appState.topLevelDestinationsWithUnreadResources
.collectAsStateWithLifecycle()
@ -156,7 +156,7 @@ internal fun NiaApp(
val selected = destination.key == currentTopLevelKey
item(
selected = selected,
onClick = { appState.niaBackStack.navigate(destination.key) },
onClick = { appState.niaNavigator.navigate(destination.key) },
icon = {
Icon(
imageVector = destination.unselectedIcon,
@ -227,7 +227,7 @@ internal fun NiaApp(
containerColor = Color.Transparent,
),
onActionClick = { onTopAppBarActionClick() },
onNavigationClick = { appState.niaBackStack.navigateToSearch() },
onNavigationClick = { appState.niaNavigator.navigateToSearch() },
)
}
@ -242,8 +242,8 @@ internal fun NiaApp(
),
) {
NiaNavDisplay(
niaBackStack = appState.niaBackStack,
entryProviderBuilders,
niaNavigator = appState.niaNavigator,
entryProviderBuilders = entryProviderBuilders,
)
}

@ -20,12 +20,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.BOOKMARKS
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU
@ -43,19 +41,19 @@ fun rememberNiaAppState(
networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor,
niaBackStack: NiaBackStack,
niaNavigator: NiaNavigator,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
): NiaAppState {
NavigationTrackingSideEffect(niaBackStack)
NavigationTrackingSideEffect(niaNavigator)
return remember(
niaBackStack,
niaNavigator,
coroutineScope,
networkMonitor,
userNewsResourceRepository,
timeZoneMonitor,
) {
NiaAppState(
niaBackStack = niaBackStack,
niaNavigator = niaNavigator,
coroutineScope = coroutineScope,
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
@ -66,14 +64,14 @@ fun rememberNiaAppState(
@Stable
class NiaAppState(
val niaBackStack: NiaBackStack,
val niaNavigator: NiaNavigator,
coroutineScope: CoroutineScope,
networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor,
) {
val currentTopLevelDestination: TopLevelDestination?
@Composable get() = TopLevelDestinations[niaBackStack.currentTopLevelKey]
@Composable get() = TopLevelDestinations[niaNavigator.navigatorState.currentTopLevelKey]
val isOffline = networkMonitor.isOnline
.map(Boolean::not)
@ -118,12 +116,12 @@ class NiaAppState(
* Stores information about navigation events to be used with JankStats
*/
@Composable
private fun NavigationTrackingSideEffect(niaBackStack: NiaBackStack) {
TrackDisposableJank(niaBackStack) { metricsHolder ->
snapshotFlow {
val stack = niaBackStack.backStack.toList()
metricsHolder.state?.putState("Navigation", stack.lastOrNull().toString())
}
onDispose { }
}
private fun NavigationTrackingSideEffect(niaNavigator: NiaNavigator) {
// TrackDisposableJank(niaNavigator) { metricsHolder ->
// snapshotFlow {
// val stack = niaNavigator.backStack.toList()
// metricsHolder.state?.putState("Navigation", stack.lastOrNull().toString())
// }
// onDispose { }
// }
}

@ -125,7 +125,7 @@ class NiaAppScreenSizesScreenshotTests {
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
niaBackStack = mockNiaBackStack(),
niaNavigator = mockNiaBackStack(),
)
NiaApp(
fakeAppState,

@ -68,7 +68,7 @@ class NiaAppStateTest {
composeTestRule.setContent {
state = remember(niaBackStack) {
NiaAppState(
niaBackStack = niaBackStack,
niaNavigator = niaBackStack,
coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
@ -77,16 +77,16 @@ class NiaAppStateTest {
}
}
assertEquals(ForYouRoute, state.niaBackStack.currentTopLevelKey)
assertEquals(ForYouRoute, state.niaBackStack.currentKey)
assertEquals(ForYouRoute, state.niaNavigator.currentActiveTopLevelKey)
assertEquals(ForYouRoute, state.niaNavigator.currentKey)
// Navigate to another destination once
niaBackStack.navigate(BookmarksRoute)
composeTestRule.waitForIdle()
assertEquals(BookmarksRoute, state.niaBackStack.currentTopLevelKey)
assertEquals(BookmarksRoute, state.niaBackStack.currentKey)
assertEquals(BookmarksRoute, state.niaNavigator.currentActiveTopLevelKey)
assertEquals(BookmarksRoute, state.niaNavigator.currentKey)
}
@Test
@ -96,7 +96,7 @@ class NiaAppStateTest {
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
niaBackStack = mockNiaBackStack(),
niaNavigator = mockNiaBackStack(),
)
}
@ -114,7 +114,7 @@ class NiaAppStateTest {
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
niaBackStack = mockNiaBackStack(),
niaNavigator = mockNiaBackStack(),
)
}
@ -134,7 +134,7 @@ class NiaAppStateTest {
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
niaBackStack = mockNiaBackStack(),
niaNavigator = mockNiaBackStack(),
)
}
val changedTz = TimeZone.of("Europe/Prague")

@ -250,7 +250,7 @@ class SnackbarInsetsScreenshotTests {
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
niaBackStack = mockNiaBackStack(),
niaNavigator = mockNiaBackStack(),
)
NiaApp(
appState = appState,

@ -200,7 +200,7 @@ class SnackbarScreenshotTests {
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
niaBackStack = mockNiaBackStack(),
niaNavigator = mockNiaBackStack(),
)
NiaApp(
appState = appState,

@ -16,14 +16,13 @@
package com.google.samples.apps.nowinandroid.ui
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import androidx.navigation3.runtime.EntryProviderScope
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.foryou.impl.ForYouScreen
val MockEntryProvider: Set<EntryProviderBuilder<NiaNavKey>.() -> Unit> =
val MockEntryProvider: Set<EntryProviderScope<NiaNavKey>.() -> Unit> =
setOf(
{
entry<ForYouRoute> {
@ -34,4 +33,4 @@ val MockEntryProvider: Set<EntryProviderBuilder<NiaNavKey>.() -> Unit> =
private val startKey = ForYouRoute
fun mockNiaBackStack() = NiaBackStack(startKey)
fun mockNiaBackStack() = NiaNavigator(startKey)

@ -29,6 +29,7 @@ android {
dependencies {
api(libs.androidx.navigation3.runtime)
implementation(libs.androidx.savedstate.compose)
implementation(libs.androidx.lifecycle.viewModel.navigation3)
testImplementation(libs.truth)

@ -16,6 +16,9 @@
package com.google.samples.apps.nowinandroid.core.navigation
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.createSavedStateHandle
@ -47,7 +50,7 @@ class NiaBackStackViewModelTest {
private fun createViewModel() = NiaBackStackViewModel(
savedStateHandle = SavedStateHandle(),
niaBackStack = NiaBackStack(TestStartKey),
niaNavigatorState = NiaNavigatorState(TestStartKey),
serializersModules = serializersModules,
)
@ -55,10 +58,10 @@ class NiaBackStackViewModelTest {
fun testStartKeySaved() {
rule.setContent {
val viewModel = createViewModel()
assertThat(viewModel.backStackMap).containsEntry(
TestStartKey,
mutableListOf(TestStartKey),
)
assertThat(viewModel.backStackMap.size).isEqualTo(1)
val entry = viewModel.backStackMap[TestStartKey]
assertThat(entry).isNotNull()
assertThat(entry).containsExactly(TestStartKey)
}
}
@ -66,88 +69,99 @@ class NiaBackStackViewModelTest {
fun testNonTopLevelKeySaved() {
val viewModel = createViewModel()
rule.setContent {
val backStack = viewModel.niaBackStack
backStack.navigate(TestKeyFirst)
val navigator = remember { NiaNavigator( viewModel.niaNavigatorState) }
navigator.navigate(TestKeyFirst)
}
assertThat(viewModel.backStackMap).containsEntry(
TestStartKey,
mutableListOf(TestStartKey, TestKeyFirst),
)
assertThat(viewModel.backStackMap.size).isEqualTo(1)
val entry = viewModel.backStackMap[TestStartKey]
assertThat(entry).isNotNull()
assertThat(entry).containsExactly(TestStartKey, TestKeyFirst).inOrder()
}
@Test
fun testTopLevelKeySaved() {
val viewModel = createViewModel()
rule.setContent {
val backStack = viewModel.niaBackStack
val navigator = remember { NiaNavigator( viewModel.niaNavigatorState) }
backStack.navigate(TestKeyFirst)
backStack.navigate(TestTopLevelKeyFirst)
navigator.navigate(TestKeyFirst)
navigator.navigate(TestTopLevelKeyFirst)
}
assertThat(viewModel.backStackMap).containsExactly(
TestStartKey,
mutableListOf(TestStartKey, TestKeyFirst),
TestTopLevelKeyFirst,
mutableListOf(TestTopLevelKeyFirst),
).inOrder()
assertThat(viewModel.backStackMap.size).isEqualTo(2)
val entry = viewModel.backStackMap[TestStartKey]
assertThat(entry).isNotNull()
assertThat(entry).containsExactly(TestStartKey, TestKeyFirst).inOrder()
val entry2 = viewModel.backStackMap[TestTopLevelKeyFirst]
assertThat(entry2).isNotNull()
assertThat(entry2).containsExactly(TestTopLevelKeyFirst)
}
@Test
fun testMultiStacksSaved() {
val viewModel = createViewModel()
rule.setContent {
viewModel.niaBackStack.navigate(TestKeyFirst)
viewModel.niaBackStack.navigate(TestTopLevelKeyFirst)
viewModel.niaBackStack.navigate(TestKeySecond)
val navigator = remember { NiaNavigator( viewModel.niaNavigatorState) }
navigator.navigate(TestKeyFirst)
navigator.navigate(TestTopLevelKeyFirst)
navigator.navigate(TestKeySecond)
}
assertThat(viewModel.backStackMap).containsExactly(
TestStartKey,
mutableListOf(TestStartKey, TestKeyFirst),
TestTopLevelKeyFirst,
mutableListOf(TestTopLevelKeyFirst, TestKeySecond),
).inOrder()
assertThat(viewModel.backStackMap.size).isEqualTo(2)
val entry = viewModel.backStackMap[TestStartKey]
assertThat(entry).isNotNull()
assertThat(entry).containsExactly(TestStartKey, TestKeyFirst).inOrder()
val entry2 = viewModel.backStackMap[TestTopLevelKeyFirst]
assertThat(entry2).isNotNull()
assertThat(entry2).containsExactly(TestTopLevelKeyFirst, TestKeySecond).inOrder()
}
@Test
fun testPopSaved() {
val viewModel = createViewModel()
rule.setContent {
val backStack = viewModel.niaBackStack
val navigator = remember { NiaNavigator( viewModel.niaNavigatorState) }
backStack.navigate(TestKeyFirst)
assertThat(viewModel.backStackMap).containsExactly(
TestStartKey,
mutableListOf(TestStartKey, TestKeyFirst),
)
navigator.navigate(TestKeyFirst)
backStack.popLast()
assertThat(viewModel.backStackMap).containsExactly(
TestStartKey,
mutableListOf(TestStartKey),
)
assertThat(viewModel.backStackMap.size).isEqualTo(1)
val entry = viewModel.backStackMap[TestStartKey]
assertThat(entry).isNotNull()
assertThat(entry).containsExactly(TestStartKey, TestKeyFirst).inOrder()
navigator.pop()
assertThat(viewModel.backStackMap.size).isEqualTo(1)
val entry2 = viewModel.backStackMap[TestStartKey]
assertThat(entry2).isNotNull()
assertThat(entry2).containsExactly(TestStartKey).inOrder()
}
}
@Test
fun testRestore() {
lateinit var scenario: ViewModelScenario<NiaBackStackViewModel>
lateinit var navigator: NiaNavigator
lateinit var navigatorState: NiaNavigatorState
rule.setContent {
navigatorState = remember { NiaNavigatorState(TestStartKey) }
navigator = remember { NiaNavigator(navigatorState) }
scenario = viewModelScenario {
NiaBackStackViewModel(
savedStateHandle = createSavedStateHandle(),
niaBackStack = NiaBackStack(TestStartKey),
niaNavigatorState = navigatorState,
serializersModules = serializersModules,
)
}
}
rule.runOnIdle {
scenario.viewModel.niaBackStack.navigate(TestKeyFirst)
assertThat(scenario.viewModel.niaBackStack.backStack).containsExactly(
navigator.navigate(TestKeyFirst)
assertThat(navigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
).inOrder()
@ -156,32 +170,38 @@ class NiaBackStackViewModelTest {
scenario.recreate()
rule.runOnIdle {
assertThat(scenario.viewModel.niaBackStack.backStack).containsExactly(
assertThat(navigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
).inOrder()
}
scenario.close()
}
@Test
fun testRestoreMultiStacks() {
lateinit var scenario: ViewModelScenario<NiaBackStackViewModel>
lateinit var navigator: NiaNavigator
lateinit var navigatorState: NiaNavigatorState
rule.setContent {
navigatorState = remember { NiaNavigatorState(TestStartKey) }
navigator = remember { NiaNavigator(navigatorState) }
scenario = viewModelScenario {
NiaBackStackViewModel(
savedStateHandle = createSavedStateHandle(),
niaBackStack = NiaBackStack(TestStartKey),
niaNavigatorState = navigatorState,
serializersModules = serializersModules,
)
}
}
rule.runOnIdle {
scenario.viewModel.niaBackStack.navigate(TestKeyFirst)
scenario.viewModel.niaBackStack.navigate(TestTopLevelKeyFirst)
scenario.viewModel.niaBackStack.navigate(TestKeySecond)
navigator.navigate(TestKeyFirst)
navigator.navigate(TestTopLevelKeyFirst)
navigator.navigate(TestKeySecond)
assertThat(scenario.viewModel.niaBackStack.backStack).containsExactly(
assertThat(navigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
TestTopLevelKeyFirst,
@ -192,13 +212,15 @@ class NiaBackStackViewModelTest {
scenario.recreate()
rule.runOnIdle {
assertThat(scenario.viewModel.niaBackStack.backStack).containsExactly(
assertThat(navigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
TestTopLevelKeyFirst,
TestKeySecond,
).inOrder()
}
scenario.close()
}
}

@ -1,127 +0,0 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.navigation
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import org.jetbrains.annotations.VisibleForTesting
import kotlin.collections.mutableListOf
// TODO refine back behavior - perhaps take a lambda so that each screen / use site can customize back behavior?
// https://github.com/android/nowinandroid/issues/1934
class NiaBackStack(
private val startKey: NiaNavKey,
) {
internal var backStackMap: LinkedHashMap<NiaNavKey, MutableList<NiaNavKey>> =
linkedMapOf(
startKey to mutableListOf(startKey),
)
@VisibleForTesting
val backStack: SnapshotStateList<NiaNavKey> = mutableStateListOf(startKey)
var currentTopLevelKey: NiaNavKey by mutableStateOf(backStackMap.keys.last())
private set
@get:VisibleForTesting
val currentKey: NiaNavKey
get() = backStackMap[currentTopLevelKey]!!.last()
fun navigate(key: NiaNavKey) {
when {
// top level singleTop -> clear substack
key == currentTopLevelKey -> backStackMap[key] = mutableListOf(key)
// top level non-singleTop
key.isTopLevel -> {
// if navigating back to start destination, pop all other top destinations and
// store start destination substack
if (key == startKey) {
val tempStack = mapOf(startKey to backStackMap[startKey]!!)
backStackMap.clear()
backStackMap.putAll(tempStack)
// else either restore an existing substack or initiate new one
} else {
backStackMap[key] = backStackMap.remove(key) ?: mutableListOf(key)
}
}
// not top level - add to current substack
else -> {
val currentStack = backStackMap.values.last()
// single top
if (currentStack.lastOrNull() == key) {
currentStack.removeLastOrNull()
}
currentStack.add(key)
}
}
updateBackStack()
}
fun popLast(count: Int = 1) {
var popCount = count
var currentEntry = backStackMap.entries.last()
while (popCount > 0) {
val currentStack = currentEntry.value
if (currentStack.size == 1) {
// if current sub-stack only has one key, remove the sub-stack from the map
backStackMap.remove(currentEntry.key)
when {
// throw if map is empty after pop
backStackMap.isEmpty() -> error(popErrorMessage(count, currentEntry.key))
// otherwise update currentEntry
else -> currentEntry = backStackMap.entries.last()
}
} else {
// if current sub-stack has more than one key, just pop the last key off the sub-stack
currentStack.removeLastOrNull()
}
popCount--
}
updateBackStack()
}
private fun updateBackStack() {
backStack.apply {
clear()
backStack.addAll(
backStackMap.flatMap { it.value },
)
}
currentTopLevelKey = backStackMap.keys.last()
}
internal fun restore(map: LinkedHashMap<NiaNavKey, MutableList<NiaNavKey>>?) {
map ?: return
backStackMap.clear()
backStackMap.putAll(map)
updateBackStack()
}
}
interface NiaNavKey {
val isTopLevel: Boolean
}
private fun popErrorMessage(count: Int, lastPopped: NiaNavKey) =
"""
Failed to pop $count entries. BackStack has been popped to an empty stack. Last
popped key is $lastPopped.
""".trimIndent()

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.core.navigation
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.serialization.saved
@ -26,6 +27,7 @@ import androidx.savedstate.serialization.SavedStateConfiguration
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer
@ -34,7 +36,7 @@ import javax.inject.Inject
@HiltViewModel
class NiaBackStackViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
val niaBackStack: NiaBackStack,
val niaNavigatorState: NiaNavigatorState,
serializersModules: SerializersModule,
) : ViewModel() {
@ -42,29 +44,39 @@ class NiaBackStackViewModel @Inject constructor(
@VisibleForTesting
internal var backStackMap by savedStateHandle.saved(
serializer = getMapSerializer<NiaNavKey>(),
serializer = MapSerializer(
serializer<NiaNavKey>(),
serializer<List<NiaNavKey>>()
),
configuration = config,
) {
linkedMapOf()
}
@VisibleForTesting
internal var activeTopLeveLKeys by savedStateHandle.saved(
serializer = ListSerializer(serializer<NiaNavKey>()),
configuration = config,
) {
listOf()
}
init {
if (backStackMap.isNotEmpty()) {
// Restore backstack from saved state handle if not emtpy
// Restore backstack from saved state handle if not emtpy
@Suppress("UNCHECKED_CAST")
niaBackStack.restore(
backStackMap as LinkedHashMap<NiaNavKey, MutableList<NiaNavKey>>,
niaNavigatorState.restore(
activeTopLeveLKeys,
backStackMap as LinkedHashMap<NiaNavKey, SnapshotStateList<NiaNavKey>>,
)
}
// Start observing changes to the backStack and save backStack whenever it updates
viewModelScope.launch {
snapshotFlow {
niaBackStack.backStack.toList()
backStackMap = niaBackStack.backStackMap
activeTopLeveLKeys = niaNavigatorState.activeTopLeveLKeys.toList()
backStackMap = niaNavigatorState.backStacks
}.collect()
}
}
}
private inline fun <reified T : NiaNavKey> getMapSerializer() = MapSerializer(serializer<T>(), serializer<List<T>>())
}

@ -0,0 +1,164 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import org.jetbrains.annotations.VisibleForTesting
import javax.inject.Inject
import kotlin.collections.plus
class NiaNavigatorState(
internal val startKey: NiaNavKey,
) {
internal var backStacks: MutableMap<NiaNavKey, SnapshotStateList<NiaNavKey>> =
linkedMapOf(
startKey to mutableStateListOf(startKey),
)
val activeTopLeveLKeys: SnapshotStateList<NiaNavKey> = mutableStateListOf(startKey)
var currentTopLevelKey: NiaNavKey by mutableStateOf(activeTopLeveLKeys.last())
private set
@get:VisibleForTesting
val currentBackStack: List<NiaNavKey>
get() = activeTopLeveLKeys.fold(mutableListOf()) { list, topLevelKey ->
list.apply {
addAll(backStacks[topLevelKey]!!)
}
}
@get:VisibleForTesting
val currentKey: NiaNavKey
get() = backStacks[currentTopLevelKey]!!.last()
internal fun updateActiveTopLevelKeys(activeKeys: List<NiaNavKey>) {
check(activeKeys.isNotEmpty()) { "List of active top-level keys should not be empty" }
activeTopLeveLKeys.clear()
activeTopLeveLKeys.addAll(activeKeys)
currentTopLevelKey = activeTopLeveLKeys.last()
}
internal fun restore(activeKeys: List<NiaNavKey>, map: LinkedHashMap<NiaNavKey, SnapshotStateList<NiaNavKey>>?) {
map ?: return
backStacks.clear()
map.forEach { entry ->
backStacks[entry.key] = entry.value.toMutableStateList()
}
updateActiveTopLevelKeys(activeKeys)
}
}
// https://github.com/android/nowinandroid/issues/1934
class NiaNavigator @Inject constructor(
val navigatorState: NiaNavigatorState
) {
fun navigate(key: NiaNavKey) {
val currentActiveSubStacks = linkedSetOf<NiaNavKey>()
navigatorState.apply {
currentActiveSubStacks.addAll(activeTopLeveLKeys)
when {
// top level singleTop -> clear substack
key == currentTopLevelKey -> {
backStacks[key] = mutableStateListOf(key)
// no change to currentActiveTabs
}
// top level non-singleTop
key.isTopLevel -> {
// if navigating back to start destination, then only show the starting substack
if (key == startKey) {
currentActiveSubStacks.clear()
currentActiveSubStacks.add(key)
} else {
// else either restore an existing substack or initiate new one
backStacks[key] = backStacks.remove(key) ?: mutableStateListOf(key)
// move this top level key to the top of active substacks
currentActiveSubStacks.remove(key)
currentActiveSubStacks.add(key)
}
}
// not top level - add to current substack
else -> {
val currentStack = backStacks[currentTopLevelKey]!!
// single top
if (currentStack.lastOrNull() == key) {
currentStack.removeLastOrNull()
}
currentStack.add(key)
// no change to currentActiveTabs
}
}
updateActiveTopLevelKeys(currentActiveSubStacks.toList())
}
}
fun pop() {
navigatorState.apply {
val currentSubstack = backStacks[currentTopLevelKey]!!
if (currentSubstack.size == 1) {
// if current sub-stack only has one key, remove the sub-stack from the map
currentSubstack.removeLastOrNull()
backStacks.remove(currentTopLevelKey)
updateActiveTopLevelKeys(activeTopLeveLKeys.dropLast(1))
} else {
currentSubstack.removeLastOrNull()
}
}
}
}
interface NiaNavKey {
val isTopLevel: Boolean
}
@Composable
public fun NiaNavigatorState.getEntries(
entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>,
): List<NavEntry<NiaNavKey>> =
activeTopLeveLKeys.fold(emptyList()) { entries, topLevelKey ->
val decorated = key(topLevelKey) {
val decorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator<NiaNavKey>()
)
rememberDecoratedNavEntries(
backStack = backStacks[topLevelKey]!!,
entryDecorators = decorators,
entryProvider = entryProvider {
entryProviderBuilders.forEach { builder ->
builder()
}
},
)
}
entries + decorated
}

@ -1,280 +0,0 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.navigation
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import kotlin.test.assertFailsWith
class NiaBackStackTest {
private lateinit var niaBackStack: NiaBackStack
@Before
fun setup() {
niaBackStack = NiaBackStack(TestStartKey)
}
@Test
fun testStartKey() {
assertThat(niaBackStack.currentKey).isEqualTo(TestStartKey)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun testNavigate() {
niaBackStack.navigate(TestKeyFirst)
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun testNavigateTopLevel() {
niaBackStack.navigate(TestTopLevelKey)
assertThat(niaBackStack.currentKey).isEqualTo(TestTopLevelKey)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
}
@Test
fun testNavigateSingleTop() {
niaBackStack.navigate(TestKeyFirst)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestKeyFirst,
).inOrder()
niaBackStack.navigate(TestKeyFirst)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestKeyFirst,
).inOrder()
}
@Test
fun testNavigateTopLevelSingleTop() {
niaBackStack.navigate(TestTopLevelKey)
niaBackStack.navigate(TestKeyFirst)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestTopLevelKey,
TestKeyFirst,
).inOrder()
niaBackStack.navigate(TestTopLevelKey)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestTopLevelKey,
).inOrder()
}
@Test
fun testSubStack() {
niaBackStack.navigate(TestKeyFirst)
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
niaBackStack.navigate(TestKeySecond)
assertThat(niaBackStack.currentKey).isEqualTo(TestKeySecond)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun testMultiStack() {
// add to start stack
niaBackStack.navigate(TestKeyFirst)
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
// navigate to new top level
niaBackStack.navigate(TestTopLevelKey)
assertThat(niaBackStack.currentKey).isEqualTo(TestTopLevelKey)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
// add to new stack
niaBackStack.navigate(TestKeySecond)
assertThat(niaBackStack.currentKey).isEqualTo(TestKeySecond)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
// go back to start stack
niaBackStack.navigate(TestStartKey)
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun testRestore() {
assertThat(niaBackStack.backStack).containsExactly(TestStartKey)
niaBackStack.restore(
linkedMapOf(
TestStartKey to mutableListOf(TestStartKey, TestKeyFirst),
TestTopLevelKey to mutableListOf(TestTopLevelKey, TestKeySecond),
),
)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestKeyFirst,
TestTopLevelKey,
TestKeySecond,
).inOrder()
assertThat(niaBackStack.currentKey).isEqualTo(TestKeySecond)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
}
@Test
fun testPopOneNonTopLevel() {
niaBackStack.navigate(TestKeyFirst)
niaBackStack.navigate(TestKeySecond)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestKeyFirst,
TestKeySecond,
).inOrder()
niaBackStack.popLast()
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestKeyFirst,
).inOrder()
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun testPopOneTopLevel() {
niaBackStack.navigate(TestKeyFirst)
niaBackStack.navigate(TestTopLevelKey)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestKeyFirst,
TestTopLevelKey,
).inOrder()
assertThat(niaBackStack.currentKey).isEqualTo(TestTopLevelKey)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
// remove TopLevel
niaBackStack.popLast()
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestKeyFirst,
).inOrder()
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun popMultipleNonTopLevel() {
niaBackStack.navigate(TestKeyFirst)
niaBackStack.navigate(TestKeySecond)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestKeyFirst,
TestKeySecond,
).inOrder()
niaBackStack.popLast(2)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
).inOrder()
assertThat(niaBackStack.currentKey).isEqualTo(TestStartKey)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun popMultipleTopLevel() {
val testTopLevelKeyTwo = object : NiaNavKey {
override val isTopLevel: Boolean
get() = true
}
// second sub-stack
niaBackStack.navigate(TestTopLevelKey)
niaBackStack.navigate(TestKeyFirst)
// third sub-stack
niaBackStack.navigate(testTopLevelKeyTwo)
niaBackStack.navigate(TestKeySecond)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
TestTopLevelKey,
TestKeyFirst,
testTopLevelKeyTwo,
TestKeySecond,
).inOrder()
niaBackStack.popLast(4)
assertThat(niaBackStack.backStack).containsExactly(
TestStartKey,
).inOrder()
assertThat(niaBackStack.currentKey).isEqualTo(TestStartKey)
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun throwOnEmptyBackStack() {
assertFailsWith<IllegalStateException> {
niaBackStack.popLast(1)
}
}
}
private object TestStartKey : NiaNavKey {
override val isTopLevel: Boolean
get() = true
}
private object TestTopLevelKey : NiaNavKey {
override val isTopLevel: Boolean
get() = true
}
private object TestKeyFirst : NiaNavKey {
override val isTopLevel: Boolean
get() = false
}
private object TestKeySecond : NiaNavKey {
override val isTopLevel: Boolean
get() = false
}

@ -0,0 +1,287 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.navigation
import androidx.compose.runtime.mutableStateListOf
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import kotlin.test.assertFailsWith
class NiaNavigatorStateTest {
private lateinit var niaNavigatorState: NiaNavigatorState
private lateinit var niaNavigator: NiaNavigator
@Before
fun setup() {
niaNavigatorState = NiaNavigatorState(TestStartKey)
niaNavigator = NiaNavigator(niaNavigatorState)
}
@Test
fun testStartKey() {
assertThat(niaNavigatorState.currentKey).isEqualTo(TestStartKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun testNavigate() {
niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun testNavigateTopLevel() {
niaNavigator.navigate(TestTopLevelKey)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestTopLevelKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
}
@Test
fun testNavigateSingleTop() {
niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
).inOrder()
niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
).inOrder()
}
@Test
fun testNavigateTopLevelSingleTop() {
niaNavigator.navigate(TestTopLevelKey)
niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestTopLevelKey,
TestKeyFirst,
).inOrder()
niaNavigator.navigate(TestTopLevelKey)
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestTopLevelKey,
).inOrder()
}
@Test
fun testSubStack() {
niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
niaNavigator.navigate(TestKeySecond)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeySecond)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun testMultiStack() {
// add to start stack
niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
// navigate to new top level
niaNavigator.navigate(TestTopLevelKey)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestTopLevelKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
// add to new stack
niaNavigator.navigate(TestKeySecond)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeySecond)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
// go back to start stack
niaNavigator.navigate(TestStartKey)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun testRestore() {
assertThat(niaNavigatorState.currentBackStack).containsExactly(TestStartKey)
niaNavigatorState.restore(
listOf(TestStartKey, TestTopLevelKey),
linkedMapOf(
TestStartKey to mutableStateListOf(TestStartKey, TestKeyFirst),
TestTopLevelKey to mutableStateListOf(TestTopLevelKey, TestKeySecond),
),
)
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
TestTopLevelKey,
TestKeySecond,
).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeySecond)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
}
@Test
fun testPopOneNonTopLevel() {
niaNavigator.navigate(TestKeyFirst)
niaNavigator.navigate(TestKeySecond)
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
TestKeySecond,
).inOrder()
niaNavigator.pop()
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun testPopOneTopLevel() {
niaNavigator.navigate(TestKeyFirst)
niaNavigator.navigate(TestTopLevelKey)
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
TestTopLevelKey,
).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestTopLevelKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
// remove TopLevel
niaNavigator.pop()
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun popMultipleNonTopLevel() {
niaNavigator.navigate(TestKeyFirst)
niaNavigator.navigate(TestKeySecond)
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestKeyFirst,
TestKeySecond,
).inOrder()
niaNavigator.pop()
niaNavigator.pop()
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestStartKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun popMultipleTopLevel() {
val testTopLevelKeyTwo = object : NiaNavKey {
override val isTopLevel: Boolean
get() = true
}
// second sub-stack
niaNavigator.navigate(TestTopLevelKey)
niaNavigator.navigate(TestKeyFirst)
// third sub-stack
niaNavigator.navigate(testTopLevelKeyTwo)
niaNavigator.navigate(TestKeySecond)
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
TestTopLevelKey,
TestKeyFirst,
testTopLevelKeyTwo,
TestKeySecond,
).inOrder()
repeat(4) {
niaNavigator.pop()
}
assertThat(niaNavigatorState.currentBackStack).containsExactly(
TestStartKey,
).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestStartKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey)
}
@Test
fun throwOnEmptyBackStack() {
assertFailsWith<IllegalStateException> {
niaNavigator.pop()
}
}
}
private object TestStartKey : NiaNavKey {
override val isTopLevel: Boolean
get() = true
}
private object TestTopLevelKey : NiaNavKey {
override val isTopLevel: Boolean
get() = true
}
private object TestKeyFirst : NiaNavKey {
override val isTopLevel: Boolean
get() = false
}
private object TestKeySecond : NiaNavKey {
override val isTopLevel: Boolean
get() = false
}

@ -20,9 +20,8 @@ import androidx.compose.material3.SnackbarDuration.Short
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult.ActionPerformed
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import androidx.navigation3.runtime.EntryProviderScope
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.BookmarksScreen
@ -40,12 +39,12 @@ object BookmarksEntryProvider {
@Provides
@IntoSet
fun provideBookmarksEntryProviderBuilder(
backStack: NiaBackStack,
): EntryProviderBuilder<NiaNavKey>.() -> Unit = {
navigator: NiaNavigator,
): EntryProviderScope<NiaNavKey>.() -> Unit = {
entry<BookmarksRoute> {
val snackbarHostState = LocalSnackbarHostState.current
BookmarksScreen(
onTopicClick = backStack::navigateToTopic,
onTopicClick = navigator::navigateToTopic,
onShowSnackbar = { message, action ->
snackbarHostState.showSnackbar(
message = message,

@ -16,9 +16,8 @@
package com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import androidx.navigation3.runtime.EntryProviderScope
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.foryou.impl.ForYouScreen
@ -39,11 +38,11 @@ object ForYouEntryProvider {
@Provides
@IntoSet
fun provideForYouEntryProviderBuilder(
backStack: NiaBackStack,
): EntryProviderBuilder<NiaNavKey>.() -> Unit = {
navigator: NiaNavigator,
): EntryProviderScope<NiaNavKey>.() -> Unit = {
entry<ForYouRoute> {
ForYouScreen(
onTopicClick = backStack::navigateToTopic,
onTopicClick = navigator::navigateToTopic,
)
}
}

@ -19,9 +19,8 @@ package com.google.samples.apps.nowinandroid.feature.interests.impl.navigation
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import androidx.navigation3.runtime.EntryProviderScope
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsDetailPlaceholder
@ -42,8 +41,8 @@ object InterestsEntryProvider {
@Provides
@IntoSet
fun provideInterestsEntryProviderBuilder(
backStack: NiaBackStack,
): EntryProviderBuilder<NiaNavKey>.() -> Unit = {
navigator: NiaNavigator,
): EntryProviderScope<NiaNavKey>.() -> Unit = {
entry<InterestsRoute>(
metadata = ListDetailSceneStrategy.listPane {
InterestsDetailPlaceholder()
@ -53,7 +52,7 @@ object InterestsEntryProvider {
it.create(key)
}
InterestsScreen(
onTopicClick = backStack::navigateToTopic,
onTopicClick = navigator::navigateToTopic,
shouldHighlightSelectedTopic = false,
viewModel = viewModel,
)

@ -30,13 +30,14 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import androidx.test.espresso.Espresso
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackViewModel
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.feature.interests.api.R
@ -49,6 +50,7 @@ import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
@ -87,7 +89,7 @@ class InterestsListDetailScreenTest {
@EntryPoint
@InstallIn(ActivityComponent::class)
interface EntryProvidersEntryPoint {
fun getEntryProviders(): Set<@JvmSuppressWildcards EntryProviderBuilder<NiaNavKey>.() -> Unit>
fun getEntryProviders(): Set<@JvmSuppressWildcards EntryProviderScope<NiaNavKey>.() -> Unit>
}
@Inject
@ -104,7 +106,7 @@ class InterestsListDetailScreenTest {
private val Topic.testTag
get() = "topic:${this.id}"
private lateinit var entryProviderBuilders: Set<EntryProviderBuilder<NiaNavKey>.() -> Unit>
private lateinit var entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>
@Before
fun setup() {
@ -162,7 +164,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply {
setContent {
val backStackViewModel by composeTestRule.activity.viewModels<NiaBackStackViewModel>()
val backStack = backStackViewModel.niaBackStack.backStack
val backStack = backStackViewModel.niaNavigator.backStack
NiaTheme {
NavDisplay(
backStack = backStack,
@ -189,7 +191,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply {
setContent {
val backStackViewModel by composeTestRule.activity.viewModels<NiaBackStackViewModel>()
val backStack = backStackViewModel.niaBackStack.backStack
val backStack = backStackViewModel.niaNavigator.backStack
NiaTheme {
NavDisplay(
backStack = backStack,
@ -216,7 +218,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply {
setContent {
val backStackViewModel by composeTestRule.activity.viewModels<NiaBackStackViewModel>()
val backStack = backStackViewModel.niaBackStack.backStack
val backStack = backStackViewModel.niaNavigator.backStack
NiaTheme {
NavDisplay(
backStack = backStack,
@ -251,8 +253,8 @@ private fun AndroidComposeTestRule<*, *>.stringResource(
object BackStackProvider {
@Provides
@Singleton
fun provideNiaBackStack(): NiaBackStack =
NiaBackStack(startKey = InterestsRoute())
fun provideNiaBackStack(): NiaNavigator =
NiaNavigator(startKey = InterestsRoute())
@Provides
@Singleton

@ -16,7 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.search.api.navigation
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import kotlinx.serialization.Serializable
@ -26,6 +26,6 @@ object SearchRoute : NiaNavKey {
get() = false
}
fun NiaBackStack.navigateToSearch() {
fun NiaNavigator.navigateToSearch() {
navigate(SearchRoute)
}

@ -16,9 +16,8 @@
package com.google.samples.apps.nowinandroid.feature.search.impl.navigation
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import androidx.navigation3.runtime.EntryProviderScope
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchRoute
@ -37,13 +36,13 @@ object SearchEntryProvider {
@Provides
@IntoSet
fun provideSearchEntryProviderBuilder(
backStack: NiaBackStack,
): EntryProviderBuilder<NiaNavKey>.() -> Unit = {
navigator: NiaNavigator,
): EntryProviderScope<NiaNavKey>.() -> Unit = {
entry<SearchRoute> { key ->
SearchScreen(
onBackClick = backStack::popLast,
onInterestsClick = { backStack.navigate(InterestsRoute()) },
onTopicClick = backStack::navigateToTopic,
onBackClick = navigator::pop,
onInterestsClick = { navigator.navigate(InterestsRoute()) },
onTopicClick = navigator::navigateToTopic,
)
}
}

@ -16,7 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.topic.api.navigation
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import kotlinx.serialization.Serializable
@ -26,7 +26,7 @@ data class TopicRoute(val id: String) : NiaNavKey {
get() = false
}
fun NiaBackStack.navigateToTopic(
fun NiaNavigator.navigateToTopic(
topicId: String,
) {
navigate(TopicRoute(topicId))

@ -19,9 +19,8 @@ package com.google.samples.apps.nowinandroid.feature.topic.impl.navigation
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
import androidx.navigation3.runtime.EntryProviderScope
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicRoute
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic
@ -42,16 +41,16 @@ object TopicEntryProvider {
@Provides
@IntoSet
fun provideTopicEntryProviderBuilder(
backStack: NiaBackStack,
): EntryProviderBuilder<NiaNavKey>.() -> Unit = {
navigator: NiaNavigator,
): EntryProviderScope<NiaNavKey>.() -> Unit = {
entry<TopicRoute>(
metadata = ListDetailSceneStrategy.detailPane(),
) { key ->
val id = key.id
TopicScreen(
showBackButton = true,
onBackClick = backStack::popLast,
onTopicClick = backStack::navigateToTopic,
onBackClick = navigator::pop,
onTopicClick = navigator::navigateToTopic,
viewModel = hiltViewModel<TopicViewModel, Factory>(
key = id,
) { factory ->

@ -10,20 +10,20 @@ androidxBrowser = "1.8.0"
androidxComposeBom = "2025.09.01"
androidxComposeFoundation = "1.8.0-alpha07"
androidxComposeMaterial3Adaptive = "1.1.0-rc01"
androidxComposeMaterial3AdaptiveNavigation3 = "1.0.0-SNAPSHOT"
androidxComposeMaterial3AdaptiveNavigation3 = "1.3.0-alpha04"
androidxComposeRuntimeTracing = "1.7.6"
androidxCore = "1.15.0"
androidxCoreSplashscreen = "1.0.1"
androidxDataStore = "1.1.1"
androidxDataStore = "1.2.0"
androidxEspresso = "3.6.1"
androidxHiltLifecycleViewModelCompose = "1.3.0-alpha02"
androidxLifecycle = "2.8.7"
androidxLintGradle = "1.0.0-alpha03"
androidxLifecycleViewModelNavigation3 = "2.10.0-alpha05"
androidxLifecycleViewModelNavigation3 = "2.10.0"
androidxMacroBenchmark = "1.4.1"
androidxMetrics = "1.0.0-beta01"
androidxNavigation = "2.8.5"
androidxNavigation3 = "1.0.0-SNAPSHOT"
androidxNavigation3 = "1.0.0"
androidxProfileinstaller = "1.4.1"
androidxSavedStateCompose = "1.3.1"
androidxTestCore = "1.7.0-rc01"

@ -41,7 +41,7 @@ dependencyResolutionManagement {
}
mavenCentral()
maven {
url = uri("https://androidx.dev/snapshots/builds/13898898/artifacts/repository")
url = uri("https://androidx.dev/snapshots/builds/14161874/artifacts/repository")
}
}
}

Loading…
Cancel
Save