diff --git a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt index 5606bcd1d..ea1e0801c 100644 --- a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt +++ b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt @@ -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 diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt index 797e9e2ce..7bea0cb3e 100644 --- a/app/dependencies/prodReleaseRuntimeClasspath.txt +++ b/app/dependencies/prodReleaseRuntimeClasspath.txt @@ -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 diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt index 71b50e799..e717a9b39 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt @@ -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.() -> Unit> @Inject - lateinit var entryProviderBuilders: Set<@JvmSuppressWildcards EntryProviderBuilder.() -> 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 ) } } diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/BackStackProvider.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/NiaNavigatorProvider.kt similarity index 72% rename from app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/BackStackProvider.kt rename to app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/NiaNavigatorProvider.kt index 998d60ce9..59ae9c398 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/BackStackProvider.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/NiaNavigatorProvider.kt @@ -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 diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavDisplay.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavDisplay.kt index da4461a84..ecb78ac9c 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavDisplay.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavDisplay.kt @@ -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.() -> Unit>, + niaNavigator: NiaNavigator, + entryProviderBuilders: Set.() -> Unit>, ) { val listDetailStrategy = rememberListDetailSceneStrategy() - + 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() }, ) } diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index c15797879..7949496d4 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -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.() -> Unit>, modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), + entryProviderBuilders: Set.() -> 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.() -> Unit>, showSettingsDialog: Boolean, onSettingsDismissed: () -> Unit, onTopAppBarActionClick: () -> Unit, modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), + entryProviderBuilders: Set.() -> 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, ) } diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt index 7e3d8eb68..ec4413304 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt @@ -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 { } +// } } diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt index e463fb439..44070dfc4 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt @@ -125,7 +125,7 @@ class NiaAppScreenSizesScreenshotTests { networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, - niaBackStack = mockNiaBackStack(), + niaNavigator = mockNiaBackStack(), ) NiaApp( fakeAppState, diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index 2c67c1dc1..90880d5f2 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -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") diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt index b20a8e5a6..29a470315 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt @@ -250,7 +250,7 @@ class SnackbarInsetsScreenshotTests { networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, - niaBackStack = mockNiaBackStack(), + niaNavigator = mockNiaBackStack(), ) NiaApp( appState = appState, diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt index bc538d494..7e89184f4 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt @@ -200,7 +200,7 @@ class SnackbarScreenshotTests { networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, - niaBackStack = mockNiaBackStack(), + niaNavigator = mockNiaBackStack(), ) NiaApp( appState = appState, diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/TestUtil.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/TestUtil.kt index a92b5a387..d0be3c5b0 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/TestUtil.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/TestUtil.kt @@ -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.() -> Unit> = +val MockEntryProvider: Set.() -> Unit> = setOf( { entry { @@ -34,4 +33,4 @@ val MockEntryProvider: Set.() -> Unit> = private val startKey = ForYouRoute -fun mockNiaBackStack() = NiaBackStack(startKey) +fun mockNiaBackStack() = NiaNavigator(startKey) diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index abc59d239..3e7831dca 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -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) diff --git a/core/navigation/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModelTest.kt b/core/navigation/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModelTest.kt index 7007d1abf..d91065138 100644 --- a/core/navigation/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModelTest.kt +++ b/core/navigation/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModelTest.kt @@ -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 + 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 + 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() } } diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStack.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStack.kt deleted file mode 100644 index 82a7bb1c9..000000000 --- a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStack.kt +++ /dev/null @@ -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> = - linkedMapOf( - startKey to mutableListOf(startKey), - ) - - @VisibleForTesting - val backStack: SnapshotStateList = 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>?) { - 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() diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModel.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModel.kt index 097b79967..c442919d4 100644 --- a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModel.kt +++ b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackViewModel.kt @@ -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(), + serializer = MapSerializer( + serializer(), + serializer>() + ), configuration = config, ) { linkedMapOf() } + @VisibleForTesting + internal var activeTopLeveLKeys by savedStateHandle.saved( + serializer = ListSerializer(serializer()), + 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>, + niaNavigatorState.restore( + activeTopLeveLKeys, + backStackMap as LinkedHashMap>, ) } // 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 getMapSerializer() = MapSerializer(serializer(), serializer>()) +} \ No newline at end of file diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigator.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigator.kt new file mode 100644 index 000000000..20366590a --- /dev/null +++ b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigator.kt @@ -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> = + linkedMapOf( + startKey to mutableStateListOf(startKey), + ) + + val activeTopLeveLKeys: SnapshotStateList = mutableStateListOf(startKey) + + var currentTopLevelKey: NiaNavKey by mutableStateOf(activeTopLeveLKeys.last()) + private set + + @get:VisibleForTesting + val currentBackStack: List + 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) { + 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, map: LinkedHashMap>?) { + 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() + 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.() -> Unit>, +): List> = + activeTopLeveLKeys.fold(emptyList()) { entries, topLevelKey -> + val decorated = key(topLevelKey) { + val decorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator() + ) + rememberDecoratedNavEntries( + backStack = backStacks[topLevelKey]!!, + entryDecorators = decorators, + entryProvider = entryProvider { + entryProviderBuilders.forEach { builder -> + builder() + } + }, + ) + } + entries + decorated +} \ No newline at end of file diff --git a/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackTest.kt b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackTest.kt deleted file mode 100644 index 2a87024f9..000000000 --- a/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaBackStackTest.kt +++ /dev/null @@ -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 { - 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 -} diff --git a/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigatorStateTest.kt b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigatorStateTest.kt new file mode 100644 index 000000000..eeef1b803 --- /dev/null +++ b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigatorStateTest.kt @@ -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 { + 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 +} diff --git a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt index cff561bf4..e1bee5e88 100644 --- a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt @@ -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.() -> Unit = { + navigator: NiaNavigator, + ): EntryProviderScope.() -> Unit = { entry { val snackbarHostState = LocalSnackbarHostState.current BookmarksScreen( - onTopicClick = backStack::navigateToTopic, + onTopicClick = navigator::navigateToTopic, onShowSnackbar = { message, action -> snackbarHostState.showSnackbar( message = message, diff --git a/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt index 8259afa31..81fe584d2 100644 --- a/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt @@ -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.() -> Unit = { + navigator: NiaNavigator, + ): EntryProviderScope.() -> Unit = { entry { ForYouScreen( - onTopicClick = backStack::navigateToTopic, + onTopicClick = navigator::navigateToTopic, ) } } diff --git a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt index 68c0a5981..7f759c653 100644 --- a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt @@ -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.() -> Unit = { + navigator: NiaNavigator, + ): EntryProviderScope.() -> Unit = { entry( metadata = ListDetailSceneStrategy.listPane { InterestsDetailPlaceholder() @@ -53,7 +52,7 @@ object InterestsEntryProvider { it.create(key) } InterestsScreen( - onTopicClick = backStack::navigateToTopic, + onTopicClick = navigator::navigateToTopic, shouldHighlightSelectedTopic = false, viewModel = viewModel, ) diff --git a/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsListDetailScreenTest.kt b/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsListDetailScreenTest.kt index 81ec9c549..cd21bf058 100644 --- a/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsListDetailScreenTest.kt +++ b/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsListDetailScreenTest.kt @@ -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.() -> Unit> + fun getEntryProviders(): Set<@JvmSuppressWildcards EntryProviderScope.() -> Unit> } @Inject @@ -104,7 +106,7 @@ class InterestsListDetailScreenTest { private val Topic.testTag get() = "topic:${this.id}" - private lateinit var entryProviderBuilders: Set.() -> Unit> + private lateinit var entryProviderBuilders: Set.() -> Unit> @Before fun setup() { @@ -162,7 +164,7 @@ class InterestsListDetailScreenTest { composeTestRule.apply { setContent { val backStackViewModel by composeTestRule.activity.viewModels() - 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() - 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() - 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 diff --git a/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchRoute.kt b/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchRoute.kt index e45c177b3..fbd80b040 100644 --- a/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchRoute.kt +++ b/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchRoute.kt @@ -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) } diff --git a/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt index 272e4c7ba..dbb3f0fe5 100644 --- a/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt @@ -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.() -> Unit = { + navigator: NiaNavigator, + ): EntryProviderScope.() -> Unit = { entry { key -> SearchScreen( - onBackClick = backStack::popLast, - onInterestsClick = { backStack.navigate(InterestsRoute()) }, - onTopicClick = backStack::navigateToTopic, + onBackClick = navigator::pop, + onInterestsClick = { navigator.navigate(InterestsRoute()) }, + onTopicClick = navigator::navigateToTopic, ) } } diff --git a/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicRoute.kt b/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicRoute.kt index 396887d09..1f226099f 100644 --- a/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicRoute.kt +++ b/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicRoute.kt @@ -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)) diff --git a/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt index 4584fe93d..92f871104 100644 --- a/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt +++ b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt @@ -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.() -> Unit = { + navigator: NiaNavigator, + ): EntryProviderScope.() -> Unit = { entry( 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( key = id, ) { factory -> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef01c4fa1..11a8d7e79 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/settings.gradle.kts b/settings.gradle.kts index f264a7b6f..fafb2b696 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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") } } }